Merge pull request #71 from 0xrsydn/feat/quote-behind-session

feat: flag quotes behind the latest IDX session (#50)
This commit is contained in:
brutaljokerz 2026-10-19 17:43:38 +07:00 • committed by GitHub
commit 64c25ad0a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
98 changed files with 9648 additions and 55 deletions

266
Cargo.lock generated
View file

@ -380,6 +380,29 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "cssparser"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf",
"smallvec",
]
[[package]]
name = "cssparser-macros"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
dependencies = [
"quote",
"syn 3.0.9",
]
[[package]] [[package]]
name = "csv" name = "csv"
version = "1.4.0" version = "1.4.0"
@ -444,6 +467,27 @@ dependencies = [
"powerfmt", "powerfmt",
] ]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn 2.0.117",
]
[[package]] [[package]]
name = "difflib" name = "difflib"
version = "0.4.0" version = "0.4.0"
@ -501,12 +545,33 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "dtoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590"
[[package]]
name = "dtoa-short"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87"
dependencies = [
"dtoa",
]
[[package]] [[package]]
name = "dyn-clone" name = "dyn-clone"
version = "1.0.20" version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ego-tree"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b04dc5a38e4f151a79d9f2451ae6037fb6eaf5cba34771f44781f80e508498e3"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "1.0.0" version = "1.0.0"
@ -732,6 +797,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "html5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
"markup5ever",
]
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.0"
@ -910,6 +985,7 @@ dependencies = [
"fastrand", "fastrand",
"quick-xml", "quick-xml",
"rusqlite", "rusqlite",
"scraper",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2",
@ -1025,6 +1101,17 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "markup5ever"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
"tendril",
"web_atoms",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@ -1047,6 +1134,12 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.31.3" version = "0.31.3"
@ -1149,6 +1242,59 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
dependencies = [
"phf_macros",
"phf_shared",
"serde",
]
[[package]]
name = "phf_codegen"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
dependencies = [
"fastrand",
"phf_shared",
]
[[package]]
name = "phf_macros"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "phf_shared"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -1176,6 +1322,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]] [[package]]
name = "predicates" name = "predicates"
version = "3.1.4" version = "3.1.4"
@ -1377,6 +1529,21 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "rustc-hash"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "1.1.4" version = "1.1.4"
@ -1469,6 +1636,45 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scraper"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdd0be4d296f048bfb06dd01bbc80ef789ddd2e55583e8d2e6b804942abfabc2"
dependencies = [
"cssparser",
"ego-tree",
"html5ever",
"precomputed-hash",
"selectors",
"tendril",
]
[[package]]
name = "selectors"
version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags",
"cssparser",
"derive_more",
"log",
"new_debug_unreachable",
"phf",
"phf_codegen",
"precomputed-hash",
"rustc-hash",
"servo_arc",
"smallvec",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -1532,6 +1738,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "servo_arc"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930"
dependencies = [
"stable_deref_trait",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.9" version = "0.10.9"
@ -1555,6 +1770,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "siphasher"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@ -1573,6 +1794,30 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "string_cache"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "string_cache_codegen"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@ -1618,6 +1863,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "tendril"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08"
dependencies = [
"new_debug_unreachable",
]
[[package]] [[package]]
name = "termtree" name = "termtree"
version = "0.5.1" version = "0.5.1"
@ -2005,6 +2259,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "web_atoms"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3"
dependencies = [
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
]
[[package]] [[package]]
name = "webpki-roots" name = "webpki-roots"
version = "1.0.6" version = "1.0.6"

View file

@ -11,6 +11,13 @@ history lives in git.
- `screen presets` / `screen run <PRESET>`: built-in screener presets - `screen presets` / `screen run <PRESET>`: built-in screener presets
(graduate `screen` out of `stocks` only if these grow into a richer workflow) (graduate `screen` out of `stocks` only if these grow into a richer workflow)
- Multi-period financial statements (today `financials` is single-period) - Multi-period financial statements (today `financials` is single-period)
- Order book (bid/offer depth): no free, login-free source exists. IDX sells
delayed depth to vendors and Stockbit's needs a user login. Revisit if a
permitted source appears; do not ship the closing best bid/offer as one.
- Bandarmology follow-ups: a daily series (per-session readings and the
cumulative top-N net) and a price-move phase (quiet accumulation, markup),
calibration of the `top_concentration_v1` thresholds across more sessions,
and a market-wide foreign flow screen from one IDX trading summary
- News beyond MSN in `news` / `get_company` (`docs/IDX_NEWS.md`): Google News - News beyond MSN in `news` / `get_company` (`docs/IDX_NEWS.md`): Google News
RSS per ticker, and IDX disclosures and exchange news through a RSS per ticker, and IDX disclosures and exchange news through a
maintainer-published feed maintainer-published feed
@ -26,6 +33,12 @@ history lives in git.
- Composite `analyze` command (quote, fundamentals, ownership, sentiment) - Composite `analyze` command (quote, fundamentals, ownership, sentiment)
## Known limits ## Known limits
- `stocks broker` and `stocks bandar` show only IPOT's top 10 buyers and
sellers, ranked by gross lots, with figures of a million and more rounded
to one decimal.
- `stocks flow --source idx` depends on IDX's Cloudflare passing a
curl-impersonate profile (only `curl_safari184` in October 2026) and on the
network; the default IPOT source rounds to one decimal.
- Yahoo can return `429` from datacenter IPs; requests retry with backoff but - Yahoo can return `429` from datacenter IPs; requests retry with backoff but
can still fail. can still fail.
- `stocks technical` reports the SMA200 trend as unavailable when fewer than - `stocks technical` reports the SMA200 trend as unavailable when fewer than

View file

@ -4,18 +4,26 @@ use clap::{Args, Subcommand};
use crate::config::{HistoryProviderKind, IdxConfig}; use crate::config::{HistoryProviderKind, IdxConfig};
use crate::output::{ use crate::output::{
render_compare, render_earnings, render_financials, render_fundamental, render_growth, render_bandar, render_broker_summary, render_compare, render_earnings, render_financials,
render_history, render_insights, render_news, render_profile, render_quotes, render_risk, render_foreign_flow, render_fundamental, render_growth, render_history, render_insights,
render_screener, render_sentiment, render_technical, render_valuation, render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment,
render_technical, render_valuation,
}; };
use idx_core::analysis::BandarQuery;
use idx_core::analysis::bandar::DEFAULT_TOP;
use idx_core::api::types::{Interval, Period}; use idx_core::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter, StatementKind}; use idx_core::financial::{EarningsFilter, StatementFilter, StatementKind};
use idx_core::flow::types::{FlowRange, FlowSource};
use idx_core::market::ScreenQuery; use idx_core::market::ScreenQuery;
use idx_core::{CachePolicy, Idx, runtime}; use idx_core::{CachePolicy, Idx, runtime};
/// Splits comma-separated symbol arguments; `-` reads symbols from stdin, /// Splits comma-separated symbol arguments; `-` reads symbols from stdin,
/// separated by whitespace or commas. /// separated by whitespace or commas.
/// Most symbols one `bandar` screen fetches, one request each.
const MAX_BANDAR_SYMBOLS: usize = 100;
fn expand_symbols(args: &[String]) -> Result<Vec<String>, IdxError> { fn expand_symbols(args: &[String]) -> Result<Vec<String>, IdxError> {
let mut symbols = Vec::new(); let mut symbols = Vec::new();
for arg in args { for arg in args {
@ -76,6 +84,37 @@ impl From<EarningsFilterArgs> for EarningsFilter {
} }
} }
/// Which sessions and trades a broker command covers.
#[derive(Debug, Clone, Args)]
pub struct BrokerScopeArgs {
/// One session, YYYY-MM-DD. Default: the latest session with trades.
#[arg(long, conflicts_with_all = ["from", "to"])]
date: Option<String>,
/// First session of a range, YYYY-MM-DD (at most 366 days).
#[arg(long)]
from: Option<String>,
/// Last session of the range, YYYY-MM-DD. Default: today.
#[arg(long, requires = "from")]
to: Option<String>,
/// Whose trades to count.
#[arg(long, value_enum, default_value_t = BrokerInvestor::All)]
investor: BrokerInvestor,
/// Which board's trades to count; `regular` leaves out negotiated block trades.
#[arg(long, value_enum, default_value_t = BrokerBoard::All)]
board: BrokerBoard,
}
impl BrokerScopeArgs {
fn range(&self, idx: &Idx) -> Result<BrokerRange, IdxError> {
BrokerRange::from_args(
self.date.as_deref(),
self.from.as_deref(),
self.to.as_deref(),
idx.broker().today(),
)
}
}
#[derive(Debug, Args)] #[derive(Debug, Args)]
#[command(about = "Stock data and analysis")] #[command(about = "Stock data and analysis")]
pub struct StocksCmd { pub struct StocksCmd {
@ -150,6 +189,65 @@ pub enum StocksSubcommand {
/// Single ticker symbol (e.g. BBCA). /// Single ticker symbol (e.g. BBCA).
symbol: String, symbol: String,
}, },
#[command(
about = "Which brokers bought and sold a stock",
long_about = "Which brokers bought and sold a stock: the top 10 buyers and top 10 \
sellers by lots, with value and average price, from Indo Premier (IPOT). End of day: IDX \
does not show broker codes during trading. Without dates, the latest session with trades.",
after_help = "Examples:\n idx stocks broker BBCA\n idx stocks broker BBCA --date 2026-10-16\n idx stocks broker BBCA --from 2026-10-12 --to 2026-10-16 --board regular\n idx -o json stocks broker BBCA --investor foreign"
)]
Broker {
/// Single ticker symbol (e.g. BBCA).
symbol: String,
#[command(flatten)]
scope: BrokerScopeArgs,
},
#[command(
about = "Bandarmology: is buying more concentrated than selling",
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). `--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 -"
)]
Bandar {
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
#[arg(required = true)]
symbols: Vec<String>,
#[command(flatten)]
scope: BrokerScopeArgs,
/// Tier behind the headline signal: 1, 3, 5 or 10 brokers per side.
#[arg(long, default_value_t = DEFAULT_TOP)]
top: u8,
},
#[command(
about = "Foreign buying and selling of a stock, session by session",
long_about = "Foreign buying and selling of a stock, session by session, end of day. By \
default from Indo Premier (IPOT): foreign buy, sell and net value across all boards and \
foreign investors' share of trading, two plain requests per session. `--source idx` reads \
IDX's own trading summary instead: exact foreign share counts, the close and the share \
traded outside the regular board, one request per session to idx.co.id, which needs \
curl-impersonate and a network IDX's Cloudflare lets through.",
after_help = "Examples:\n idx stocks flow BBCA\n idx stocks flow BBCA --days 20\n idx stocks flow BBCA --from 2026-10-01 --to 2026-10-16\n idx stocks flow BBCA --source idx\n idx -o json stocks flow BBCA | jq '.foreign_net_value'"
)]
Flow {
/// Single ticker symbol (e.g. BBCA).
symbol: String,
/// Latest sessions to show (default 10, at most 60).
#[arg(long, conflicts_with_all = ["from", "to"])]
days: Option<usize>,
/// First session of a range, YYYY-MM-DD (at most 92 days).
#[arg(long)]
from: Option<String>,
/// Last session of the range, YYYY-MM-DD. Default: today.
#[arg(long, requires = "from")]
to: Option<String>,
/// Where the figures come from.
#[arg(long, value_enum, default_value_t = FlowSource::Ipot)]
source: FlowSource,
},
#[command(about = "Get company profile")] #[command(about = "Get company profile")]
Profile { symbol: String }, Profile { symbol: String },
#[command( #[command(
@ -256,6 +354,53 @@ pub fn handle(
let report = idx.analysis().fundamental(symbol, policy)?; let report = idx.analysis().fundamental(symbol, policy)?;
render_fundamental(&report.data, format, no_color) render_fundamental(&report.data, format, no_color)
} }
StocksSubcommand::Broker { symbol, scope } => {
let summary = idx.broker().fetch(
symbol,
scope.range(&idx)?,
scope.investor,
scope.board,
policy,
)?;
render_broker_summary(&summary.data, format)
}
StocksSubcommand::Bandar {
symbols,
scope,
top,
} => {
let symbols = expand_symbols(symbols)?;
if symbols.len() > MAX_BANDAR_SYMBOLS {
return Err(IdxError::InvalidInput(format!(
"at most {MAX_BANDAR_SYMBOLS} symbols per bandar screen (got {})",
symbols.len()
)));
}
let query = BandarQuery {
range: scope.range(&idx)?,
investor: scope.investor,
board: scope.board,
top: *top,
};
let screen = idx.analysis().bandar_screen(&symbols, &query, policy)?;
for (resolved, err) in screen.failures {
runtime::warn(format!("failed to read brokers for {resolved}: {err}"));
}
let reports: Vec<_> = screen.reports.into_iter().map(|r| r.data).collect();
render_bandar(&reports, symbols.len() > 1, format, no_color)
}
StocksSubcommand::Flow {
symbol,
days,
from,
to,
source,
} => {
let range =
FlowRange::from_args(*days, from.as_deref(), to.as_deref(), idx.flow().today())?;
let flow = idx.flow().foreign(symbol, range, *source, policy)?;
render_foreign_flow(&flow.data, format)
}
StocksSubcommand::Profile { symbol } => { StocksSubcommand::Profile { symbol } => {
let profile = idx.financial().profile(symbol, policy)?; let profile = idx.financial().profile(symbol, policy)?;
render_profile(&profile.data, format) render_profile(&profile.data, format)

View file

@ -8,6 +8,7 @@ use std::io::IsTerminal;
use clap::ValueEnum; use clap::ValueEnum;
use serde::Serialize; use serde::Serialize;
use idx_core::analysis::bandar::BandarReport;
use idx_core::analysis::fundamental::{ use idx_core::analysis::fundamental::{
FundamentalReport, GrowthReport, RiskReport, ValuationReport, FundamentalReport, GrowthReport, RiskReport, ValuationReport,
}; };
@ -16,7 +17,9 @@ use idx_core::api::types::{
CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote, CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote,
SentimentData, SentimentData,
}; };
use idx_core::broker::types::BrokerSummary;
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::flow::types::ForeignFlow;
/// Output format requested by the user (flag, `IDX_OUTPUT`, or config file). /// Output format requested by the user (flag, `IDX_OUTPUT`, or config file).
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Serialize, serde::Deserialize, Default)] #[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Serialize, serde::Deserialize, Default)]
@ -73,6 +76,38 @@ pub fn render_quotes(
} }
} }
pub fn render_broker_summary(
summary: &BrokerSummary,
format: &OutputFormat,
) -> Result<(), IdxError> {
match format {
OutputFormat::Table => table::print_broker_summary(summary),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(summary),
}
}
/// One symbol gets the detailed table, several the ranked screen (even when
/// only one of them succeeded); JSON is always the array.
pub fn render_bandar(
reports: &[BandarReport],
screen: bool,
format: &OutputFormat,
no_color: bool,
) -> Result<(), IdxError> {
match (format, reports) {
(OutputFormat::Table, [report]) if !screen => table::print_bandar_report(report, no_color),
(OutputFormat::Table, _) => table::print_bandar_screen(reports, no_color),
(OutputFormat::Json | OutputFormat::Jsonl, _) => json::print_json(reports),
}
}
pub fn render_foreign_flow(flow: &ForeignFlow, format: &OutputFormat) -> Result<(), IdxError> {
match format {
OutputFormat::Table => table::print_foreign_flow(flow),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(flow),
}
}
pub fn render_history( pub fn render_history(
symbol: &str, symbol: &str,
history: &[Ohlc], history: &[Ohlc],

View file

@ -2,6 +2,9 @@ use crate::output::style::Paint;
use crate::output::text::{cell, painted, terminal_safe, truncate_chars}; use crate::output::text::{cell, painted, terminal_safe, truncate_chars};
use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL}; use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
use idx_core::analysis::bandar::{
ACCUMULATION_BPS, BIG_BPS, BandarReport, BandarSignal, METHOD as BANDAR_METHOD,
};
use idx_core::analysis::fundamental::{ use idx_core::analysis::fundamental::{
FundamentalReport, GrowthReport, RiskReport, ValuationReport, FundamentalReport, GrowthReport, RiskReport, ValuationReport,
}; };
@ -11,7 +14,9 @@ use idx_core::api::types::{
CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc,
Quote, SentimentData, Quote, SentimentData,
}; };
use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary};
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::flow::types::{FlowSource, ForeignFlow};
pub fn format_idr(value: i64) -> String { pub fn format_idr(value: i64) -> String {
let sign = if value.is_negative() { "-" } else { "" }; let sign = if value.is_negative() { "-" } else { "" };
@ -89,19 +94,29 @@ pub fn print_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
} }
/// A quote whose last trade is over a week old is almost always a suspended /// A quote whose last trade is over a week old is almost always a suspended
/// stock; say so under the table instead of letting it pass as current. /// stock; say so under the table instead of letting it pass as current. One
/// from before the latest session is usually the provider lagging the close.
fn stale_quote_notes(quotes: &[Quote], now: chrono::DateTime<chrono::Utc>) -> Vec<String> { fn stale_quote_notes(quotes: &[Quote], now: chrono::DateTime<chrono::Utc>) -> Vec<String> {
quotes quotes
.iter() .iter()
.filter_map(|q| { .filter_map(|q| {
let as_of = q.as_of?; let as_of = q.as_of?;
(now.signed_duration_since(as_of) > chrono::Duration::days(7)).then(|| { if now.signed_duration_since(as_of) > chrono::Duration::days(7) {
format!( Some(format!(
"Note: {} last traded on {}; this price is not current.", "Note: {} last traded on {}; this price is not current.",
q.symbol, q.symbol,
as_of.date_naive() as_of.date_naive()
) ))
}) } else if q.behind_session {
Some(format!(
"Note: {} last traded on {}, before the latest IDX session; \
this price may be out of date.",
q.symbol,
as_of.date_naive()
))
} else {
None
}
}) })
.collect() .collect()
} }
@ -893,6 +908,483 @@ fn truncate_url(url: &str) -> String {
truncate_chars(url, 72) truncate_chars(url, 72)
} }
/// A figure at the precision the broker summary source publishes:
/// `841.2B`, `1.6M`, exact below a million.
fn format_compact(value: i64) -> String {
let abs = value.unsigned_abs();
let (unit, suffix) = match abs {
1_000_000_000_000.. => (1e12, "T"),
1_000_000_000.. => (1e9, "B"),
1_000_000.. => (1e6, "M"),
_ => return format_idr(value),
};
format!("{:.1}{suffix}", value as f64 / unit)
}
fn format_compact_option(value: Option<i64>) -> String {
value.map(format_compact).unwrap_or_else(|| "-".to_string())
}
fn broker_cells(flow: Option<&BrokerFlow>) -> [Cell; 4] {
match flow {
Some(flow) => [
cell(&flow.code),
cell(format_compact_option(flow.lots)),
cell(format_compact_option(flow.value)),
cell(
flow.average_price
.map(format_idr)
.unwrap_or_else(|| "-".to_string()),
),
],
None => [cell(""), cell(""), cell(""), cell("")],
}
}
/// `2026-10-16`, or `2026-10-12 to 2026-10-16`, plus any filters.
pub(crate) fn broker_scope(
from: chrono::NaiveDate,
to: chrono::NaiveDate,
investor: BrokerInvestor,
board: BrokerBoard,
) -> String {
let mut scope = if from == to {
from.to_string()
} else {
format!("{from} to {to}")
};
if investor != BrokerInvestor::All {
scope.push_str(&format!(", {} investors", investor.as_str()));
}
if board != BrokerBoard::All {
scope.push_str(&format!(", {} board", board.as_str()));
}
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!(
"{}",
format!(
"Broker summary for {}, {}",
terminal_safe(&summary.symbol),
broker_scope(summary.from, summary.to, summary.investor, summary.board)
)
.bold()
);
if summary.is_empty() {
println!("No trades.");
return Ok(());
}
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"#", "BUYER", "LOTS", "VALUE", "AVG", "SELLER", "LOTS", "VALUE", "AVG",
]);
let rows = summary.buyers.len().max(summary.sellers.len());
for index in 0..rows {
let mut row = vec![cell(index + 1)];
row.extend(broker_cells(summary.buyers.get(index)));
row.extend(broker_cells(summary.sellers.get(index)));
table.add_row(row);
}
println!("{table}");
if summary.total_value.is_some() {
let mut totals = format!(
"Total {} IDR, {} lots",
format_compact_option(summary.total_value),
format_compact_option(summary.total_lots)
);
if let Some(average) = summary.average_price {
totals.push_str(&format!(", average {}", format_idr(average)));
}
if let Some(net) = summary.foreign_net_value {
totals.push_str(&format!("; foreign net {:+}", format_compact(net)));
}
println!("{totals}.");
} 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"),
(BrokerKind::StateOwned, "State-owned brokers"),
] {
let mut codes: Vec<&str> = summary
.buyers
.iter()
.chain(&summary.sellers)
.filter(|flow| flow.kind == Some(kind))
.map(|flow| flow.code.as_str())
.collect();
codes.sort_unstable();
codes.dedup();
if !codes.is_empty() {
println!("{label}: {}.", terminal_safe(&codes.join(", ")));
}
}
println!("Buyers and sellers are each ranked by lots; a broker can be on both sides.");
if summary.rounded {
println!("Note: the source rounds figures of a million and more to one decimal.");
}
Ok(())
}
fn format_bps_pct(bps: Option<i64>) -> String {
bps.map(|bps| format!("{:+.1}%", bps as f64 / 100.0))
.unwrap_or_else(|| "-".to_string())
}
fn format_bps_share(bps: Option<i64>) -> String {
bps.map(|bps| format!("{:.1}%", bps as f64 / 100.0))
.unwrap_or_else(|| "-".to_string())
}
fn format_bandar_signal(signal: Option<BandarSignal>, no_color: bool) -> String {
let Some(signal) = signal else {
return "-".to_string();
};
let label = signal.as_str().replace('_', " ");
if no_color {
return label;
}
match signal {
BandarSignal::BigAccumulation | BandarSignal::Accumulation => label.green(),
BandarSignal::BigDistribution | BandarSignal::Distribution => label.red(),
BandarSignal::Neutral => label,
}
}
fn bandar_method_note(top: u8) -> String {
format!(
"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
)
}
pub fn print_bandar_report(report: &BandarReport, no_color: bool) -> Result<(), IdxError> {
println!(
"{}",
format!(
"Bandar reading for {}, {}",
terminal_safe(&report.symbol),
broker_scope(report.from, report.to, report.investor, report.board)
)
.bold()
);
// Brokers on either side mean trades, even when a total is unavailable.
if report
.tiers
.iter()
.all(|tier| tier.buy_value == Some(0) && tier.sell_value == Some(0))
{
println!("No trades.");
return Ok(());
}
println!(
"Signal (top {}): {}, {} of value",
report.top,
format_bandar_signal(report.signal, no_color),
format_bps_pct(report.imbalance_bps)
);
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"TOP", "BUY", "SELL", "NET", "NET %", "BUY AVG", "SELL AVG", "SIGNAL",
]);
for tier in &report.tiers {
table.add_row(vec![
cell(tier.top),
cell(format_compact_option(tier.buy_value)),
cell(format_compact_option(tier.sell_value)),
cell(format_compact_option(tier.imbalance_value)),
cell(format_bps_pct(tier.imbalance_bps)),
cell(
tier.buy_average
.map(format_idr)
.unwrap_or_else(|| "-".to_string()),
),
cell(
tier.sell_average
.map(format_idr)
.unwrap_or_else(|| "-".to_string()),
),
painted(format_bandar_signal(tier.signal, no_color)),
]);
}
println!("{table}");
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 !report.both_sides.is_empty() {
println!(
"On both sides: {}; their gross figures overstate which way they traded.",
terminal_safe(&report.both_sides.join(", "))
);
}
println!("{}", bandar_method_note(report.top));
if report.rounded {
println!("Note: the source rounds figures of a million and more to one decimal.");
}
Ok(())
}
pub fn print_bandar_screen(reports: &[BandarReport], no_color: bool) -> Result<(), IdxError> {
let top = reports.first().map(|report| report.top).unwrap_or_default();
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"SYMBOL".to_string(),
"DATES".to_string(),
"SIGNAL".to_string(),
format!("TOP {top} NET %"),
"BUY SHARE".to_string(),
"SELL SHARE".to_string(),
"VALUE".to_string(),
"FOREIGN NET".to_string(),
]);
for report in reports {
let headline = report.tiers.iter().find(|tier| tier.top == report.top);
table.add_row(vec![
cell(&report.symbol),
cell(broker_scope(
report.from,
report.to,
BrokerInvestor::All,
BrokerBoard::All,
)),
painted(format_bandar_signal(report.signal, no_color)),
cell(format_bps_pct(report.imbalance_bps)),
cell(format_bps_share(
headline.and_then(|tier| tier.buy_share_bps),
)),
cell(format_bps_share(
headline.and_then(|tier| tier.sell_share_bps),
)),
cell(format_compact_option(report.total_value)),
cell(
report
.foreign_net_value
.map(|net| format!("{:+}", format_compact(net)))
.unwrap_or_else(|| "-".to_string()),
),
]);
}
println!("{table}");
if let Some(first) = reports.first()
&& (first.investor, first.board) != (BrokerInvestor::All, BrokerBoard::All)
{
println!(
"Counting {} investors on the {} board.",
first.investor.as_str(),
first.board.as_str()
);
}
println!("{}", bandar_method_note(top));
Ok(())
}
fn format_signed_compact(value: Option<i64>) -> String {
value
.map(|value| {
let text = format_compact(value);
if value > 0 { format!("+{text}") } else { text }
})
.unwrap_or_else(|| "-".to_string())
}
pub fn print_foreign_flow(flow: &ForeignFlow) -> Result<(), IdxError> {
let span = if flow.from == flow.to {
flow.from.to_string()
} else {
format!("{} to {}", flow.from, flow.to)
};
let sessions = flow.days.len();
println!(
"{}",
format!(
"Foreign flow for {}, {span} ({sessions} session{}, {})",
terminal_safe(&flow.symbol),
if sessions == 1 { "" } else { "s" },
match flow.source {
FlowSource::Ipot => "IPOT",
FlowSource::Idx => "IDX",
}
)
.bold()
);
match flow.source {
FlowSource::Ipot => print_ipot_flow(flow),
FlowSource::Idx => print_idx_flow(flow),
}
if flow.rounded {
println!("Note: the source rounds figures of a million and more to one decimal.");
}
Ok(())
}
fn print_ipot_flow(flow: &ForeignFlow) {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"DATE",
"VALUE",
"FOREIGN BUY",
"FOREIGN SELL",
"FOREIGN NET",
"FOREIGN %",
]);
for day in &flow.days {
table.add_row(vec![
cell(day.date),
cell(format_compact_option(day.value)),
cell(format_compact_option(day.foreign_buy_value)),
cell(format_compact_option(day.foreign_sell_value)),
cell(format_signed_compact(day.foreign_net_value)),
cell(format_bps_share(day.foreign_share_bps)),
]);
}
println!("{table}");
let mut summary = format!(
"Foreign investors bought {} IDR and sold {} IDR: net {}",
format_compact_option(flow.foreign_buy_value),
format_compact_option(flow.foreign_sell_value),
format_signed_compact(flow.foreign_net_value)
);
if let Some(share) = flow.foreign_share_bps {
summary.push_str(&format!(
"; they made up {} of trading",
format_bps_share(Some(share))
));
}
println!("{summary}.");
println!(
"Values in IDR across all boards. For exact share counts, the close and the non-regular \
share, use --source idx (needs curl-impersonate)."
);
}
fn print_idx_flow(flow: &ForeignFlow) {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"DATE",
"CLOSE",
"CHG%",
"VALUE",
"FOREIGN BUY",
"FOREIGN SELL",
"FOREIGN NET",
"NET VALUE",
"NON-REG",
]);
for day in &flow.days {
table.add_row(vec![
cell(day.date),
cell(day.close.map(format_idr).unwrap_or_else(|| "-".to_string())),
cell(format_bps_pct(day.change_bps)),
cell(format_compact_option(day.value)),
cell(format_compact_option(day.foreign_buy)),
cell(format_compact_option(day.foreign_sell)),
cell(format_signed_compact(day.foreign_net)),
cell(format_signed_compact(day.foreign_net_value)),
cell(format_bps_share(day.non_regular_bps)),
]);
}
println!("{table}");
let mut summary = format!(
"Net foreign: {} shares, about {} IDR",
format_signed_compact(flow.foreign_net),
format_signed_compact(flow.foreign_net_value)
);
if let Some(share) = flow.foreign_share_bps {
summary.push_str(&format!(
"; foreign investors made up {} of trading",
format_bps_share(Some(share))
));
}
println!("{summary}.");
println!(
"Volume, value and foreign figures are the regular board, volumes in shares. Net value \
is estimated at each session's average price; NON-REG is the share of all value traded \
outside the regular board (negotiated and cash)."
);
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
@ -917,6 +1409,7 @@ mod tests {
prev_close: None, prev_close: None,
avg_volume: None, avg_volume: None,
as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()), as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()),
behind_session: false,
} }
} }
@ -936,6 +1429,30 @@ mod tests {
); );
} }
#[test]
fn notes_quotes_behind_the_latest_session() {
let now = chrono::DateTime::parse_from_rfc3339("2026-10-16T16:53:00+07:00")
.unwrap()
.to_utc();
let mut behind = quote_as_of("BBCA.JK", "2026-10-15T16:15:00+07:00");
behind.behind_session = true;
let mut suspended = quote_as_of("WIKA.JK", "2025-02-17T16:14:58+07:00");
suspended.behind_session = true;
let quotes = [
behind,
suspended,
quote_as_of("BBRI.JK", "2026-10-16T16:15:00+07:00"),
];
assert_eq!(
stale_quote_notes(&quotes, now),
vec![
"Note: BBCA.JK last traded on 2026-10-15, before the latest IDX session; \
this price may be out of date.",
"Note: WIKA.JK last traded on 2025-02-17; this price is not current.",
]
);
}
#[test] #[test]
fn formats_idr_numbers() { fn formats_idr_numbers() {
assert_eq!(format_idr(9875), "9,875"); assert_eq!(format_idr(9875), "9,875");

View file

@ -2407,6 +2407,68 @@ fn msn_screen_ranks_complete_candidates_and_caches_before_limit() {
} }
} }
#[test]
fn msn_screen_flags_idx_rows_behind_the_session() {
let root = test_env_dir("screen-behind-session");
let fixture = write_large_screener_fixture(&root, Some(503));
let behind = |rows: &serde_json::Value| -> Vec<bool> {
rows.as_array()
.unwrap()
.iter()
.map(|row| row["behind_session"].as_bool().unwrap())
.collect()
};
// Every fixture row last traded on 2026-01-02, long before any session now.
let fresh = run_success_json(&mut screener_fixture_command(
&root,
&fixture,
"top-performers",
3,
));
assert_eq!(behind(&fresh), [true; 3]);
let mut offline = screener_fixture_command(&root, &fixture, "top-performers", 3);
offline.env("IDX_MOCK_ERROR", "1").arg("--offline");
assert_eq!(behind(&run_success_json(&mut offline)), [true; 3]);
// IDX's session rule says nothing about other exchanges.
let mut us = screener_fixture_command(&root, &fixture, "top-performers", 3);
us.args(["--region", "us"]);
assert_eq!(behind(&run_success_json(&mut us)), [false; 3]);
fs::remove_dir_all(root).unwrap();
}
#[test]
fn quote_read_from_a_0_3_3_cache_entry_is_behind_the_session() {
// IDX's session rule says nothing about other exchanges.
for (symbol, file, behind) in [
("BBCA", "BBCA.JK.json", true),
("AAPL.US", "AAPL.US.json", false),
] {
let root = test_env_dir(&format!("quote-behind-session-{symbol}"));
let mut warm = mock_bin_with_root(&root);
run_success_json(warm.args(["-o", "json", "stocks", "quote", symbol]));
let entry = fs::read_dir(root.join("cache"))
.unwrap()
.flat_map(|dir| fs::read_dir(dir.unwrap().path()).unwrap())
.flat_map(|bucket| fs::read_dir(bucket.unwrap().path()).unwrap())
.map(|file| file.unwrap().path())
.find(|path| path.ends_with(file))
.expect("cached quote");
// 0.3.3 wrote quotes without the field; this one last traded long ago.
let mut cached: serde_json::Value =
serde_json::from_str(&fs::read_to_string(&entry).unwrap()).unwrap();
let data = cached["data"].as_object_mut().unwrap();
data.remove("behind_session")
.expect("field is cached unmarked");
data.insert("as_of".into(), "2026-01-02T16:15:00+07:00".into());
fs::write(&entry, cached.to_string()).unwrap();
let mut offline = mock_bin_with_root(&root);
let quotes =
run_success_json(offline.args(["--offline", "-o", "json", "stocks", "quote", symbol]));
assert_eq!(quotes[0]["behind_session"], behind, "{symbol}");
fs::remove_dir_all(root).unwrap();
}
}
#[test] #[test]
fn msn_screen_rejects_invalid_totals_without_populating_cache() { fn msn_screen_rejects_invalid_totals_without_populating_cache() {
for (name, count) in [("missing", None), ("incomplete", Some(504))] { for (name, count) in [("missing", None), ("incomplete", Some(504))] {
@ -2434,3 +2496,629 @@ fn msn_screen_rejects_invalid_totals_without_populating_cache() {
fs::remove_dir_all(root).unwrap(); fs::remove_dir_all(root).unwrap();
} }
} }
/// A command on the fixture-backed mock provider, never the network.
fn mock_bin(name: &str) -> Command {
let mut cmd = test_bin(name);
cmd.env("IDX_USE_MOCK_PROVIDER", "1");
cmd
}
fn mock_bin_with_root(root: &Path) -> Command {
let mut cmd = bin_with_root(root);
cmd.env("IDX_USE_MOCK_PROVIDER", "1");
cmd
}
#[test]
fn broker_summary_json_names_brokers_and_keeps_source_totals() {
let summary = run_success_json(mock_bin("broker-json").args([
"-o",
"json",
"stocks",
"broker",
"bbca",
"--date",
"2026-10-16",
]));
assert_eq!(summary["symbol"], "BBCA.JK");
assert_eq!(
(summary["from"].as_str(), summary["to"].as_str()),
(Some("2026-10-16"), Some("2026-10-16"))
);
assert_eq!(summary["buyers"].as_array().unwrap().len(), 10);
let top = &summary["buyers"][0];
assert_eq!(top["code"], "MG");
assert_eq!(top["name"], "SEMESTA INDOVEST SEKURITAS");
assert_eq!(top["kind"], "local");
assert_eq!(top["lots"], 1_600_000);
assert_eq!(top["value"], 841_200_000_000_i64);
assert_eq!(top["average_price"], 5_191);
assert_eq!(summary["sellers"][0]["kind"], "state_owned");
assert_eq!(summary["total_value"], 6_600_000_000_000_i64);
assert_eq!(summary["foreign_net_value"], -2_300_000_000_000_i64);
assert_eq!(summary["rounded"], true);
// The source does not filter its foreign net figure, so a filtered
// summary leaves it out rather than passing it off as filtered.
let filtered = run_success_json(mock_bin("broker-filtered").args([
"-o",
"json",
"stocks",
"broker",
"BBCA",
"--date",
"2026-10-16",
"--investor",
"foreign",
"--board",
"regular",
]));
assert_eq!(filtered["investor"], "foreign");
assert_eq!(filtered["board"], "regular");
assert_eq!(filtered["buyers"][0]["code"], "KZ");
for field in [
"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",
"BBCA",
"--date",
"2026-10-16",
"--investor",
"foreign",
"--board",
"regular",
]));
assert!(!table.contains("Total "), "{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]
fn broker_summary_without_dates_finds_the_latest_session_offline() {
let root = test_env_dir("broker-latest");
run_success_json(mock_bin_with_root(&root).args([
"-o",
"json",
"stocks",
"broker",
"BBCA",
"--date",
"2026-10-16",
]));
// Monday 2026-10-19 is not cached and the weekend is skipped, so the
// offline lookup lands on Friday's cached session.
let latest = run_success_json(
mock_bin_with_root(&root)
.env("IDX_MOCK_TODAY", "2026-10-19")
.args(["-o", "json", "--offline", "stocks", "broker", "BBCA"]),
);
assert_eq!(latest["from"], "2026-10-16");
assert_eq!(latest["buyers"][0]["code"], "MG");
run_error_json(
mock_bin_with_root(&root)
.env("IDX_MOCK_TODAY", "2026-10-19")
.args(["-o", "json", "--offline", "stocks", "broker", "BBRI"]),
"CACHEMISS",
);
}
#[test]
fn broker_summary_session_without_trades_is_empty_not_an_error() {
let summary = run_success_json(mock_bin("broker-empty").args([
"-o",
"json",
"stocks",
"broker",
"BBCA",
"--date",
"2026-10-17",
]));
assert_eq!(summary["buyers"], serde_json::json!([]));
assert_eq!(summary["total_value"], 0);
assert!(summary["average_price"].is_null());
let table = run_success_stdout(mock_bin("broker-empty-table").args([
"stocks",
"broker",
"BBCA",
"--date",
"2026-10-17",
]));
assert!(table.contains("No trades."), "{table}");
}
#[test]
fn broker_summary_errors_follow_the_output_contract() {
for (args, code) in [
(vec!["ZZZZ", "--date", "2026-10-16"], "SYMBOLNOTFOUND"),
(vec!["BBCA.US", "--date", "2026-10-16"], "UNSUPPORTED"),
(vec!["BBCA", "--date", "16/10/2026"], "INVALIDINPUT"),
(
vec!["BBCA", "--from", "2026-10-16", "--to", "2026-10-12"],
"INVALIDINPUT",
),
(vec!["BBCA", "--to", "2026-10-16"], "INVALIDINPUT"),
(
vec!["BBCA", "--date", "2026-10-16", "--from", "2026-10-12"],
"INVALIDINPUT",
),
(vec!["BBCA", "--date", "2999-01-01"], "INVALIDINPUT"),
] {
let mut cmd = mock_bin("broker-errors");
cmd.args(["-o", "json", "stocks", "broker"]).args(&args);
run_error_json(&mut cmd, code);
}
let mut failing = mock_bin("broker-provider-error");
failing.env("IDX_MOCK_ERROR", "1").args([
"-o",
"json",
"stocks",
"broker",
"BBCA",
"--date",
"2026-10-16",
]);
run_error_json(&mut failing, "PROVIDERUNAVAILABLE");
}
#[test]
fn bandar_reads_the_broker_summary_tiers() {
let reports = run_success_json(mock_bin("bandar-json").args([
"-o",
"json",
"stocks",
"bandar",
"BBCA",
"--date",
"2026-10-16",
]));
let report = &reports[0];
assert_eq!(reports.as_array().unwrap().len(), 1);
assert_eq!(report["symbol"], "BBCA.JK");
assert_eq!(report["method"], "top_concentration_v1");
let tiers: Vec<i64> = report["tiers"]
.as_array()
.unwrap()
.iter()
.map(|tier| tier["top"].as_i64().unwrap())
.collect();
assert_eq!(tiers, [1, 3, 5, 10]);
// Top 5 bought 2,939.5B and sold 4,540.4B of 6.6T traded.
let top5 = &report["tiers"][2];
assert_eq!(top5["buy_value"], 2_939_500_000_000_i64);
assert_eq!(top5["sell_value"], 4_540_400_000_000_i64);
assert_eq!(top5["imbalance_bps"], -2425);
assert_eq!(report["top"], 5);
assert_eq!(report["imbalance_bps"], -2425);
assert_eq!(report["signal"], "big_distribution");
assert_eq!(
report["both_sides"],
serde_json::json!(["AK", "CC", "MG", "XL", "YU", "ZP"])
);
}
#[test]
fn bandar_screen_ranks_symbols_and_reports_failures_on_stderr() {
let output = mock_bin("bandar-screen")
.args([
"-o",
"json",
"stocks",
"bandar",
"BBCA,ZZZZ",
"TLKM",
"BBRI",
"--date",
"2026-10-16",
"--top",
"3",
])
.output()
.unwrap();
assert!(output.status.success(), "{output:?}");
let reports: Value = serde_json::from_slice(&output.stdout).unwrap();
let ranked: Vec<(&str, i64)> = reports
.as_array()
.unwrap()
.iter()
.map(|r| {
(
r["symbol"].as_str().unwrap(),
r["imbalance_bps"].as_i64().unwrap(),
)
})
.collect();
assert_eq!(ranked.len(), 3);
assert!(
ranked.windows(2).all(|pair| pair[0].1 >= pair[1].1),
"{ranked:?}"
);
assert!(reports.as_array().unwrap().iter().all(|r| r["top"] == 3));
let warning: Value = serde_json::from_slice(&output.stderr).expect("one JSON warning line");
assert_eq!(warning["level"], "warning");
assert!(warning["message"].as_str().unwrap().contains("ZZZZ.JK"));
// A one-symbol screen that loses a symbol still prints the screen table.
let table = run_success_stdout(mock_bin("bandar-screen-table").args([
"--no-color",
"stocks",
"bandar",
"BBCA,ZZZZ",
"--date",
"2026-10-16",
]));
assert!(table.contains("TOP 5 NET %"), "{table}");
}
#[test]
fn bandar_errors_follow_the_output_contract() {
let many: Vec<String> = (0..101).map(|i| format!("S{i}")).collect();
for (args, code) in [
(
vec!["BBCA".to_string(), "--top".into(), "4".into()],
"INVALIDINPUT",
),
(vec![many.join(",")], "INVALIDINPUT"),
(
vec!["ZZZZ".to_string(), "--date".into(), "2026-10-16".into()],
"SYMBOLNOTFOUND",
),
(
vec![
"BBCA,BBRI".to_string(),
"--date".into(),
"2999-01-01".into(),
],
"INVALIDINPUT",
),
(
vec![
"BBCA".to_string(),
"--date".into(),
"2026-10-16".into(),
"--investor".into(),
"foreign".into(),
"--board".into(),
"regular".into(),
],
"INVALIDINPUT",
),
] {
let mut cmd = mock_bin("bandar-errors");
cmd.args(["-o", "json", "stocks", "bandar"]).args(&args);
run_error_json(&mut cmd, code);
}
}
#[test]
fn foreign_flow_from_idx_reads_sessions_oldest_first() {
let flow = run_success_json(
mock_bin("flow-json")
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o", "json", "stocks", "flow", "BBCA", "--days", "3", "--source", "idx",
]),
);
assert_eq!(flow["symbol"], "BBCA.JK");
let dates: Vec<&str> = flow["days"]
.as_array()
.unwrap()
.iter()
.map(|day| day["date"].as_str().unwrap())
.collect();
assert_eq!(dates, ["2026-10-14", "2026-10-15", "2026-10-16"]);
let last = &flow["days"][2];
assert_eq!(last["close"], 5175);
assert_eq!(last["change_bps"], -1481);
assert_eq!(last["foreign_net"], 309_392_800_i64 - 757_227_900_i64);
assert_eq!(flow["from"], "2026-10-14");
// A range skips the weekend and the Saturday without trading.
let range = run_success_json(
mock_bin("flow-range")
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o",
"json",
"stocks",
"flow",
"--source",
"idx",
"BBCA",
"--from",
"2026-10-12",
"--to",
"2026-10-17",
]),
);
assert_eq!(range["days"].as_array().unwrap().len(), 5);
let net: i64 = range["days"]
.as_array()
.unwrap()
.iter()
.map(|day| day["foreign_net"].as_i64().unwrap())
.sum();
assert_eq!(range["foreign_net"], net);
}
#[test]
fn foreign_flow_from_idx_offline_uses_cached_sessions_only() {
let root = test_env_dir("flow-offline");
run_success_json(
mock_bin_with_root(&root)
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o",
"json",
"stocks",
"flow",
"--source",
"idx",
"BBCA",
"--from",
"2026-10-15",
"--to",
"2026-10-16",
]),
);
// Another stock's flow for the same sessions needs no new request.
let offline = run_success_json(
mock_bin_with_root(&root)
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o",
"json",
"--offline",
"stocks",
"flow",
"--source",
"idx",
"TLKM",
"--days",
"5",
]),
);
assert_eq!(offline["from"], "2026-10-15");
assert_eq!(offline["days"].as_array().unwrap().len(), 2);
run_error_json(
mock_bin_with_root(&root)
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o",
"json",
"--offline",
"stocks",
"flow",
"--source",
"idx",
"TLKM",
"--from",
"2026-10-12",
"--to",
"2026-10-16",
]),
"CACHEMISS",
);
}
#[test]
fn foreign_flow_from_ipot_by_default_gives_values_across_all_boards() {
let flow = run_success_json(
mock_bin("flow-ipot")
.env("IDX_MOCK_TODAY", "2026-10-18")
.args([
"-o",
"json",
"stocks",
"flow",
"BBCA",
"--from",
"2026-10-12",
"--to",
"2026-10-17",
]),
);
assert_eq!(flow["source"], "ipot");
assert_eq!(flow["rounded"], true);
let days = flow["days"].as_array().unwrap();
assert_eq!(days.len(), 5, "the Saturday had no trading");
let last = &days[4];
assert_eq!(last["date"], "2026-10-16");
assert_eq!(last["value"], 6_600_000_000_000_i64);
assert_eq!(last["foreign_buy_value"], 1_700_000_000_000_i64);
assert_eq!(last["foreign_sell_value"], 4_000_000_000_000_i64);
assert_eq!(last["foreign_net_value"], -2_300_000_000_000_i64);
for field in ["close", "foreign_buy", "foreign_net", "non_regular_bps"] {
assert!(last[field].is_null(), "{field}");
}
let net: i64 = days
.iter()
.map(|day| day["foreign_net_value"].as_i64().unwrap())
.sum();
assert_eq!(flow["foreign_net_value"], net);
assert!(flow["foreign_net"].is_null());
let table = run_success_stdout(
mock_bin("flow-ipot-table")
.env("IDX_MOCK_TODAY", "2026-10-18")
.args(["stocks", "flow", "BBCA", "--days", "1"]),
);
assert!(table.contains("(1 session, IPOT)"), "{table}");
assert!(table.contains("FOREIGN %"), "{table}");
}
#[test]
fn foreign_flow_errors_follow_the_output_contract() {
for (args, code) in [
(vec!["ZZZZ", "--days", "2"], "SYMBOLNOTFOUND"),
(vec!["BBCA.US"], "UNSUPPORTED"),
(vec!["BBCA", "--days", "61"], "INVALIDINPUT"),
(vec!["BBCA", "--days", "0"], "INVALIDINPUT"),
(
vec!["BBCA", "--days", "3", "--from", "2026-10-12"],
"INVALIDINPUT",
),
(
vec!["BBCA", "--from", "2026-06-01", "--to", "2026-10-16"],
"INVALIDINPUT",
),
(vec!["BBCA", "--source", "stockbit"], "INVALIDINPUT"),
(
vec!["ZZZZ", "--days", "2", "--source", "idx"],
"SYMBOLNOTFOUND",
),
] {
let mut cmd = mock_bin("flow-errors");
cmd.env("IDX_MOCK_TODAY", "2026-10-18")
.args(["-o", "json", "stocks", "flow"])
.args(&args);
run_error_json(&mut cmd, code);
}
let mut failing = mock_bin("flow-provider-error");
failing
.env("IDX_MOCK_ERROR", "1")
.env("IDX_MOCK_TODAY", "2026-10-18")
.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}"
);
}
#[test]
fn bandar_never_reads_a_broker_summary_cached_in_the_old_shape() {
// A summary cached before buy_value/sell_value existed: fresh for a
// week, with trades, but no denominators for a reading.
let root = test_env_dir("bandar-legacy-cache");
let bucket = root.join("cache").join("idx").join("ipot-broker");
fs::create_dir_all(&bucket).unwrap();
let flow = |code: &str, value: i64| {
serde_json::json!({"rank": 1, "code": code, "name": null, "kind": null,
"lots": value / 100, "value": value, "average_price": 1})
};
let legacy = serde_json::json!({
"fetched_at": chrono::Utc::now().to_rfc3339(),
"ttl_secs": 7 * 24 * 3600,
"schema_version": 1,
"data": {
"symbol": "BBCA.JK", "from": "2026-10-16", "to": "2026-10-16",
"investor": "all", "board": "all",
"buyers": [flow("AA", 60_000)], "sellers": [flow("BB", 40_000)],
"total_value": 100_000, "total_lots": 1_000, "average_price": 100,
"foreign_net_value": null, "rounded": false
}
});
fs::write(
bucket.join("BBCA-2026-10-16-2026-10-16-all-all.json"),
legacy.to_string(),
)
.unwrap();
// Offline, the old entry is not served as a complete summary.
run_error_json(
mock_bin_with_root(&root).args([
"-o",
"json",
"--offline",
"stocks",
"bandar",
"BBCA",
"--date",
"2026-10-16",
]),
"CACHEMISS",
);
// Online, the summary is fetched again and read in full.
let reports = run_success_json(mock_bin_with_root(&root).args([
"-o",
"json",
"stocks",
"bandar",
"BBCA",
"--date",
"2026-10-16",
]));
assert_eq!(reports[0]["imbalance_bps"], -2425);
let table = run_success_stdout(mock_bin_with_root(&root).args([
"--no-color",
"stocks",
"bandar",
"BBCA",
"--date",
"2026-10-16",
]));
assert!(!table.contains("No trades."), "{table}");
assert!(table.contains("big distribution"), "{table}");
}

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -244,6 +244,48 @@ name = "yahoo-fundamental"
args = ["stocks", "fundamental", "BBCA"] args = ["stocks", "fundamental", "BBCA"]
env = { IDX_PROVIDER = "yahoo" } env = { IDX_PROVIDER = "yahoo" }
# --- stocks: broker summaries (IPOT) and foreign flow (IDX) --------------------
[[case]]
name = "stocks-broker"
args = ["stocks", "broker", "BBCA", "--date", "2026-10-16"]
[[case]]
name = "stocks-broker-range"
args = ["stocks", "broker", "BBCA", "--from", "2026-10-12", "--to", "2026-10-16"]
[[case]]
name = "stocks-broker-no-trades"
args = ["stocks", "broker", "BBCA", "--date", "2026-10-17"]
[[case]]
name = "stocks-bandar"
args = ["stocks", "bandar", "BBCA", "--date", "2026-10-16"]
[[case]]
name = "stocks-bandar-screen"
formats = ["table", "json", "jsonl"]
args = ["stocks", "bandar", "BBCA,BBRI,TLKM", "--date", "2026-10-16", "--top", "3"]
[[case]]
name = "stocks-flow"
formats = ["table", "json", "jsonl"]
args = ["stocks", "flow", "BBCA", "--from", "2026-10-12", "--to", "2026-10-16"]
[[case]]
name = "stocks-flow-idx"
formats = ["table", "json", "jsonl"]
args = ["stocks", "flow", "BBCA", "--from", "2026-10-12", "--to", "2026-10-16", "--source", "idx"]
[[case]]
name = "stocks-flow-idx-latest"
args = ["stocks", "flow", "TLKM", "--days", "3", "--source", "idx"]
env = { IDX_MOCK_TODAY = "2026-10-18" }
[[case]]
name = "error-broker-unknown-symbol"
args = ["stocks", "broker", "ZZZZ", "--date", "2026-10-16"]
# --- errors --------------------------------------------------------------------- # --- errors ---------------------------------------------------------------------
[[case]] [[case]]

View file

@ -174,6 +174,12 @@ _idx() {
idx__help__ownership__resolve,unresolved) idx__help__ownership__resolve,unresolved)
cmd="idx__help__ownership__resolve__unresolved" cmd="idx__help__ownership__resolve__unresolved"
;; ;;
idx__help__stocks,bandar)
cmd="idx__help__stocks__bandar"
;;
idx__help__stocks,broker)
cmd="idx__help__stocks__broker"
;;
idx__help__stocks,compare) idx__help__stocks,compare)
cmd="idx__help__stocks__compare" cmd="idx__help__stocks__compare"
;; ;;
@ -183,6 +189,9 @@ _idx() {
idx__help__stocks,financials) idx__help__stocks,financials)
cmd="idx__help__stocks__financials" cmd="idx__help__stocks__financials"
;; ;;
idx__help__stocks,flow)
cmd="idx__help__stocks__flow"
;;
idx__help__stocks,fundamental) idx__help__stocks,fundamental)
cmd="idx__help__stocks__fundamental" cmd="idx__help__stocks__fundamental"
;; ;;
@ -336,6 +345,12 @@ _idx() {
idx__ownership__resolve__help,unresolved) idx__ownership__resolve__help,unresolved)
cmd="idx__ownership__resolve__help__unresolved" cmd="idx__ownership__resolve__help__unresolved"
;; ;;
idx__stocks,bandar)
cmd="idx__stocks__bandar"
;;
idx__stocks,broker)
cmd="idx__stocks__broker"
;;
idx__stocks,compare) idx__stocks,compare)
cmd="idx__stocks__compare" cmd="idx__stocks__compare"
;; ;;
@ -345,6 +360,9 @@ _idx() {
idx__stocks,financials) idx__stocks,financials)
cmd="idx__stocks__financials" cmd="idx__stocks__financials"
;; ;;
idx__stocks,flow)
cmd="idx__stocks__flow"
;;
idx__stocks,fundamental) idx__stocks,fundamental)
cmd="idx__stocks__fundamental" cmd="idx__stocks__fundamental"
;; ;;
@ -384,6 +402,12 @@ _idx() {
idx__stocks,valuation) idx__stocks,valuation)
cmd="idx__stocks__valuation" cmd="idx__stocks__valuation"
;; ;;
idx__stocks__help,bandar)
cmd="idx__stocks__help__bandar"
;;
idx__stocks__help,broker)
cmd="idx__stocks__help__broker"
;;
idx__stocks__help,compare) idx__stocks__help,compare)
cmd="idx__stocks__help__compare" cmd="idx__stocks__help__compare"
;; ;;
@ -393,6 +417,9 @@ _idx() {
idx__stocks__help,financials) idx__stocks__help,financials)
cmd="idx__stocks__help__financials" cmd="idx__stocks__help__financials"
;; ;;
idx__stocks__help,flow)
cmd="idx__stocks__help__flow"
;;
idx__stocks__help,fundamental) idx__stocks__help,fundamental)
cmd="idx__stocks__help__fundamental" cmd="idx__stocks__help__fundamental"
;; ;;
@ -1191,7 +1218,7 @@ _idx() {
return 0 return 0
;; ;;
idx__help__stocks) idx__help__stocks)
opts="quote history technical growth valuation risk fundamental profile financials earnings sentiment insights news screen compare" opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1204,6 +1231,34 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__help__stocks__bandar)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__broker)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__compare) idx__help__stocks__compare)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -1246,6 +1301,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__help__stocks__flow)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__fundamental) idx__help__stocks__fundamental)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -2193,7 +2262,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks) idx__stocks)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help quote history technical growth valuation risk fundamental profile financials earnings sentiment insights news screen compare help" opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2214,6 +2283,94 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__bandar)
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--date)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--to)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--investor)
COMPREPLY=($(compgen -W "all foreign domestic" -- "${cur}"))
return 0
;;
--board)
COMPREPLY=($(compgen -W "all regular cash negotiated" -- "${cur}"))
return 0
;;
--top)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__broker)
opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--date)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--to)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--investor)
COMPREPLY=($(compgen -W "all foreign domestic" -- "${cur}"))
return 0
;;
--board)
COMPREPLY=($(compgen -W "all regular cash negotiated" -- "${cur}"))
return 0
;;
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__compare) idx__stocks__compare)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@ -2284,6 +2441,44 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__flow)
opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--days)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--from)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--to)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--source)
COMPREPLY=($(compgen -W "ipot idx" -- "${cur}"))
return 0
;;
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__fundamental) idx__stocks__fundamental)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
@ -2329,7 +2524,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__help) idx__stocks__help)
opts="quote history technical growth valuation risk fundamental profile financials earnings sentiment insights news screen compare help" opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2342,6 +2537,34 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__help__bandar)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__broker)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__compare) idx__stocks__help__compare)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -2384,6 +2607,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__help__flow)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__fundamental) idx__stocks__help__fundamental)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then

View file

@ -0,0 +1,4 @@
exit: 3
--- stdout
--- stderr
{"code":"SYMBOLNOTFOUND","error":true,"message":"symbol not found: ZZZZ"}

View file

@ -0,0 +1,4 @@
exit: 3
--- stdout
--- stderr
Error: symbol not found: ZZZZ

View file

@ -12,6 +12,9 @@ Commands:
valuation Run valuation analysis on a stock valuation Run valuation analysis on a stock
risk Run risk analysis on a stock risk Run risk analysis on a stock
fundamental Run full fundamental analysis on a stock fundamental Run full fundamental analysis on a stock
broker Which brokers bought and sold a stock
bandar Bandarmology: is buying more concentrated than selling
flow Foreign buying and selling of a stock, session by session
profile Get company profile profile Get company profile
financials Get financial statements financials Get financial statements
earnings Get earnings report earnings Get earnings report

View file

@ -0,0 +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,"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

@ -0,0 +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,"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

@ -0,0 +1,13 @@
exit: 0
--- stdout
┌─────────┬────────────┬──────────────────┬─────────────┬───────────┬────────────┬───────┬─────────────┐
│ SYMBOL ┆ DATES ┆ SIGNAL ┆ TOP 3 NET % ┆ BUY SHARE ┆ SELL SHARE ┆ VALUE ┆ FOREIGN NET │
╞═════════╪════════════╪══════════════════╪═════════════╪═══════════╪════════════╪═══════╪═════════════╡
│ BBRI.JK ┆ 2026-10-16 ┆ big distribution ┆ -17.5% ┆ 38.3% ┆ 55.7% ┆ 3.3T ┆ -1.1T │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ TLKM.JK ┆ 2026-10-16 ┆ big distribution ┆ -19.9% ┆ 43.1% ┆ 63.1% ┆ 2.2T ┆ -90.2B │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 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' 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

@ -0,0 +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,"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

@ -0,0 +1,20 @@
exit: 0
--- stdout
Bandar reading for BBCA.JK, 2026-10-16
Signal (top 5): big distribution, -24.2% of value
┌─────┬────────┬──────┬───────┬────────┬─────────┬──────────┬──────────────────┐
│ TOP ┆ BUY ┆ SELL ┆ NET ┆ NET % ┆ BUY AVG ┆ SELL AVG ┆ SIGNAL │
╞═════╪════════╪══════╪═══════╪════════╪═════════╪══════════╪══════════════════╡
│ 1 ┆ 841.2B ┆ 2.2T ┆ -1.4T ┆ -20.6% ┆ 5,191 ┆ 5,211 ┆ big distribution │
├╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 3 ┆ 2.0T ┆ 3.9T ┆ -1.9T ┆ -29.1% ┆ 5,212 ┆ 5,233 ┆ big distribution │
├╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 5 ┆ 2.9T ┆ 4.5T ┆ -1.6T ┆ -24.2% ┆ 5,211 ┆ 5,229 ┆ big distribution │
├╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 10 ┆ 4.5T ┆ 5.6T ┆ -1.1T ┆ -16.3% ┆ 5,213 ┆ 5,228 ┆ big distribution │
└─────┴────────┴──────┴───────┴────────┴─────────┴──────────┴──────────────────┘
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' 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

@ -0,0 +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,"buy_value":0,"sell_value":0,"foreign_net_value":0,"rounded":false}
--- stderr

View file

@ -0,0 +1,5 @@
exit: 0
--- stdout
Broker summary for BBCA.JK, 2026-10-17
No trades.
--- stderr

View file

@ -0,0 +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,"buy_value":9900000000000,"sell_value":9900000000000,"foreign_net_value":-4100000000000,"rounded":true}
--- stderr

View file

@ -0,0 +1,32 @@
exit: 0
--- stdout
Broker summary for BBCA.JK, 2026-10-12 to 2026-10-16
┌────┬───────┬─────────┬────────┬───────┬────────┬─────────┬────────┬───────┐
│ # ┆ BUYER ┆ LOTS ┆ VALUE ┆ AVG ┆ SELLER ┆ LOTS ┆ VALUE ┆ AVG │
╞════╪═══════╪═════════╪════════╪═══════╪════════╪═════════╪════════╪═══════╡
│ 1 ┆ KZ ┆ 1.7M ┆ 963.0B ┆ 5,573 ┆ CC ┆ 5.1M ┆ 2.7T ┆ 5,336 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 2 ┆ MG ┆ 1.6M ┆ 842.0B ┆ 5,192 ┆ RX ┆ 1.8M ┆ 928.5B ┆ 5,295 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 3 ┆ ZP ┆ 1.6M ┆ 900.9B ┆ 5,780 ┆ ZP ┆ 1.6M ┆ 942.1B ┆ 5,756 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 4 ┆ YU ┆ 1.4M ┆ 781.7B ┆ 5,626 ┆ MG ┆ 1.5M ┆ 800.5B ┆ 5,239 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 5 ┆ CC ┆ 1.4M ┆ 737.9B ┆ 5,408 ┆ AK ┆ 1.4M ┆ 786.5B ┆ 5,785 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 6 ┆ XL ┆ 1.3M ┆ 687.2B ┆ 5,313 ┆ BB ┆ 852,704 ┆ 457.7B ┆ 5,368 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 7 ┆ AK ┆ 1.1M ┆ 643.9B ┆ 5,771 ┆ YU ┆ 830,940 ┆ 466.7B ┆ 5,617 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 8 ┆ SQ ┆ 973,921 ┆ 519.4B ┆ 5,333 ┆ BK ┆ 709,658 ┆ 393.3B ┆ 5,542 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 9 ┆ YP ┆ 919,655 ┆ 487.1B ┆ 5,296 ┆ KZ ┆ 621,425 ┆ 363.3B ┆ 5,847 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 10 ┆ PD ┆ 646,162 ┆ 341.2B ┆ 5,280 ┆ XL ┆ 441,903 ┆ 242.6B ┆ 5,490 │
└────┴───────┴─────────┴────────┴───────┴────────┴─────────┴────────┴───────┘
Total 9.9T IDR, 18.1M lots, average 5,471; foreign net -4.1T.
Foreign brokers: AK, BK, KZ, RX, YP, YU, ZP.
State-owned brokers: CC.
Buyers and sellers are each ranked by lots; a broker can be on both sides.
Note: the source rounds figures of a million and more to one decimal.
--- stderr

View file

@ -0,0 +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,"buy_value":6600000000000,"sell_value":6600000000000,"foreign_net_value":-2300000000000,"rounded":true}
--- stderr

View file

@ -0,0 +1,32 @@
exit: 0
--- stdout
Broker summary for BBCA.JK, 2026-10-16
┌────┬───────┬─────────┬────────┬───────┬────────┬─────────┬────────┬───────┐
│ # ┆ BUYER ┆ LOTS ┆ VALUE ┆ AVG ┆ SELLER ┆ LOTS ┆ VALUE ┆ AVG │
╞════╪═══════╪═════════╪════════╪═══════╪════════╪═════════╪════════╪═══════╡
│ 1 ┆ MG ┆ 1.6M ┆ 841.2B ┆ 5,191 ┆ CC ┆ 4.3M ┆ 2.2T ┆ 5,211 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 2 ┆ XL ┆ 1.1M ┆ 600.0B ┆ 5,229 ┆ RX ┆ 1.7M ┆ 908.2B ┆ 5,282 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 3 ┆ CC ┆ 1.0M ┆ 543.5B ┆ 5,227 ┆ MG ┆ 1.5M ┆ 799.4B ┆ 5,238 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 4 ┆ KZ ┆ 1.0M ┆ 522.4B ┆ 5,198 ┆ BB ┆ 687,067 ┆ 357.4B ┆ 5,202 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 5 ┆ SQ ┆ 828,163 ┆ 432.4B ┆ 5,221 ┆ ZP ┆ 529,223 ┆ 275.4B ┆ 5,203 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 6 ┆ YP ┆ 827,340 ┆ 431.7B ┆ 5,218 ┆ AK ┆ 444,696 ┆ 233.5B ┆ 5,251 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 7 ┆ YU ┆ 687,603 ┆ 356.6B ┆ 5,186 ┆ BK ┆ 419,266 ┆ 218.8B ┆ 5,219 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 8 ┆ PD ┆ 592,667 ┆ 309.2B ┆ 5,218 ┆ YU ┆ 418,986 ┆ 218.0B ┆ 5,202 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 9 ┆ ZP ┆ 441,000 ┆ 230.2B ┆ 5,220 ┆ NI ┆ 357,177 ┆ 186.1B ┆ 5,212 │
├╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 10 ┆ AK ┆ 398,474 ┆ 209.9B ┆ 5,268 ┆ XL ┆ 296,269 ┆ 155.0B ┆ 5,232 │
└────┴───────┴─────────┴────────┴───────┴────────┴─────────┴────────┴───────┘
Total 6.6T IDR, 12.6M lots, average 5,228; foreign net -2.3T.
Foreign brokers: AK, BK, KZ, RX, YP, YU, ZP.
State-owned brokers: CC, NI.
Buyers and sellers are each ranked by lots; a broker can be on both sides.
Note: the source rounds figures of a million and more to one decimal.
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"symbol":"TLKM.JK","source":"idx","from":"2026-10-14","to":"2026-10-16","days":[{"date":"2026-10-14","close":2250,"change_bps":45,"volume":87159900,"value":197725738000,"foreign_buy":63030200,"foreign_sell":60811200,"foreign_net":2219000,"foreign_buy_value":142986543253,"foreign_sell_value":137952652523,"foreign_net_value":5033890729,"foreign_share_bps":7104,"non_regular_bps":979},{"date":"2026-10-15","close":2230,"change_bps":-89,"volume":150485300,"value":333287991000,"foreign_buy":97385100,"foreign_sell":105521800,"foreign_net":-8136700,"foreign_buy_value":215684085637,"foreign_sell_value":233704878341,"foreign_net_value":-18020792704,"foreign_share_bps":6742,"non_regular_bps":78},{"date":"2026-10-16","close":1900,"change_bps":-1480,"volume":963614200,"value":1855341292500,"foreign_buy":679004400,"foreign_sell":727289800,"foreign_net":-48285400,"foreign_buy_value":1307354023123,"foreign_sell_value":1400322657713,"foreign_net_value":-92968634589,"foreign_share_bps":7297,"non_regular_bps":1485}],"foreign_net":-54203100,"foreign_buy_value":1666024652013,"foreign_sell_value":1771980188577,"foreign_net_value":-105955536564,"foreign_share_bps":7213,"rounded":false}
--- stderr

View file

@ -0,0 +1,15 @@
exit: 0
--- stdout
Foreign flow for TLKM.JK, 2026-10-14 to 2026-10-16 (3 sessions, IDX)
┌────────────┬───────┬────────┬────────┬─────────────┬──────────────┬─────────────┬───────────┬─────────┐
│ DATE ┆ CLOSE ┆ CHG% ┆ VALUE ┆ FOREIGN BUY ┆ FOREIGN SELL ┆ FOREIGN NET ┆ NET VALUE ┆ NON-REG │
╞════════════╪═══════╪════════╪════════╪═════════════╪══════════════╪═════════════╪═══════════╪═════════╡
│ 2026-10-14 ┆ 2,250 ┆ +0.5% ┆ 197.7B ┆ 63.0M ┆ 60.8M ┆ +2.2M ┆ +5.0B ┆ 9.8% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-15 ┆ 2,230 ┆ -0.9% ┆ 333.3B ┆ 97.4M ┆ 105.5M ┆ -8.1M ┆ -18.0B ┆ 0.8% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-16 ┆ 1,900 ┆ -14.8% ┆ 1.9T ┆ 679.0M ┆ 727.3M ┆ -48.3M ┆ -93.0B ┆ 14.8% │
└────────────┴───────┴────────┴────────┴─────────────┴──────────────┴─────────────┴───────────┴─────────┘
Net foreign: -54.2M shares, about -106.0B IDR; foreign investors made up 72.1% of trading.
Volume, value and foreign figures are the regular board, volumes in shares. Net value is estimated at each session's average price; NON-REG is the share of all value traded outside the regular board (negotiated and cash).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"symbol":"BBCA.JK","source":"idx","from":"2026-10-12","to":"2026-10-16","days":[{"date":"2026-10-12","close":5950,"change_bps":-165,"volume":97965100,"value":586491892500,"foreign_buy":51499200,"foreign_sell":60883100,"foreign_net":-9383900,"foreign_buy_value":308312483427,"foreign_sell_value":364491482581,"foreign_net_value":-56178999154,"foreign_share_bps":5736,"non_regular_bps":14},{"date":"2026-10-13","close":5950,"change_bps":0,"volume":122716900,"value":726575115000,"foreign_buy":88772100,"foreign_sell":83738900,"foreign_net":5033200,"foreign_buy_value":525596708899,"foreign_sell_value":495796429810,"foreign_net_value":29800279088,"foreign_share_bps":7029,"non_regular_bps":159},{"date":"2026-10-14","close":6125,"change_bps":294,"volume":136303100,"value":828862772500,"foreign_buy":119876900,"foreign_sell":84298000,"foreign_net":35578900,"foreign_buy_value":728974613876,"foreign_sell_value":512618377690,"foreign_net_value":216356236186,"foreign_share_bps":7490,"non_regular_bps":2412},{"date":"2026-10-15","close":6075,"change_bps":-82,"volume":134763200,"value":816554920000,"foreign_buy":120850000,"foreign_sell":73638900,"foreign_net":47211100,"foreign_buy_value":732252292035,"foreign_sell_value":446191587157,"foreign_net_value":286060704877,"foreign_share_bps":7216,"non_regular_bps":1046},{"date":"2026-10-16","close":5175,"change_bps":-1481,"volume":1241098200,"value":6474934315000,"foreign_buy":309392800,"foreign_sell":757227900,"foreign_net":-447835100,"foreign_buy_value":1614133400188,"foreign_sell_value":3950534223629,"foreign_net_value":-2336400823441,"foreign_share_bps":4297,"non_regular_bps":152}],"foreign_net":-369395800,"foreign_buy_value":3909269498425,"foreign_sell_value":5769632100867,"foreign_net_value":-1860362602444,"foreign_share_bps":5050,"rounded":false}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"symbol":"BBCA.JK","source":"idx","from":"2026-10-12","to":"2026-10-16","days":[{"date":"2026-10-12","close":5950,"change_bps":-165,"volume":97965100,"value":586491892500,"foreign_buy":51499200,"foreign_sell":60883100,"foreign_net":-9383900,"foreign_buy_value":308312483427,"foreign_sell_value":364491482581,"foreign_net_value":-56178999154,"foreign_share_bps":5736,"non_regular_bps":14},{"date":"2026-10-13","close":5950,"change_bps":0,"volume":122716900,"value":726575115000,"foreign_buy":88772100,"foreign_sell":83738900,"foreign_net":5033200,"foreign_buy_value":525596708899,"foreign_sell_value":495796429810,"foreign_net_value":29800279088,"foreign_share_bps":7029,"non_regular_bps":159},{"date":"2026-10-14","close":6125,"change_bps":294,"volume":136303100,"value":828862772500,"foreign_buy":119876900,"foreign_sell":84298000,"foreign_net":35578900,"foreign_buy_value":728974613876,"foreign_sell_value":512618377690,"foreign_net_value":216356236186,"foreign_share_bps":7490,"non_regular_bps":2412},{"date":"2026-10-15","close":6075,"change_bps":-82,"volume":134763200,"value":816554920000,"foreign_buy":120850000,"foreign_sell":73638900,"foreign_net":47211100,"foreign_buy_value":732252292035,"foreign_sell_value":446191587157,"foreign_net_value":286060704877,"foreign_share_bps":7216,"non_regular_bps":1046},{"date":"2026-10-16","close":5175,"change_bps":-1481,"volume":1241098200,"value":6474934315000,"foreign_buy":309392800,"foreign_sell":757227900,"foreign_net":-447835100,"foreign_buy_value":1614133400188,"foreign_sell_value":3950534223629,"foreign_net_value":-2336400823441,"foreign_share_bps":4297,"non_regular_bps":152}],"foreign_net":-369395800,"foreign_buy_value":3909269498425,"foreign_sell_value":5769632100867,"foreign_net_value":-1860362602444,"foreign_share_bps":5050,"rounded":false}
--- stderr

View file

@ -0,0 +1,19 @@
exit: 0
--- stdout
Foreign flow for BBCA.JK, 2026-10-12 to 2026-10-16 (5 sessions, IDX)
┌────────────┬───────┬────────┬────────┬─────────────┬──────────────┬─────────────┬───────────┬─────────┐
│ DATE ┆ CLOSE ┆ CHG% ┆ VALUE ┆ FOREIGN BUY ┆ FOREIGN SELL ┆ FOREIGN NET ┆ NET VALUE ┆ NON-REG │
╞════════════╪═══════╪════════╪════════╪═════════════╪══════════════╪═════════════╪═══════════╪═════════╡
│ 2026-10-12 ┆ 5,950 ┆ -1.6% ┆ 586.5B ┆ 51.5M ┆ 60.9M ┆ -9.4M ┆ -56.2B ┆ 0.1% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-13 ┆ 5,950 ┆ +0.0% ┆ 726.6B ┆ 88.8M ┆ 83.7M ┆ +5.0M ┆ +29.8B ┆ 1.6% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-14 ┆ 6,125 ┆ +2.9% ┆ 828.9B ┆ 119.9M ┆ 84.3M ┆ +35.6M ┆ +216.4B ┆ 24.1% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-15 ┆ 6,075 ┆ -0.8% ┆ 816.6B ┆ 120.8M ┆ 73.6M ┆ +47.2M ┆ +286.1B ┆ 10.5% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2026-10-16 ┆ 5,175 ┆ -14.8% ┆ 6.5T ┆ 309.4M ┆ 757.2M ┆ -447.8M ┆ -2.3T ┆ 1.5% │
└────────────┴───────┴────────┴────────┴─────────────┴──────────────┴─────────────┴───────────┴─────────┘
Net foreign: -369.4M shares, about -1.9T IDR; foreign investors made up 50.5% of trading.
Volume, value and foreign figures are the regular board, volumes in shares. Net value is estimated at each session's average price; NON-REG is the share of all value traded outside the regular board (negotiated and cash).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"symbol":"BBCA.JK","source":"ipot","from":"2026-10-12","to":"2026-10-16","days":[{"date":"2026-10-12","close":null,"change_bps":null,"volume":98041500,"value":587100000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":308700000000,"foreign_sell_value":364600000000,"foreign_net_value":-55900000000,"foreign_share_bps":5734,"non_regular_bps":null},{"date":"2026-10-13","close":null,"change_bps":null,"volume":120000000,"value":738200000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":537300000000,"foreign_sell_value":507200000000,"foreign_net_value":30100000000,"foreign_share_bps":7075,"non_regular_bps":null},{"date":"2026-10-14","close":null,"change_bps":null,"volume":180000000,"value":1100000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":991800000000,"foreign_sell_value":775200000000,"foreign_net_value":216600000000,"foreign_share_bps":8032,"non_regular_bps":null},{"date":"2026-10-15","close":null,"change_bps":null,"volume":150000000,"value":912000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":826900000000,"foreign_sell_value":540800000000,"foreign_net_value":286100000000,"foreign_share_bps":7498,"non_regular_bps":null},{"date":"2026-10-16","close":null,"change_bps":null,"volume":1260000000,"value":6600000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":1700000000000,"foreign_sell_value":4000000000000,"foreign_net_value":-2300000000000,"foreign_share_bps":4318,"non_regular_bps":null}],"foreign_net":null,"foreign_buy_value":4364700000000,"foreign_sell_value":6187800000000,"foreign_net_value":-1823100000000,"foreign_share_bps":5310,"rounded":true}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"symbol":"BBCA.JK","source":"ipot","from":"2026-10-12","to":"2026-10-16","days":[{"date":"2026-10-12","close":null,"change_bps":null,"volume":98041500,"value":587100000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":308700000000,"foreign_sell_value":364600000000,"foreign_net_value":-55900000000,"foreign_share_bps":5734,"non_regular_bps":null},{"date":"2026-10-13","close":null,"change_bps":null,"volume":120000000,"value":738200000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":537300000000,"foreign_sell_value":507200000000,"foreign_net_value":30100000000,"foreign_share_bps":7075,"non_regular_bps":null},{"date":"2026-10-14","close":null,"change_bps":null,"volume":180000000,"value":1100000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":991800000000,"foreign_sell_value":775200000000,"foreign_net_value":216600000000,"foreign_share_bps":8032,"non_regular_bps":null},{"date":"2026-10-15","close":null,"change_bps":null,"volume":150000000,"value":912000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":826900000000,"foreign_sell_value":540800000000,"foreign_net_value":286100000000,"foreign_share_bps":7498,"non_regular_bps":null},{"date":"2026-10-16","close":null,"change_bps":null,"volume":1260000000,"value":6600000000000,"foreign_buy":null,"foreign_sell":null,"foreign_net":null,"foreign_buy_value":1700000000000,"foreign_sell_value":4000000000000,"foreign_net_value":-2300000000000,"foreign_share_bps":4318,"non_regular_bps":null}],"foreign_net":null,"foreign_buy_value":4364700000000,"foreign_sell_value":6187800000000,"foreign_net_value":-1823100000000,"foreign_share_bps":5310,"rounded":true}
--- stderr

View file

@ -0,0 +1,20 @@
exit: 0
--- stdout
Foreign flow for BBCA.JK, 2026-10-12 to 2026-10-16 (5 sessions, IPOT)
┌────────────┬────────┬─────────────┬──────────────┬─────────────┬───────────┐
│ DATE ┆ VALUE ┆ FOREIGN BUY ┆ FOREIGN SELL ┆ FOREIGN NET ┆ FOREIGN % │
╞════════════╪════════╪═════════════╪══════════════╪═════════════╪═══════════╡
│ 2026-10-12 ┆ 587.1B ┆ 308.7B ┆ 364.6B ┆ -55.9B ┆ 57.3% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-10-13 ┆ 738.2B ┆ 537.3B ┆ 507.2B ┆ +30.1B ┆ 70.8% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-10-14 ┆ 1.1T ┆ 991.8B ┆ 775.2B ┆ +216.6B ┆ 80.3% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-10-15 ┆ 912.0B ┆ 826.9B ┆ 540.8B ┆ +286.1B ┆ 75.0% │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-10-16 ┆ 6.6T ┆ 1.7T ┆ 4.0T ┆ -2.3T ┆ 43.2% │
└────────────┴────────┴─────────────┴──────────────┴─────────────┴───────────┘
Foreign investors bought 4.4T IDR and sold 6.2T IDR: net -1.8T; they made up 53.1% of trading.
Values in IDR across all boards. For exact share counts, the close and the non-regular share, use --source idx (needs curl-impersonate).
Note: the source rounds figures of a million and more to one decimal.
--- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,5 +1,5 @@
exit: 0 exit: 0
--- stdout --- stdout
{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null} {"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}
{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null} {"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false},{"symbol":"BBRI.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null},{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null,"behind_session":false},{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null},{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false},{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null},{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false},{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null},{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBRI.JK","price":5100,"change":210,"change_pct":4.3,"volume":22200000,"market_cap":780000000000000,"week52_high":5400,"week52_low":3900,"week52_position":0.8,"range_signal":"upper","prev_close":4890,"avg_volume":18000000,"as_of":null,"behind_session":false},{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.2,"volume":12300000,"market_cap":1215200000000000,"week52_high":10500,"week52_low":8000,"week52_position":0.75,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -1,4 +1,4 @@
exit: 0 exit: 0
--- stdout --- stdout
[{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.1990161918425908,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null}] [{"symbol":"BBCA.JK","price":9875,"change":117,"change_pct":1.1990161918425908,"volume":12300000,"market_cap":1215200000000000,"week52_high":10250,"week52_low":7800,"week52_position":0.8469387755102041,"range_signal":"upper","prev_close":9758,"avg_volume":10000000,"as_of":null,"behind_session":false}]
--- stderr --- stderr

View file

@ -34,3 +34,4 @@ rusqlite = { workspace = true, optional = true }
quick-xml = { version = "0.37", optional = true } quick-xml = { version = "0.37", optional = true }
sha2 = { workspace = true, optional = true } sha2 = { workspace = true, optional = true }
zip = { workspace = true, optional = true } zip = { workspace = true, optional = true }
scraper = { version = "0.27", default-features = false }

View file

@ -0,0 +1,466 @@
//! Bandarmology over a broker summary: is buying more concentrated in a few
//! 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' 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 share - sell share, % | signal |
//! |------------------------------------|--------------------|
//! | >= +15 | `big_accumulation` |
//! | +5 to +15 | `accumulation` |
//! | within +-5 | `neutral` |
//! | -15 to -5 | `distribution` |
//! | <= -15 | `big_distribution` |
//!
//! The thresholds follow fzn0x/idx (MIT), which set them against the 40 most
//! traded IDX stocks in October 2026. They are heuristics: a broker code is
//! not an investor, and gross figures overstate a broker that both bought
//! and sold (listed in `both_sides`).
use serde::{Deserialize, Serialize};
use crate::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerSummary};
use crate::error::IdxError;
use chrono::NaiveDate;
/// Names the rules below; bumped when they change.
pub const METHOD: &str = "top_concentration_v1";
/// Tiers every report carries.
pub const TIERS: [u8; 4] = [1, 3, 5, 10];
/// Tier behind the headline signal unless asked otherwise.
pub const DEFAULT_TOP: u8 = 5;
/// Imbalance, in basis points of value traded, from which a tier reads
/// accumulation (or, negative, distribution).
pub const ACCUMULATION_BPS: i64 = 500;
/// Imbalance from which the reading is big.
pub const BIG_BPS: i64 = 1500;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BandarSignal {
BigAccumulation,
Accumulation,
Neutral,
Distribution,
BigDistribution,
}
impl BandarSignal {
pub fn as_str(self) -> &'static str {
match self {
Self::BigAccumulation => "big_accumulation",
Self::Accumulation => "accumulation",
Self::Neutral => "neutral",
Self::Distribution => "distribution",
Self::BigDistribution => "big_distribution",
}
}
pub fn from_imbalance_bps(bps: i64) -> Self {
match bps {
b if b >= BIG_BPS => Self::BigAccumulation,
b if b >= ACCUMULATION_BPS => Self::Accumulation,
b if b <= -BIG_BPS => Self::BigDistribution,
b if b <= -ACCUMULATION_BPS => Self::Distribution,
_ => Self::Neutral,
}
}
}
/// The top N buyers against the top N sellers. Every figure is `None` when
/// a broker in the tier lacks it, or when nothing traded.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BandarTier {
pub top: u8,
/// Value the top N buyers bought, whole IDR.
pub buy_value: Option<i64>,
/// Value the top N sellers sold, whole IDR.
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 bought, basis points.
pub buy_share_bps: Option<i64>,
/// 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>,
/// Volume-weighted average price the top N sellers got, whole IDR.
pub sell_average: Option<i64>,
pub signal: Option<BandarSignal>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BandarReport {
pub symbol: String,
pub from: NaiveDate,
pub to: NaiveDate,
pub investor: BrokerInvestor,
pub board: BrokerBoard,
/// The rules applied, e.g. `top_concentration_v1`.
pub method: String,
/// Tier behind the headline `signal`.
pub top: u8,
/// The headline tier's signal; `None` when nothing traded or a figure
/// is missing.
pub signal: Option<BandarSignal>,
/// 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; `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.
pub foreign_net_value: Option<i64>,
/// `foreign_net_value` in basis points of the value traded.
pub foreign_net_bps: Option<i64>,
/// Brokers among the top 10 on both sides, whose gross figures overstate
/// which way they traded.
pub both_sides: Vec<String>,
/// The source rounded figures of a million and more to one decimal.
pub rounded: bool,
}
/// The headline tier must be one the report carries.
pub fn check_top(top: u8) -> Result<u8, IdxError> {
if TIERS.contains(&top) {
Ok(top)
} else {
Err(IdxError::InvalidInput(format!(
"top must be 1, 3, 5 or 10 (got {top})"
)))
}
}
/// `part` in basis points of `whole`, truncated toward zero so a negative
/// reading never reaches a threshold its magnitude has not; `None` without
/// a positive whole.
fn bps(part: i64, whole: Option<i64>) -> Option<i64> {
let whole = whole.filter(|whole| *whole > 0)?;
i64::try_from(i128::from(part) * 10_000 / i128::from(whole)).ok()
}
/// Sum of `field` over `flows`; `None` when any flow lacks it.
fn sum(flows: &[BrokerFlow], field: fn(&BrokerFlow) -> Option<i64>) -> Option<i64> {
flows
.iter()
.try_fold(0_i64, |total, flow| total.checked_add(field(flow)?))
}
/// Value over shares, the shares taken from each broker's value and its
/// exact published average (lots of a million and more are rounded,
/// averages never are).
fn vwap(flows: &[BrokerFlow]) -> Option<i64> {
let (mut value, mut shares) = (0.0_f64, 0.0_f64);
for flow in flows {
let (flow_value, average) = (flow.value?, flow.average_price?);
if average <= 0 {
return None;
}
value += flow_value as f64;
shares += flow_value as f64 / average as f64;
}
(shares > 0.0).then(|| (value / shares).round() as i64)
}
pub fn bandar_tier(summary: &BrokerSummary, top: u8) -> BandarTier {
let n = usize::from(top);
let buyers = &summary.buyers[..n.min(summary.buyers.len())];
let sellers = &summary.sellers[..n.min(summary.sellers.len())];
let buy_value = sum(buyers, |flow| flow.value);
let sell_value = sum(sellers, |flow| flow.value);
let imbalance_value = buy_value
.zip(sell_value)
.and_then(|(buy, sell)| buy.checked_sub(sell));
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),
);
// Checked: past i128 there is no reading, never a wrapped one.
buy.checked_mul(sold)
.zip(sell.checked_mul(bought))
.and_then(|(buys, sells)| buys.checked_sub(sells))
.and_then(|diff| diff.checked_mul(10_000))
.zip(bought.checked_mul(sold))
.and_then(|(diff, denominator)| i64::try_from(diff / denominator).ok())
}
_ => None,
};
BandarTier {
top,
buy_value,
sell_value,
imbalance_value,
imbalance_bps,
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),
}
}
/// The full reading of one summary, headlined by the top `top` tier (one of
/// [`TIERS`]; see [`check_top`]).
pub fn analyze_bandar(summary: &BrokerSummary, top: u8) -> BandarReport {
let tiers: Vec<BandarTier> = TIERS.iter().map(|n| bandar_tier(summary, *n)).collect();
let headline = tiers
.iter()
.find(|tier| tier.top == top)
.cloned()
.unwrap_or_else(|| bandar_tier(summary, top));
let mut both_sides: Vec<String> = summary
.buyers
.iter()
.filter(|buyer| {
summary
.sellers
.iter()
.any(|seller| seller.code == buyer.code)
})
.map(|buyer| buyer.code.clone())
.collect();
both_sides.sort_unstable();
BandarReport {
symbol: summary.symbol.clone(),
from: summary.from,
to: summary.to,
investor: summary.investor,
board: summary.board,
method: METHOD.to_string(),
top,
signal: headline.signal,
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
.foreign_net_value
.and_then(|value| bps(value, summary.total_value)),
both_sides,
rounded: summary.rounded,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn flow(code: &str, value: Option<i64>, average: i64) -> BrokerFlow {
BrokerFlow {
rank: 0,
code: code.to_string(),
name: None,
kind: None,
lots: value.map(|value| value / average / 100),
value,
average_price: Some(average),
}
}
fn summary(buyers: Vec<BrokerFlow>, sellers: Vec<BrokerFlow>, total: i64) -> BrokerSummary {
let day = NaiveDate::from_ymd_opt(2026, 10, 16).unwrap();
BrokerSummary {
symbol: "TEST.JK".to_string(),
from: day,
to: day,
investor: BrokerInvestor::All,
board: BrokerBoard::All,
buyers,
sellers,
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,
}
}
#[test]
fn test_bandar_signal_thresholds_are_inclusive() {
use BandarSignal::*;
for (bps, signal) in [
(1500, BigAccumulation),
(1499, Accumulation),
(500, Accumulation),
(499, Neutral),
(-499, Neutral),
(-500, Distribution),
(-1499, Distribution),
(-1500, BigDistribution),
] {
assert_eq!(BandarSignal::from_imbalance_bps(bps), signal, "{bps}");
}
}
#[test]
fn test_analyze_bandar_weighs_top_buyers_against_top_sellers() {
let s = summary(
vec![
flow("AA", Some(400), 100),
flow("BB", Some(200), 100),
flow("CC", Some(100), 50),
],
vec![
flow("DD", Some(300), 110),
flow("AA", Some(100), 110),
flow("EE", Some(50), 110),
],
1000,
);
let report = analyze_bandar(&s, 3);
let top1 = &report.tiers[0];
assert_eq!((top1.top, top1.imbalance_value), (1, Some(100)));
assert_eq!(top1.imbalance_bps, Some(1000));
assert_eq!(top1.signal, Some(BandarSignal::Accumulation));
let top3 = &report.tiers[1];
assert_eq!((top3.buy_value, top3.sell_value), (Some(700), Some(450)));
assert_eq!(
(top3.buy_share_bps, top3.sell_share_bps),
(Some(7000), Some(4500))
);
assert_eq!(top3.imbalance_bps, Some(2500));
// 700 IDR over 4 + 2 + 2 shares.
assert_eq!(top3.buy_average, Some(88));
assert_eq!(top3.sell_average, Some(110));
// Short lists: every broker on a side is listed, so the sums hold.
assert_eq!(report.tiers[3].imbalance_value, Some(250));
assert_eq!(
(report.top, report.signal),
(3, Some(BandarSignal::BigAccumulation))
);
assert_eq!(report.imbalance_bps, Some(2500));
assert_eq!(report.foreign_net_bps, Some(-1000));
assert_eq!(report.both_sides, vec!["AA".to_string()]);
assert_eq!(report.method, METHOD);
}
#[test]
fn test_bandar_tier_thresholds_are_symmetric_for_sign() {
let reading = |buy: i64, sell: i64| {
let s = summary(
vec![flow("AA", Some(buy), 100)],
vec![flow("BB", Some(sell), 100)],
2_100_000_000,
);
let tier = bandar_tier(&s, 1);
(tier.imbalance_bps, tier.signal)
};
// -4.9952% and -14.9952% stay short of the thresholds, like their
// mirror images.
assert_eq!(
reading(504_300_000, 609_200_000),
(Some(-499), Some(BandarSignal::Neutral))
);
assert_eq!(
reading(609_200_000, 504_300_000),
(Some(499), Some(BandarSignal::Neutral))
);
assert_eq!(
reading(504_300_000, 819_200_000),
(Some(-1499), Some(BandarSignal::Distribution))
);
assert_eq!(
reading(819_200_000, 504_300_000),
(Some(1499), Some(BandarSignal::Accumulation))
);
}
#[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(
vec![flow("AA", Some(400), 100), flow("BB", None, 100)],
vec![flow("DD", Some(300), 100)],
1000,
);
let report = analyze_bandar(&s, 3);
assert_eq!(report.tiers[0].imbalance_bps, Some(1000));
let top3 = &report.tiers[1];
assert_eq!(
(top3.buy_value, top3.imbalance_bps, top3.signal),
(None, None, None)
);
assert_eq!(top3.buy_average, None);
assert_eq!(report.signal, None);
}
#[test]
fn test_analyze_bandar_without_trades_has_no_signal() {
let report = analyze_bandar(&summary(vec![], vec![], 0), DEFAULT_TOP);
assert!(report.tiers.iter().all(|tier| tier.signal.is_none()));
assert_eq!((report.signal, report.imbalance_bps), (None, None));
assert_eq!(report.tiers[2].buy_value, Some(0));
}
#[test]
fn test_bandar_tier_past_i128_has_no_reading_instead_of_a_wrapped_one() {
let reading = |total: i64| {
let buyers = vec![flow("AA", Some(total / 10 * 6), 1)];
let sellers = vec![flow("BB", Some(total / 10 * 4), 1)];
bandar_tier(&summary(buyers, sellers, total), 1).imbalance_bps
};
// A quadrillion rupiah, far past any session, is still exact.
assert_eq!(reading(1_000_000_000_000_000), Some(2000));
assert_eq!(reading(1_000_000_000_000_000_000), None);
}
#[test]
fn test_check_top_allows_only_report_tiers() {
assert_eq!(check_top(5).unwrap(), 5);
assert!(matches!(check_top(4), Err(IdxError::InvalidInput(_))));
}
}

View file

@ -1,3 +1,4 @@
pub mod bandar;
pub mod fundamental; pub mod fundamental;
pub mod report; pub mod report;
pub mod signals; pub mod signals;
@ -8,9 +9,11 @@ use serde::de::DeserializeOwned;
use crate::api::history_provider; use crate::api::history_provider;
use crate::api::types::{Fundamentals, Interval, Period}; use crate::api::types::{Fundamentals, Interval, Period};
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerRange};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch}; use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::error::IdxError; use crate::error::IdxError;
use crate::market::HistorySource; use crate::market::HistorySource;
use bandar::{BandarReport, analyze_bandar, check_top};
use fundamental::{ use fundamental::{
FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental, FundamentalReport, GrowthReport, RiskReport, ValuationReport, analyze_fundamental,
analyze_growth, analyze_risk, analyze_valuation, analyze_growth, analyze_risk, analyze_valuation,
@ -28,7 +31,85 @@ pub struct Comparison {
pub failures: Vec<(String, IdxError)>, pub failures: Vec<(String, IdxError)>,
} }
/// 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 for {} investors needs --board all: the broker summary gives one side's \
selling only across all boards",
investor.as_str()
)))
}
}
/// Bandar readings for several symbols, strongest accumulation first;
/// symbols that failed are listed with their error.
pub struct BandarScreen {
pub reports: Vec<Fetched<BandarReport>>,
pub failures: Vec<(String, IdxError)>,
}
/// The sessions and trades a bandar reading covers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BandarQuery {
pub range: BrokerRange,
pub investor: BrokerInvestor,
pub board: BrokerBoard,
/// Headline tier, one of `bandar::TIERS`.
pub top: u8,
}
impl Analysis<'_> { impl Analysis<'_> {
/// Bandarmology over the broker summary `query` selects.
pub fn bandar(
&self,
symbol: &str,
query: &BandarQuery,
policy: CachePolicy,
) -> Result<Fetched<BandarReport>, IdxError> {
let top = check_top(query.top)?;
check_scope(query.investor, query.board)?;
let summary =
self.idx
.broker()
.fetch(symbol, query.range, query.investor, query.board, policy)?;
Ok(summary.map(|summary| analyze_bandar(&summary, top)))
}
/// [`Self::bandar`] for each symbol, ranked by the headline imbalance.
/// Invalid input fails the whole screen; other errors fail one symbol.
pub fn bandar_screen<S: AsRef<str>>(
&self,
symbols: &[S],
query: &BandarQuery,
policy: CachePolicy,
) -> Result<BandarScreen, IdxError> {
check_top(query.top)?;
check_scope(query.investor, query.board)?;
let mut reports = Vec::new();
let mut failures = Vec::new();
for symbol in symbols {
let resolved = self.idx.resolve(symbol.as_ref())?;
match self.bandar(&resolved, query, policy) {
Ok(report) => reports.push(report),
Err(err @ IdxError::InvalidInput(_)) => return Err(err),
Err(err) => failures.push((resolved, err)),
}
}
if reports.is_empty() {
return Err(failures
.pop()
.map(|(_, err)| err)
.unwrap_or_else(|| IdxError::InvalidInput("no symbols to screen".to_string())));
}
// Strongest accumulation first; readings without an imbalance last.
reports.sort_by_key(|report| std::cmp::Reverse(report.data.imbalance_bps));
Ok(BandarScreen { reports, failures })
}
/// Technical analysis over one year of daily history from `source` /// Technical analysis over one year of daily history from `source`
/// (see `Market::history_source`). /// (see `Market::history_source`).
pub fn technical( pub fn technical(

View file

@ -0,0 +1,73 @@
//! IDX's own site: the daily trading summary of every listed stock.
//!
//! `GET https://www.idx.co.id/primary/TradingSummary/GetStockSummary`
//! `?length=9999&start=0&date=YYYYMMDD` answers JSON with one row per stock:
//! prices, volume and value, foreign buy and sell volume, and non-regular
//! (negotiated and cash) volume and value. A day without trading has no
//! rows. Cloudflare in front of the site rejects some TLS fingerprints, so
//! the request goes through curl-impersonate's profile fallback.
mod parse;
use chrono::NaiveDate;
pub use parse::parse_stock_summary;
use super::TradingSummaryProvider;
use crate::curl_impersonate;
use crate::error::IdxError;
use crate::flow::types::StockDay;
const STOCK_SUMMARY_URL: &str = "https://www.idx.co.id/primary/TradingSummary/GetStockSummary";
const USER_AGENT: &str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.4 Safari/605.1.15";
/// One session's summary is about 650 KB; it must not hold a command or an
/// MCP call for long.
const MAX_TIME_SECS: &str = "30";
pub fn stock_summary_url(date: NaiveDate) -> String {
format!(
"{STOCK_SUMMARY_URL}?length=9999&start=0&date={}",
date.format("%Y%m%d")
)
}
/// A Cloudflare challenge page is not the JSON asked for: try the next
/// profile.
fn accept_json(body: &[u8]) -> Result<(), IdxError> {
match body.iter().find(|byte| !byte.is_ascii_whitespace()) {
Some(b'{') => Ok(()),
_ => Err(IdxError::Http(
"IDX stock summary: answered a challenge page instead of JSON (HTTP 403)".to_string(),
)),
}
}
pub struct IdxSiteProvider;
impl TradingSummaryProvider for IdxSiteProvider {
fn stock_summary(&self, date: NaiveDate) -> Result<Vec<StockDay>, IdxError> {
let args: Vec<String> = [
"--silent",
"--show-error",
"--location",
"--fail",
"--compressed",
"--max-time",
MAX_TIME_SECS,
"--header",
&format!("User-Agent: {USER_AGENT}"),
"--header",
"Accept: application/json,text/plain,*/*",
"--header",
"Referer: https://www.idx.co.id/id/data-pasar/ringkasan-perdagangan/ringkasan-saham/",
&stock_summary_url(date),
]
.iter()
.map(|arg| arg.to_string())
.collect();
let output =
curl_impersonate::run_owned_with_fallback("IDX stock summary", &args, &accept_json)?;
let body = String::from_utf8(output.stdout)
.map_err(|e| IdxError::Http(format!("IDX stock summary: not UTF-8: {e}")))?;
parse_stock_summary(&body, date)
}
}

View file

@ -0,0 +1,121 @@
//! Pure parser for IDX's `GetStockSummary` JSON, tested against
//! `tests/fixtures/idx_stocksummary_*.json`.
use chrono::NaiveDate;
use serde::Deserialize;
use crate::error::IdxError;
use crate::flow::types::StockDay;
#[derive(Deserialize)]
struct StockSummaryResponse {
data: Vec<StockSummaryRawRow>,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct StockSummaryRawRow {
date: String,
stock_code: String,
previous: Option<f64>,
high: Option<f64>,
low: Option<f64>,
close: Option<f64>,
volume: Option<f64>,
value: Option<f64>,
frequency: Option<f64>,
foreign_buy: Option<f64>,
foreign_sell: Option<f64>,
non_regular_volume: Option<f64>,
non_regular_value: Option<f64>,
}
fn whole(value: Option<f64>) -> Option<i64> {
value
.filter(|value| value.is_finite())
.map(|value| value.round() as i64)
}
/// The rows of one session. A day without trading (weekend, holiday) is
/// empty; rows dated other than `date` are dropped, so another session's
/// figures never pass as this one's. Rows for other days only are an error,
/// not an empty session, so they are never cached as one.
pub fn parse_stock_summary(raw: &str, date: NaiveDate) -> Result<Vec<StockDay>, IdxError> {
let response: StockSummaryResponse = serde_json::from_str(raw)
.map_err(|e| IdxError::ParseError(format!("idx stock summary: {e}")))?;
let answered = response.data.len();
let rows: Vec<StockDay> = response
.data
.into_iter()
.filter(|row| {
row.date
.get(..10)
.and_then(|day| NaiveDate::parse_from_str(day, "%Y-%m-%d").ok())
== Some(date)
})
.map(|row| StockDay {
code: row.stock_code.trim().to_ascii_uppercase(),
date,
previous: whole(row.previous),
high: whole(row.high),
low: whole(row.low),
close: whole(row.close),
volume: whole(row.volume),
value: whole(row.value),
frequency: whole(row.frequency),
foreign_buy: whole(row.foreign_buy),
foreign_sell: whole(row.foreign_sell),
non_regular_volume: whole(row.non_regular_volume),
non_regular_value: whole(row.non_regular_value),
})
.collect();
if rows.is_empty() && answered > 0 {
return Err(IdxError::ParseError(format!(
"idx stock summary: asked for {date}, got {answered} rows for other days"
)));
}
Ok(rows)
}
#[cfg(test)]
mod tests {
use super::*;
const DAY: &str = include_str!("../../../tests/fixtures/idx_stocksummary_20261016.json");
const SATURDAY: &str = include_str!("../../../tests/fixtures/idx_stocksummary_20261017.json");
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).unwrap()
}
#[test]
fn test_parse_stock_summary_reads_foreign_and_non_regular_figures() {
let rows = parse_stock_summary(DAY, date(2026, 10, 16)).unwrap();
let bbca = rows.iter().find(|row| row.code == "BBCA").unwrap();
assert_eq!((bbca.previous, bbca.close), (Some(6075), Some(5175)));
assert_eq!(bbca.volume, Some(1_241_098_200));
assert_eq!(bbca.value, Some(6_474_934_315_000));
assert_eq!(
(bbca.foreign_buy, bbca.foreign_sell),
(Some(309_392_800), Some(757_227_900))
);
assert_eq!(bbca.non_regular_value, Some(100_183_151_475));
}
#[test]
fn test_parse_stock_summary_keeps_only_the_requested_session() {
assert!(
parse_stock_summary(SATURDAY, date(2026, 10, 17))
.unwrap()
.is_empty()
);
assert!(matches!(
parse_stock_summary(DAY, date(2026, 10, 15)),
Err(IdxError::ParseError(_))
));
assert!(matches!(
parse_stock_summary("<html>Just a moment...</html>", date(2026, 10, 16)),
Err(IdxError::ParseError(_))
));
}
}

View file

@ -0,0 +1,62 @@
use std::time::Duration;
use crate::error::IdxError;
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36";
/// Tries per request. In some stretches of seconds the server answers its
/// own nginx 404 page to a third or more of requests and then recovers, so a
/// 404 is retried (an unknown ticker is a 200, not a 404).
const ATTEMPTS: u32 = 5;
const FIRST_PAUSE: Duration = Duration::from_millis(250);
pub(super) struct IpotClient {
agent: ureq::Agent,
}
impl IpotClient {
pub(super) fn new() -> Self {
let agent: ureq::Agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_recv_body(Some(Duration::from_secs(10)))
// A server that stalls before sending headers must not hold a
// call (and an MCP call slot) forever.
.timeout_global(Some(Duration::from_secs(20)))
.build()
.into();
Self { agent }
}
pub(super) fn fetch_html(&self, url: &str) -> Result<String, IdxError> {
let mut pause = FIRST_PAUSE;
let mut last = None;
for attempt in 1..=ATTEMPTS {
if attempt > 1 {
std::thread::sleep(pause);
pause *= 2;
}
let response = self
.agent
.get(url)
.header("User-Agent", USER_AGENT)
.header("Accept", "text/html")
.call();
match response {
Ok(ok) => {
return ok
.into_body()
.read_to_string()
.map_err(|e| IdxError::Http(format!("ipot broker summary: {e}")));
}
Err(ureq::Error::StatusCode(code)) if code == 404 || code >= 500 => {
last = Some(code);
}
Err(ureq::Error::StatusCode(429)) => return Err(IdxError::RateLimited),
Err(err) => return Err(IdxError::Http(format!("ipot broker summary: {err}"))),
}
}
Err(IdxError::Http(format!(
"ipot broker summary: status {} after {ATTEMPTS} attempts",
last.unwrap_or_default()
)))
}
}

View file

@ -0,0 +1,101 @@
//! Indo Premier Sekuritas (IPOT) broker summary: which brokers bought and
//! sold a stock. No login or key.
//!
//! `GET https://www.indopremier.com/module/saham/include/data-brokersummary.php`
//! `?code=BBCA&start=MM/DD/YYYY&end=MM/DD/YYYY&fd=all|F|D&board=all|RG|TN|NG`
//! answers an HTML fragment with the top 10 buyers and the top 10 sellers,
//! ranked by lots, and totals for the range. Figures of a million and more
//! are abbreviated to one decimal (`1.6 M`). A range without trades gives
//! blank rows and zero totals; an unknown code gives `Invalid stock code!`
//! with HTTP 200. The endpoint's behavior was first mapped by fzn0x/idx
//! (MIT).
mod client;
pub mod parse;
use client::IpotClient;
pub use parse::{BrokerSummaryRaw, IpotPage, parse_broker_summary, parse_figure};
use super::BrokerSummaryProvider;
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
use crate::error::IdxError;
const ENDPOINT: &str = "https://www.indopremier.com/module/saham/include/data-brokersummary.php";
/// The request URL for `code` (a bare IDX code such as `BBCA`).
pub fn broker_summary_url(code: &str, query: &BrokerQuery) -> String {
let investor = match query.investor {
BrokerInvestor::All => "all",
BrokerInvestor::Foreign => "F",
BrokerInvestor::Domestic => "D",
};
let board = match query.board {
BrokerBoard::All => "all",
BrokerBoard::Regular => "RG",
BrokerBoard::Cash => "TN",
BrokerBoard::Negotiated => "NG",
};
format!(
"{ENDPOINT}?code={code}&start={}&end={}&fd={investor}&board={board}",
query.from.format("%m/%d/%Y"),
query.to.format("%m/%d/%Y"),
)
}
/// A parsed page as a summary; an unknown code is `SymbolNotFound`.
pub(crate) fn summary_from_html(code: &str, html: &str) -> Result<BrokerSummaryRaw, IdxError> {
match parse_broker_summary(html)? {
IpotPage::Invalid => Err(IdxError::SymbolNotFound(code.to_string())),
IpotPage::Summary(raw) => Ok(raw),
}
}
pub struct IpotProvider {
client: IpotClient,
}
impl IpotProvider {
pub fn new() -> Self {
Self {
client: IpotClient::new(),
}
}
}
impl Default for IpotProvider {
fn default() -> Self {
Self::new()
}
}
impl BrokerSummaryProvider for IpotProvider {
fn broker_summary(
&self,
code: &str,
query: &BrokerQuery,
) -> Result<BrokerSummaryRaw, IdxError> {
let html = self.client.fetch_html(&broker_summary_url(code, query))?;
summary_from_html(code, &html)
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use super::*;
#[test]
fn test_broker_summary_url_maps_filters_to_source_codes() {
let query = BrokerQuery {
from: NaiveDate::from_ymd_opt(2026, 10, 12).unwrap(),
to: NaiveDate::from_ymd_opt(2026, 10, 16).unwrap(),
investor: BrokerInvestor::Foreign,
board: BrokerBoard::Regular,
};
assert_eq!(
broker_summary_url("BBCA", &query),
"https://www.indopremier.com/module/saham/include/data-brokersummary.php?code=BBCA&start=10/12/2026&end=10/16/2026&fd=F&board=RG"
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,11 @@
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it. //! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
use super::types::{Bar, Fundamentals, Interval, Period, Quote}; use super::types::{Bar, Fundamentals, Interval, Period, Quote};
use super::{FundamentalsProvider, HistoryProvider, QuoteProvider, msn, yahoo}; use super::{
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, QuoteProvider,
TradingSummaryProvider, idx, ipot, msn, yahoo,
};
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
use crate::config::ProviderKind; use crate::config::ProviderKind;
use crate::error::IdxError; use crate::error::IdxError;
@ -10,6 +14,15 @@ pub(crate) fn enabled() -> bool {
std::env::var("IDX_USE_MOCK_PROVIDER").is_ok() std::env::var("IDX_USE_MOCK_PROVIDER").is_ok()
} }
/// `IDX_MOCK_TODAY` (YYYY-MM-DD) pins today's date under the mock provider.
pub(crate) fn today() -> Option<chrono::NaiveDate> {
if !enabled() {
return None;
}
let raw = std::env::var("IDX_MOCK_TODAY").ok()?;
chrono::NaiveDate::parse_from_str(&raw, "%Y-%m-%d").ok()
}
pub struct MockProvider { pub struct MockProvider {
quote: Result<Quote, IdxError>, quote: Result<Quote, IdxError>,
fundamentals: Result<Fundamentals, IdxError>, fundamentals: Result<Fundamentals, IdxError>,
@ -117,3 +130,61 @@ impl HistoryProvider for MockProvider {
self.history.clone() self.history.clone()
} }
} }
/// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from>
/// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an
/// unknown code.
pub struct MockBrokerProvider;
impl BrokerSummaryProvider for MockBrokerProvider {
fn broker_summary(
&self,
code: &str,
query: &BrokerQuery,
) -> Result<ipot::BrokerSummaryRaw, IdxError> {
if std::env::var("IDX_MOCK_ERROR").is_ok() {
return Err(IdxError::ProviderUnavailable);
}
let mut name = format!(
"ipot_brokersummary_{}_{}",
code.to_ascii_lowercase(),
query.from.format("%Y%m%d")
);
if query.to != query.from {
name.push_str(&query.to.format("_%Y%m%d").to_string());
}
if (query.investor, query.board) != (BrokerInvestor::All, BrokerBoard::All) {
name.push_str(&format!(
"_{}_{}",
query.investor.as_str(),
query.board.as_str()
));
}
let html = std::fs::read_to_string(format!("tests/fixtures/{name}.html"))
.unwrap_or_else(|_| "Invalid stock code!".to_string());
ipot::summary_from_html(code, &html)
}
}
/// Trading summaries from `tests/fixtures/idx_stocksummary_<date>.json`; a
/// date without a fixture had no trading.
pub struct MockTradingSummaryProvider;
impl TradingSummaryProvider for MockTradingSummaryProvider {
fn stock_summary(
&self,
date: chrono::NaiveDate,
) -> Result<Vec<crate::flow::types::StockDay>, IdxError> {
if std::env::var("IDX_MOCK_ERROR").is_ok() {
return Err(IdxError::ProviderUnavailable);
}
let path = format!(
"tests/fixtures/idx_stocksummary_{}.json",
date.format("%Y%m%d")
);
match std::fs::read_to_string(path) {
Ok(raw) => idx::parse_stock_summary(&raw, date),
Err(_) => Ok(Vec::new()),
}
}
}

View file

@ -1,3 +1,5 @@
pub mod idx;
pub mod ipot;
#[cfg(feature = "testing")] #[cfg(feature = "testing")]
pub mod mock; pub mod mock;
pub mod msn; pub mod msn;
@ -6,6 +8,7 @@ pub mod yahoo;
use chrono::FixedOffset; use chrono::FixedOffset;
use crate::broker::types::BrokerQuery;
use crate::config::{HistoryProviderKind, ProviderKind}; use crate::config::{HistoryProviderKind, ProviderKind};
use crate::error::IdxError; use crate::error::IdxError;
use types::{ use types::{
@ -41,6 +44,26 @@ pub trait HistoryProvider {
) -> Result<Vec<Bar>, IdxError>; ) -> Result<Vec<Bar>, IdxError>;
} }
/// Which brokers bought and sold a stock: a separate capability, served by
/// IPOT whichever market provider is selected.
pub trait BrokerSummaryProvider {
/// `code` is the bare IDX code, e.g. `BBCA`.
fn broker_summary(
&self,
code: &str,
query: &BrokerQuery,
) -> Result<ipot::BrokerSummaryRaw, IdxError>;
}
/// Every stock's trading summary for one session (IDX's own site).
pub trait TradingSummaryProvider {
/// Empty for a day without trading.
fn stock_summary(
&self,
date: chrono::NaiveDate,
) -> Result<Vec<crate::flow::types::StockDay>, IdxError>;
}
pub trait FundamentalsProvider { pub trait FundamentalsProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>; fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
} }
@ -258,6 +281,31 @@ fn msn_capability_error(subject: &str) -> IdxError {
IdxError::Unsupported(format!("{subject}: command requires --provider msn")) IdxError::Unsupported(format!("{subject}: command requires --provider msn"))
} }
pub fn broker_provider() -> Box<dyn BrokerSummaryProvider> {
#[cfg(feature = "testing")]
if mock::enabled() {
return Box::new(mock::MockBrokerProvider);
}
Box::new(ipot::IpotProvider::new())
}
pub fn trading_summary_provider() -> Box<dyn TradingSummaryProvider> {
#[cfg(feature = "testing")]
if mock::enabled() {
return Box::new(mock::MockTradingSummaryProvider);
}
Box::new(idx::IdxSiteProvider)
}
/// Today in WIB, the date IDX trades on.
pub(crate) fn today_wib() -> chrono::NaiveDate {
#[cfg(feature = "testing")]
if let Some(today) = mock::today() {
return today;
}
chrono::Utc::now().with_timezone(&idx_offset()).date_naive()
}
/// The provider that serves history for `provider` under `history_mode`. /// The provider that serves history for `provider` under `history_mode`.
pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind) -> ProviderKind { pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind) -> ProviderKind {
match history_mode { match history_mode {

View file

@ -70,6 +70,7 @@ pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result<Quote, Id
prev_close, prev_close,
avg_volume: round_u64(quote.average_volume), avg_volume: round_u64(quote.average_volume),
as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()), as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()),
behind_session: false,
}) })
} }
@ -718,6 +719,7 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
prev_close, prev_close,
avg_volume: round_u64(q.average_volume), avg_volume: round_u64(q.average_volume),
as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()), as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()),
behind_session: false,
}) })
}) })
.collect(); .collect();

View file

@ -50,6 +50,13 @@ pub struct Quote {
/// Suspended stocks can carry a last trade months in the past. /// Suspended stocks can carry a last trade months in the past.
#[serde(default)] #[serde(default)]
pub as_of: Option<DateTime<FixedOffset>>, pub as_of: Option<DateTime<FixedOffset>>,
/// True when `as_of` is before the latest IDX session that has ended: the
/// provider has not caught up yet (MSN can lag hours after the close), the
/// stock did not trade, or the day was an exchange holiday. False when
/// `as_of` is unknown and for non-IDX symbols and regions. Set when the
/// quote is returned, not when it is cached.
#[serde(default)]
pub behind_session: bool,
} }
/// OHLC candle data normalized from Yahoo Finance chart indicators. /// OHLC candle data normalized from Yahoo Finance chart indicators.

View file

@ -64,6 +64,7 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
.regular_market_time .regular_market_time
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)) .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
.map(|t| t.with_timezone(&crate::api::idx_offset())), .map(|t| t.with_timezone(&crate::api::idx_offset())),
behind_session: false,
}) })
} }

View file

@ -0,0 +1,92 @@
# IDX exchange members: code<TAB>name, from idx.co.id
# primary/ExchangeMember/GetBrokerCodeList, fetched 2026-10-18 (89 members).
# A code missing here is shown without a name.
AD SUKADANA PRIMA SEKURITAS
AF HARITA KENCANA SEKURITAS
AG KIWOOM SEKURITAS INDONESIA
AH SHINHAN SEKURITAS INDONESIA
AI KAY HIAN SEKURITAS
AK UBS SEKURITAS INDONESIA
AO ERDIKHA ELIT SEKURITAS
AP PACIFIC SEKURITAS INDONESIA
AR BINAARTHA SEKURITAS
AT PHINTRACO SEKURITAS
AZ SUCOR SEKURITAS
BB VERDHANA SEKURITAS INDONESIA
BF INTI FIKASA SEKURITAS
BK J.P. MORGAN SEKURITAS INDONESIA
BQ KOREA INVESTMENT AND SEKURITAS INDONESIA
BR TRUST SEKURITAS
BS EQUITY SEKURITAS INDONESIA
CC MANDIRI SEKURITAS
CD MEGA CAPITAL SEKURITAS
CP KB VALBURY SEKURITAS
DD MAKINDO SEKURITAS
DH SINARMAS SEKURITAS
DP DBS VICKERS SEKURITAS INDONESIA
DR RHB SEKURITAS INDONESIA
DU KAF SEKURITAS INDONESIA
DX BAHANA SEKURITAS
EL EVERGREEN SEKURITAS INDONESIA
EP MNC SEKURITAS
ES EKOKAPITAL SEKURITAS
FO FORTE GLOBAL SEKURITAS
FS YUANTA SEKURITAS INDONESIA
FZ WATERFRONT SEKURITAS INDONESIA
GA BNC SEKURITAS INDONESIA
GI WEBULL SEKURITAS INDONESIA
GR PANIN SEKURITAS TBK.
HD KGI SEKURITAS INDONESIA
HP HENAN PUTIHRAI SEKURITAS
IC INTEGRITY CAPITAL SEKURITAS
ID ANUGERAH SEKURITAS INDONESIA
IF SAMUEL SEKURITAS INDONESIA
IH INDO HARVEST SEKURITAS
II DANATAMA MAKMUR SEKURITAS
IN INVESTINDO NUSANTARA SEKURITAS
IT INTI TELADAN SEKURITAS
IU INDO CAPITAL SEKURITAS
KI CIPTADANA SEKURITAS ASIA
KK PHILLIP SEKURITAS INDONESIA
KZ CLSA SEKURITAS INDONESIA
LG TRIMEGAH SEKURITAS INDONESIA TBK.
LS RELIANCE SEKURITAS INDONESIA TBK.
MG SEMESTA INDOVEST SEKURITAS
MI VICTORIA SEKURITAS INDONESIA
MU MINNA PADI INVESTAMA SEKURITAS TBK
NI BNI SEKURITAS
OD BRI DANAREKSA SEKURITAS
OK NET SEKURITAS
PC FAC SEKURITAS INDONESIA
PD INDO PREMIER SEKURITAS
PF DANASAKTI SEKURITAS INDONESIA
PG PANCA GLOBAL SEKURITAS
PI MAGENTA KAPITAL SEKURITAS INDONESIA
PO PILARMAS INVESTINDO SEKURITAS
PP ALDIRACITA SEKURITAS INDONESIA
PS PARAMITRA ALFA SEKURITAS
QA TUNTUN SEKURITAS INDONESIA
RB INA SEKURITAS INDONESIA
RF BUANA CAPITAL SEKURITAS
RG PROFINDO SEKURITAS INDONESIA
RO PLUANG MAJU SEKURITAS
RS YULIE SEKURITAS INDONESIA TBK.
RX MACQUARIE SEKURITAS INDONESIA
SA ELIT SUKSES SEKURITAS
SF SURYA FAJAR SEKURITAS
SH ARTHA SEKURITAS INDONESIA
SQ BCA SEKURITAS
SS SUPRA SEKURITAS INDONESIA
TF LABA SEKURITAS INDONESIA
TP OCBC SEKURITAS INDONESIA
TS DWIDANA SAKTI SEKURITAS
XA NH KORINDO SEKURITAS INDONESIA
XC AJAIB SEKURITAS ASIA
XL STOCKBIT SEKURITAS DIGITAL
YB YAKIN BERTUMBUH SEKURITAS
YJ LOTUS ANDALAN SEKURITAS
YO AMANTARA SEKURITAS INDONESIA
YP MIRAE ASSET SEKURITAS INDONESIA
YU CGS INTERNATIONAL SEKURITAS INDONESIA
ZP MAYBANK SEKURITAS INDONESIA
ZR BUMIPUTERA SEKURITAS
Can't render this file because it has a wrong number of fields in line 4.

View file

@ -0,0 +1,432 @@
//! Broker summaries: which IDX brokers bought and sold a stock over a date
//! range (IPOT, end of day). See docs/ARCHITECTURE.md.
pub mod types;
use std::collections::HashMap;
use std::sync::OnceLock;
use chrono::{Datelike, Duration, NaiveDate, Weekday};
use crate::api::ipot::BrokerSummaryRaw;
use crate::api::{broker_provider, today_wib};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::error::IdxError;
use types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerRange, BrokerSummary};
const SOURCE: &str = "ipot";
/// Cache bucket for normalized summaries, versioned with their shape: a
/// summary cached before `buy_value`/`sell_value` existed would read back
/// without them (and an investor-filtered one without its totals), so a new
/// shape gets a new bucket rather than serving such entries.
const CACHE_BUCKET: &str = "ipot-broker-v2";
/// Longest range one summary may cover.
pub const MAX_RANGE_DAYS: i64 = 366;
/// Calendar days searched back for the latest session with trades.
const LATEST_LOOKBACK_DAYS: i64 = 10;
/// A range that ended before today no longer changes.
const TTL_CLOSED_SECS: u64 = 7 * 24 * 3600;
/// Today's summary moves with every trade.
const TTL_TODAY_SECS: u64 = 5 * 60;
const CODES: &str = include_str!("codes.tsv");
/// Member name for an IDX broker code such as `CC`; `None` when the code is
/// not on the bundled member list.
pub fn broker_name(code: &str) -> Option<&'static str> {
static NAMES: OnceLock<HashMap<&'static str, &'static str>> = OnceLock::new();
NAMES
.get_or_init(|| {
CODES
.lines()
.filter(|line| !line.starts_with('#'))
.filter_map(|line| line.split_once('\t'))
.collect()
})
.get(code.trim().to_ascii_uppercase().as_str())
.copied()
}
pub struct Broker<'a> {
pub(crate) idx: &'a Idx,
}
impl Broker<'_> {
/// Today in WIB, the date `BrokerRange::from_args` defaults `to` to.
pub fn today(&self) -> NaiveDate {
today_wib()
}
/// The summary a frontend asked for: the latest session or a range.
pub fn fetch(
&self,
symbol: &str,
range: BrokerRange,
investor: BrokerInvestor,
board: BrokerBoard,
policy: CachePolicy,
) -> Result<Fetched<BrokerSummary>, IdxError> {
match range {
BrokerRange::Latest => self.latest(symbol, investor, board, policy),
BrokerRange::Dates { from, to } => self.summary(
symbol,
&BrokerQuery {
from,
to,
investor,
board,
},
policy,
),
}
}
/// The broker summary for `symbol` over `query`'s range. An empty
/// summary means no trades (weekend, holiday, suspension).
pub fn summary(
&self,
symbol: &str,
query: &BrokerQuery,
policy: CachePolicy,
) -> Result<Fetched<BrokerSummary>, IdxError> {
let (resolved, code) = self.idx.idx_code(symbol, "a broker summary")?;
let today = today_wib();
check_range(query, today)?;
let key = format!(
"{code}-{}-{}-{}-{}",
query.from,
query.to,
query.investor.as_str(),
query.board.as_str()
);
let provider = broker_provider();
cached_fetch(
&self.idx.cache,
CacheSpec {
bucket: CACHE_BUCKET,
key: &key,
ttl_secs: if query.to < today {
TTL_CLOSED_SECS
} else {
TTL_TODAY_SECS
},
subject: &resolved,
source: SOURCE,
},
policy,
|| {
provider
.broker_summary(&code, query)
.and_then(|raw| check_echoed_dates(query, raw))
},
|raw| Ok(summary_from_raw(&resolved, query, raw)),
)
}
/// The summary of the latest session with trades, searching back from
/// today (WIB) over weekdays.
pub fn latest(
&self,
symbol: &str,
investor: BrokerInvestor,
board: BrokerBoard,
policy: CachePolicy,
) -> Result<Fetched<BrokerSummary>, IdxError> {
let (resolved, _) = self.idx.idx_code(symbol, "a broker summary")?;
let today = today_wib();
for back in 0..LATEST_LOOKBACK_DAYS {
let day = today - Duration::days(back);
if matches!(day.weekday(), Weekday::Sat | Weekday::Sun) {
continue;
}
let query = BrokerQuery {
from: day,
to: day,
investor,
board,
};
match self.summary(symbol, &query, policy) {
Ok(fetched) if !fetched.data.is_empty() => return Ok(fetched),
Ok(_) => {}
// Offline, an uncached day says nothing; look further back.
Err(IdxError::CacheMiss(_)) if policy == CachePolicy::Offline => {}
Err(err) => return Err(err),
}
}
if policy == CachePolicy::Offline {
return Err(IdxError::CacheMiss(format!(
"{CACHE_BUCKET}/{resolved} (no cached session in the last {LATEST_LOOKBACK_DAYS} days)"
)));
}
Err(IdxError::NoMarketData(format!(
"{resolved}: no trades in the last {LATEST_LOOKBACK_DAYS} days"
)))
}
}
/// 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.
pub(crate) fn summary_from_raw(
symbol: &str,
query: &BrokerQuery,
raw: BrokerSummaryRaw,
) -> BrokerSummary {
let one_side = query.investor != BrokerInvestor::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),
to: raw.to.unwrap_or(query.to),
investor: query.investor,
board: query.board,
buyers: raw.buyers,
sellers: raw.sellers,
total_value: raw.total_value.filter(|_| !one_side),
total_lots: raw.total_lots.filter(|_| !one_side),
average_price: raw.average_price.filter(|_| !one_side),
buy_value,
sell_value,
foreign_net_value: foreign_net,
rounded: raw.rounded,
};
for flow in summary.buyers.iter_mut().chain(summary.sellers.iter_mut()) {
flow.name = broker_name(&flow.code).map(str::to_string);
}
summary
}
/// The page echoes the dates it covers; another range's figures must not be
/// cached or reported as the requested one's.
fn check_echoed_dates(
query: &BrokerQuery,
raw: BrokerSummaryRaw,
) -> Result<BrokerSummaryRaw, IdxError> {
if raw.from == Some(query.from) && raw.to == Some(query.to) {
return Ok(raw);
}
let echoed = |date: Option<NaiveDate>| date.map_or("none".to_string(), |date| date.to_string());
Err(IdxError::ParseError(format!(
"ipot broker summary: asked for {} to {}, the page covers {} to {}",
query.from,
query.to,
echoed(raw.from),
echoed(raw.to)
)))
}
fn check_range(query: &BrokerQuery, today: NaiveDate) -> Result<(), IdxError> {
if query.from > query.to {
return Err(IdxError::InvalidInput(format!(
"--from {} is after --to {}",
query.from, query.to
)));
}
if query.to > today {
return Err(IdxError::InvalidInput(format!(
"{} is in the future (today is {today} in WIB)",
query.to
)));
}
if (query.to - query.from).num_days() >= MAX_RANGE_DAYS {
return Err(IdxError::InvalidInput(format!(
"a broker summary covers at most {MAX_RANGE_DAYS} days"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn date(y: i32, m: u32, d: u32) -> NaiveDate {
NaiveDate::from_ymd_opt(y, m, d).unwrap()
}
fn query(from: NaiveDate, to: NaiveDate) -> BrokerQuery {
BrokerQuery {
from,
to,
investor: BrokerInvestor::All,
board: BrokerBoard::All,
}
}
#[test]
fn test_broker_name_reads_the_bundled_member_list() {
assert_eq!(broker_name("cc"), Some("MANDIRI SEKURITAS"));
assert_eq!(broker_name("XL"), Some("STOCKBIT SEKURITAS DIGITAL"));
assert_eq!(broker_name("??"), None);
}
#[test]
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();
crate::api::ipot::summary_from_html("BBCA", &html).unwrap()
};
let all = summary_from_raw(
"BBCA.JK",
&query(day, day),
read("ipot_brokersummary_bbca_20261016.html"),
);
assert_eq!(all.total_value, Some(6_600_000_000_000));
assert_eq!(all.foreign_net_value, Some(-2_300_000_000_000));
assert_eq!(all.sellers[0].name.as_deref(), Some("MANDIRI SEKURITAS"));
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 regular = side(
BrokerInvestor::Foreign,
BrokerBoard::Regular,
"ipot_brokersummary_bbca_20261016_foreign_regular.html",
);
assert_eq!(
(
regular.total_value,
regular.total_lots,
regular.average_price
),
(None, None, None)
);
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]
fn test_broker_range_from_args() {
let today = date(2026, 10, 18);
let range = |date, from, to| BrokerRange::from_args(date, from, to, today);
assert_eq!(range(None, None, None).unwrap(), BrokerRange::Latest);
assert_eq!(
range(Some("2026-10-16"), None, None).unwrap(),
BrokerRange::Dates {
from: date(2026, 10, 16),
to: date(2026, 10, 16)
}
);
assert_eq!(
range(None, Some("2026-10-12"), None).unwrap(),
BrokerRange::Dates {
from: date(2026, 10, 12),
to: today
}
);
for (d, f, t) in [
(None, None, Some("2026-10-16")),
(Some("2026-10-16"), Some("2026-10-12"), None),
(Some("16/10/2026"), None, None),
(None, Some("2026-10-12"), Some("tomorrow")),
] {
assert!(
matches!(range(d, f, t), Err(IdxError::InvalidInput(_))),
"{d:?} {f:?} {t:?}"
);
}
}
#[test]
fn test_check_range_bounds() {
let today = date(2026, 10, 18);
assert!(check_range(&query(date(2026, 10, 12), date(2026, 10, 16)), today).is_ok());
assert!(check_range(&query(today, today), today).is_ok());
assert!(check_range(&query(date(2025, 10, 16), date(2026, 10, 16)), today).is_ok());
for bad in [
query(date(2026, 10, 16), date(2026, 10, 12)),
query(date(2026, 10, 19), date(2026, 10, 19)),
query(date(2025, 10, 15), date(2026, 10, 16)),
] {
assert!(
matches!(check_range(&bad, today), Err(IdxError::InvalidInput(_))),
"{bad:?}"
);
}
}
#[test]
fn test_check_echoed_dates_rejects_another_sessions_page() {
let friday = date(2026, 10, 16);
let page = || BrokerSummaryRaw {
from: Some(friday),
to: Some(friday),
..BrokerSummaryRaw::default()
};
assert!(check_echoed_dates(&query(friday, friday), page()).is_ok());
let thursday = date(2026, 10, 15);
for (asked, raw) in [
(query(thursday, thursday), page()),
(query(thursday, friday), page()),
(query(friday, friday), BrokerSummaryRaw::default()),
] {
assert!(
matches!(
check_echoed_dates(&asked, raw),
Err(IdxError::ParseError(_))
),
"{asked:?}"
);
}
}
}

View file

@ -0,0 +1,241 @@
//! Broker summary types: which IDX brokers bought and sold a stock.
use std::str::FromStr;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::error::IdxError;
/// Whose trades a broker summary counts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum BrokerInvestor {
/// Every trade.
#[default]
All,
/// Trades by foreign investors only.
Foreign,
/// Trades by domestic investors only.
Domestic,
}
impl BrokerInvestor {
pub const ALL: [Self; 3] = [Self::All, Self::Foreign, Self::Domestic];
pub fn as_str(self) -> &'static str {
match self {
Self::All => "all",
Self::Foreign => "foreign",
Self::Domestic => "domestic",
}
}
}
impl FromStr for BrokerInvestor {
type Err = IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"all" => Ok(Self::All),
"foreign" => Ok(Self::Foreign),
"domestic" => Ok(Self::Domestic),
_ => Err(IdxError::InvalidInput(format!(
"invalid investor '{value}'; use all, foreign or domestic"
))),
}
}
}
/// Which market board's trades a broker summary counts.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum BrokerBoard {
/// Every board.
#[default]
All,
/// Regular market (RG), the continuous order book.
Regular,
/// Cash market (TN).
Cash,
/// Negotiated market (NG): pre-arranged block trades.
Negotiated,
}
impl BrokerBoard {
pub const ALL: [Self; 4] = [Self::All, Self::Regular, Self::Cash, Self::Negotiated];
pub fn as_str(self) -> &'static str {
match self {
Self::All => "all",
Self::Regular => "regular",
Self::Cash => "cash",
Self::Negotiated => "negotiated",
}
}
}
impl FromStr for BrokerBoard {
type Err = IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"all" => Ok(Self::All),
"regular" => Ok(Self::Regular),
"cash" => Ok(Self::Cash),
"negotiated" => Ok(Self::Negotiated),
_ => Err(IdxError::InvalidInput(format!(
"invalid board '{value}'; use all, regular, cash or negotiated"
))),
}
}
}
/// The source's classification of a securities house. This is the broker's
/// own status, not the nationality of its clients.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BrokerKind {
Local,
Foreign,
/// State-owned securities house (BUMN).
StateOwned,
}
impl BrokerKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Local => "local",
Self::Foreign => "foreign",
Self::StateOwned => "state_owned",
}
}
}
/// Which sessions a frontend asked for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrokerRange {
/// The latest session with trades.
Latest,
/// `from..=to`, WIB dates.
Dates { from: NaiveDate, to: NaiveDate },
}
impl BrokerRange {
/// From a frontend's `date`, or `from` with an optional `to` (default
/// today), as `YYYY-MM-DD`; none of them means the latest session.
pub fn from_args(
date: Option<&str>,
from: Option<&str>,
to: Option<&str>,
today: NaiveDate,
) -> Result<Self, IdxError> {
let parse = |name: &str, value: &str| {
NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d").map_err(|_| {
IdxError::InvalidInput(format!("invalid {name} '{value}'; use YYYY-MM-DD"))
})
};
match (date, from, to) {
(None, None, None) => Ok(Self::Latest),
(Some(date), None, None) => {
let day = parse("date", date)?;
Ok(Self::Dates { from: day, to: day })
}
(None, Some(from), to) => Ok(Self::Dates {
from: parse("from", from)?,
to: to.map(|to| parse("to", to)).transpose()?.unwrap_or(today),
}),
(None, None, Some(_)) => Err(IdxError::InvalidInput(
"to needs from (or use date for one session)".to_string(),
)),
(Some(_), _, _) => Err(IdxError::InvalidInput(
"date cannot be combined with from or to".to_string(),
)),
}
}
}
/// The trading days a broker summary covers, both ends included (WIB dates).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerQuery {
pub from: NaiveDate,
pub to: NaiveDate,
#[serde(default)]
pub investor: BrokerInvestor,
#[serde(default)]
pub board: BrokerBoard,
}
/// One broker on one side of a summary: gross buys for a buyer, gross sells
/// for a seller. A broker can appear on both sides.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerFlow {
/// Position on its side as the source ranks it, 1 = most lots.
pub rank: u32,
/// IDX member code, e.g. `CC`.
pub code: String,
/// Member name from the bundled IDX member list; `None` for a code that
/// is not on it.
pub name: Option<String>,
/// The source's classification; `None` when it gives none.
pub kind: Option<BrokerKind>,
/// Lots bought or sold (one lot is 100 shares).
pub lots: Option<i64>,
/// Value bought or sold, whole IDR.
pub value: Option<i64>,
/// Average price, whole IDR.
pub average_price: Option<i64>,
}
/// The top buyers and sellers of one stock over a date range, as published.
/// Nothing is netted or estimated.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BrokerSummary {
/// Resolved ticker, e.g. `BBCA.JK`.
pub symbol: String,
/// First trading date covered, as the source applied it.
pub from: NaiveDate,
/// Last trading date covered, as the source applied it.
pub to: NaiveDate,
pub investor: BrokerInvestor,
pub board: BrokerBoard,
/// Top buyers by lots, most first (up to 10).
pub buyers: Vec<BrokerFlow>,
/// Top sellers by lots, most first (up to 10).
pub sellers: Vec<BrokerFlow>,
/// Value traded over the range on the selected board, whole IDR. `None`
/// with one investor side selected: the source's footer then covers only
/// that side's buying, not the trades the lists show.
pub total_value: Option<i64>,
/// Lots traded over the range on the selected board; `None` as for
/// `total_value`.
pub total_lots: Option<i64>,
/// 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 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.
pub rounded: bool,
}
impl BrokerSummary {
/// No trades in the range (weekend, holiday, suspension).
pub fn is_empty(&self) -> bool {
self.buyers.is_empty() && self.sellers.is_empty()
}
}

View file

@ -86,6 +86,14 @@ impl Idx {
crate::market::Market { idx: self } crate::market::Market { idx: self }
} }
pub fn broker(&self) -> crate::broker::Broker<'_> {
crate::broker::Broker { idx: self }
}
pub fn flow(&self) -> crate::flow::Flow<'_> {
crate::flow::Flow { idx: self }
}
pub fn financial(&self) -> crate::financial::Financial<'_> { pub fn financial(&self) -> crate::financial::Financial<'_> {
crate::financial::Financial { idx: self } crate::financial::Financial { idx: self }
} }
@ -103,6 +111,18 @@ impl Idx {
crate::api::resolve_symbol(symbol, &self.config.exchange) crate::api::resolve_symbol(symbol, &self.config.exchange)
} }
/// The resolved symbol and its bare IDX code, for sources that cover
/// IDX (`.JK`) stocks only; `what` names the data in the error.
pub(crate) fn idx_code(&self, symbol: &str, what: &str) -> Result<(String, String), IdxError> {
let resolved = self.resolve(symbol)?;
match resolved.strip_suffix(".JK") {
Some(code) if !code.is_empty() => Ok((resolved.clone(), code.to_string())),
_ => Err(IdxError::Unsupported(format!(
"{resolved}: {what} covers IDX (.JK) stocks only"
))),
}
}
pub(crate) fn bucket(&self, key: &str) -> String { pub(crate) fn bucket(&self, key: &str) -> String {
format!("{}-{key}", self.provider.kind().as_str()) format!("{}-{key}", self.provider.kind().as_str())
} }

View file

@ -0,0 +1,499 @@
//! Foreign flow: what foreign investors bought and sold of a stock, session
//! by session (end of day). IPOT (the default) needs two plain requests per
//! session; IDX's trading summary covers every stock in one request per
//! session, cached by date, but needs curl-impersonate.
pub mod types;
use chrono::{DateTime, Datelike, Duration, NaiveDate, Utc, Weekday};
use crate::api::{today_wib, trading_summary_provider};
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery, BrokerSummary};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::error::IdxError;
use types::{FlowRange, FlowSource, ForeignFlow, ForeignFlowDay, StockDay};
/// Most sessions `FlowRange::Latest` reads.
pub const MAX_SESSIONS: usize = 60;
/// Longest `FlowRange::Dates` range, calendar days.
pub const MAX_RANGE_DAYS: i64 = 92;
/// A past session no longer changes.
const TTL_CLOSED_SECS: u64 = 30 * 24 * 3600;
const TTL_TODAY_SECS: u64 = 5 * 60;
pub struct Flow<'a> {
pub(crate) idx: &'a Idx,
}
/// What one weekday held for the stock.
enum Day {
/// The market (or, for IPOT, the stock) did not trade.
NoSession,
/// The market traded without the stock (a suspension).
Absent,
Traded {
day: Box<ForeignFlowDay>,
rounded: bool,
},
}
struct Fetch {
day: Day,
fetched_at: DateTime<Utc>,
stale: bool,
}
impl Flow<'_> {
/// Today in WIB.
pub fn today(&self) -> NaiveDate {
today_wib()
}
/// Foreign buying and selling of `symbol` over `range`, from `source`.
pub fn foreign(
&self,
symbol: &str,
range: FlowRange,
source: FlowSource,
policy: CachePolicy,
) -> Result<Fetched<ForeignFlow>, IdxError> {
let (resolved, code) = self.idx.idx_code(symbol, "foreign flow")?;
let today = today_wib();
let (dates, wanted, skip_misses) = match range {
FlowRange::Latest { sessions } => {
if !(1..=MAX_SESSIONS).contains(&sessions) {
return Err(IdxError::InvalidInput(format!(
"days must be 1 to {MAX_SESSIONS} (got {sessions})"
)));
}
let lookback = 2 * sessions as i64 + 10;
let dates: Vec<NaiveDate> = (0..lookback)
.map(|back| today - Duration::days(back))
.collect();
// Offline, an uncached day says nothing; look further back.
(dates, sessions, policy == CachePolicy::Offline)
}
FlowRange::Dates { from, to } => {
check_range(from, to, today)?;
let dates = (0..=(to - from).num_days())
.rev()
.map(|offset| from + Duration::days(offset))
.collect();
(dates, usize::MAX, false)
}
};
let mut days = Vec::new();
let mut sessions = 0;
let mut rounded = false;
let mut fetched_at: Option<DateTime<Utc>> = None;
let mut stale = false;
for date in dates {
if sessions == wanted {
break;
}
if matches!(date.weekday(), Weekday::Sat | Weekday::Sun) {
continue;
}
let fetch = match source {
FlowSource::Ipot => self.ipot_day(&resolved, date, policy),
FlowSource::Idx => self.idx_day(&code, date, today, policy),
};
let fetch = match fetch {
Ok(fetch) => fetch,
Err(IdxError::CacheMiss(_)) if skip_misses => continue,
Err(err) => return Err(err),
};
// Every consulted session counts toward freshness, empty ones too.
fetched_at = Some(fetched_at.map_or(fetch.fetched_at, |at| at.min(fetch.fetched_at)));
stale |= fetch.stale;
match fetch.day {
Day::NoSession => continue,
Day::Absent => {}
Day::Traded {
day,
rounded: day_rounded,
} => {
days.push(*day);
rounded |= day_rounded;
}
}
sessions += 1;
}
if sessions == 0 {
return Err(if policy == CachePolicy::Offline {
IdxError::CacheMiss(format!(
"{}-flow/{resolved} (no cached session)",
source.as_str()
))
} else {
IdxError::NoMarketData(format!("{resolved}: no sessions in range"))
});
}
days.reverse();
let flow =
foreign_flow(&resolved, source, days, rounded).ok_or(IdxError::SymbolNotFound(code))?;
Ok(Fetched {
data: flow,
source: source.as_str(),
fetched_at: fetched_at.unwrap_or_else(Utc::now),
stale,
})
}
/// One session from IPOT: the all-investor summary for the value traded,
/// the foreign one for foreign buying, selling and net. Both go through
/// the broker summary cache.
fn ipot_day(
&self,
resolved: &str,
date: NaiveDate,
policy: CachePolicy,
) -> Result<Fetch, IdxError> {
let broker = self.idx.broker();
let all_query = BrokerQuery {
from: date,
to: date,
investor: BrokerInvestor::All,
board: BrokerBoard::All,
};
let all = broker.summary(resolved, &all_query, policy)?;
if all.data.is_empty() {
return Ok(Fetch {
day: Day::NoSession,
fetched_at: all.fetched_at,
stale: all.stale,
});
}
let foreign_query = BrokerQuery {
investor: BrokerInvestor::Foreign,
..all_query
};
let foreign = broker.summary(resolved, &foreign_query, policy)?;
Ok(Fetch {
day: Day::Traded {
day: Box::new(ipot_flow_day(date, &all.data, &foreign.data)),
rounded: all.data.rounded || foreign.data.rounded,
},
fetched_at: all.fetched_at.min(foreign.fetched_at),
stale: all.stale || foreign.stale,
})
}
/// One session from IDX's trading summary of every stock.
fn idx_day(
&self,
code: &str,
date: NaiveDate,
today: NaiveDate,
policy: CachePolicy,
) -> Result<Fetch, IdxError> {
let session = self.session(date, today, policy)?;
let day = if session.data.is_empty() {
Day::NoSession
} else {
match session.data.iter().find(|row| row.code == code) {
Some(row) => Day::Traded {
day: Box::new(idx_flow_day(row)),
rounded: false,
},
None => Day::Absent,
}
};
Ok(Fetch {
day,
fetched_at: session.fetched_at,
stale: session.stale,
})
}
/// Every stock's summary for one session; empty without trading.
fn session(
&self,
date: NaiveDate,
today: NaiveDate,
policy: CachePolicy,
) -> Result<Fetched<Vec<StockDay>>, IdxError> {
let provider = trading_summary_provider();
let key = date.to_string();
cached_fetch(
&self.idx.cache,
CacheSpec {
bucket: "idx-stocksummary",
key: &key,
ttl_secs: if date < today {
TTL_CLOSED_SECS
} else {
TTL_TODAY_SECS
},
subject: &key,
source: FlowSource::Idx.as_str(),
},
policy,
|| provider.stock_summary(date),
Ok,
)
}
}
fn check_range(from: NaiveDate, to: NaiveDate, today: NaiveDate) -> Result<(), IdxError> {
if from > to {
return Err(IdxError::InvalidInput(format!(
"--from {from} is after --to {to}"
)));
}
if to > today {
return Err(IdxError::InvalidInput(format!(
"{to} is in the future (today is {today} in WIB)"
)));
}
if (to - from).num_days() >= MAX_RANGE_DAYS {
return Err(IdxError::InvalidInput(format!(
"a foreign flow covers at most {MAX_RANGE_DAYS} days"
)));
}
Ok(())
}
/// `part` in basis points of `whole`, rounded; `None` without a positive whole.
fn bps(part: i64, whole: i64) -> Option<i64> {
(whole > 0).then(|| (part as f64 * 10_000.0 / whole as f64).round() as i64)
}
/// `shares` at the session's average price (value over volume), whole IDR.
fn at_average(shares: Option<i64>, value: Option<i64>, volume: Option<i64>) -> Option<i64> {
match (shares, value, volume) {
(Some(shares), Some(value), Some(volume)) if volume > 0 => {
i64::try_from(i128::from(shares) * i128::from(value) / i128::from(volume)).ok()
}
_ => None,
}
}
/// Foreign buying plus selling over twice `total`.
fn share(buy: Option<i64>, sell: Option<i64>, total: Option<i64>) -> Option<i64> {
let traded = buy?.checked_add(sell?)?;
bps(traded, total?.checked_mul(2)?)
}
/// One IPOT session: the value traded from the all-investor summary, the
/// foreign figures from the foreign one (`buy_value` is its footer,
/// `sell_value` that less the foreign net).
pub fn ipot_flow_day(
date: NaiveDate,
all: &BrokerSummary,
foreign: &BrokerSummary,
) -> ForeignFlowDay {
let foreign_net_value = foreign.foreign_net_value.or(all.foreign_net_value);
ForeignFlowDay {
date,
close: None,
change_bps: None,
volume: all.total_lots.and_then(|lots| lots.checked_mul(100)),
value: all.total_value,
foreign_buy: None,
foreign_sell: None,
foreign_net: None,
foreign_buy_value: foreign.buy_value,
foreign_sell_value: foreign.sell_value,
foreign_net_value,
foreign_share_bps: share(foreign.buy_value, foreign.sell_value, all.total_value),
non_regular_bps: None,
}
}
/// One IDX session for one stock.
pub fn idx_flow_day(row: &StockDay) -> ForeignFlowDay {
let foreign_net = row
.foreign_buy
.zip(row.foreign_sell)
.map(|(buy, sell)| buy - sell);
ForeignFlowDay {
date: row.date,
close: row.close,
change_bps: row
.close
.zip(row.previous)
.and_then(|(close, previous)| bps(close - previous, previous)),
volume: row.volume,
value: row.value,
foreign_buy: row.foreign_buy,
foreign_sell: row.foreign_sell,
foreign_net,
foreign_buy_value: at_average(row.foreign_buy, row.value, row.volume),
foreign_sell_value: at_average(row.foreign_sell, row.value, row.volume),
foreign_net_value: at_average(foreign_net, row.value, row.volume),
foreign_share_bps: share(row.foreign_buy, row.foreign_sell, row.volume),
// `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, regular)| bps(part, part.checked_add(regular)?)),
}
}
fn sum(days: &[ForeignFlowDay], field: fn(&ForeignFlowDay) -> Option<i64>) -> Option<i64> {
days.iter()
.try_fold(0_i64, |total, day| total.checked_add(field(day)?))
}
/// The flow over `days` (one stock, oldest first); `None` without days.
pub fn foreign_flow(
symbol: &str,
source: FlowSource,
days: Vec<ForeignFlowDay>,
rounded: bool,
) -> Option<ForeignFlow> {
let (from, to) = (days.first()?.date, days.last()?.date);
// IDX has exact shares; IPOT has values.
let foreign_share_bps = match source {
FlowSource::Idx => share(
sum(&days, |day| day.foreign_buy),
sum(&days, |day| day.foreign_sell),
sum(&days, |day| day.volume),
),
FlowSource::Ipot => share(
sum(&days, |day| day.foreign_buy_value),
sum(&days, |day| day.foreign_sell_value),
sum(&days, |day| day.value),
),
};
Some(ForeignFlow {
symbol: symbol.to_string(),
source,
from,
to,
foreign_net: sum(&days, |day| day.foreign_net),
foreign_buy_value: sum(&days, |day| day.foreign_buy_value),
foreign_sell_value: sum(&days, |day| day.foreign_sell_value),
foreign_net_value: sum(&days, |day| day.foreign_net_value),
foreign_share_bps,
rounded,
days,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn row(day: u32, close: i64, buy: i64, sell: Option<i64>) -> StockDay {
StockDay {
code: "TEST".to_string(),
date: NaiveDate::from_ymd_opt(2026, 10, day).unwrap(),
previous: Some(1000),
high: None,
low: None,
close: Some(close),
volume: Some(1_000),
value: Some(1_100_000),
frequency: None,
foreign_buy: Some(buy),
foreign_sell: sell,
non_regular_volume: None,
non_regular_value: Some(110_000),
}
}
fn idx_flow(rows: &[StockDay]) -> Option<ForeignFlow> {
let days = rows.iter().map(idx_flow_day).collect();
foreign_flow("TEST.JK", FlowSource::Idx, days, false)
}
#[test]
fn test_foreign_flow_sums_sessions_and_estimates_value() {
let flow =
idx_flow(&[row(15, 1100, 600, Some(200)), row(16, 950, 100, Some(300))]).unwrap();
let first = &flow.days[0];
assert_eq!(
(first.foreign_net, first.change_bps),
(Some(400), Some(1000))
);
// 400 shares at 1,100 (value over volume).
assert_eq!(first.foreign_net_value, Some(440_000));
assert_eq!(
(first.foreign_buy_value, first.foreign_sell_value),
(Some(660_000), Some(220_000))
);
assert_eq!(first.foreign_share_bps, Some(4000));
// 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),
(Some(200), Some(220_000))
);
// (700 + 500) / (2 * 2,000).
assert_eq!(flow.foreign_share_bps, Some(3000));
assert_eq!((flow.from.day(), flow.to.day()), (15, 16));
}
#[test]
fn test_foreign_flow_never_counts_a_missing_figure_as_zero() {
let flow = idx_flow(&[row(15, 1100, 600, Some(200)), row(16, 950, 100, None)]).unwrap();
assert_eq!(flow.days[1].foreign_net, None);
assert_eq!((flow.foreign_net, flow.foreign_share_bps), (None, None));
assert!(idx_flow(&[]).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 = idx_flow(&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_ipot_flow_day_reads_foreign_buying_and_selling_from_the_summaries() {
let day = NaiveDate::from_ymd_opt(2026, 10, 16).unwrap();
let summary = |investor, name: &str| {
let html = std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap();
let raw = crate::api::ipot::summary_from_html("BBCA", &html).unwrap();
let query = BrokerQuery {
from: day,
to: day,
investor,
board: BrokerBoard::All,
};
crate::broker::summary_from_raw("BBCA.JK", &query, raw)
};
let all = summary(BrokerInvestor::All, "ipot_brokersummary_bbca_20261016.html");
let foreign = summary(
BrokerInvestor::Foreign,
"ipot_brokersummary_bbca_20261016_foreign_all.html",
);
let flow = ipot_flow_day(day, &all, &foreign);
assert_eq!(flow.value, Some(6_600_000_000_000));
assert_eq!(flow.volume, Some(1_260_000_000));
assert_eq!(
(
flow.foreign_buy_value,
flow.foreign_sell_value,
flow.foreign_net_value
),
(
Some(1_700_000_000_000),
Some(4_000_000_000_000),
Some(-2_300_000_000_000)
)
);
// (1.7 T + 4.0 T) / (2 * 6.6 T).
assert_eq!(flow.foreign_share_bps, Some(4318));
assert_eq!((flow.close, flow.foreign_net), (None, None));
}
#[test]
fn test_check_range_bounds() {
let day = |d| NaiveDate::from_ymd_opt(2026, 10, d).unwrap();
let today = day(18);
assert!(check_range(day(1), day(16), today).is_ok());
assert!(check_range(day(16), day(12), today).is_err());
assert!(check_range(day(16), day(19), today).is_err());
let long_ago = NaiveDate::from_ymd_opt(2026, 7, 1).unwrap();
assert!(check_range(long_ago, day(16), today).is_err());
}
}

View file

@ -0,0 +1,184 @@
//! Foreign flow types: what foreign investors bought and sold of a stock,
//! session by session (end of day), from IPOT or IDX's trading summary.
use std::str::FromStr;
use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use crate::error::IdxError;
/// Sessions shown when a frontend gives neither days nor dates.
pub const DEFAULT_SESSIONS: usize = 10;
/// Where a foreign flow comes from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "snake_case")]
pub enum FlowSource {
/// Indo Premier's broker summary: foreign buy, sell and net value across
/// all boards, two plain requests per session. Figures of a million and
/// more are rounded to one decimal.
#[default]
Ipot,
/// IDX's own trading summary: exact foreign share counts, close and the
/// non-regular share, regular board. Needs curl-impersonate and a
/// network IDX's Cloudflare lets through.
Idx,
}
impl FlowSource {
pub const ALL: [Self; 2] = [Self::Ipot, Self::Idx];
pub fn as_str(self) -> &'static str {
match self {
Self::Ipot => "ipot",
Self::Idx => "idx",
}
}
}
impl FromStr for FlowSource {
type Err = IdxError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"ipot" => Ok(Self::Ipot),
"idx" => Ok(Self::Idx),
_ => Err(IdxError::InvalidInput(format!(
"invalid source '{value}'; use ipot or idx"
))),
}
}
}
/// One stock's trading summary for one session, as IDX publishes it.
/// 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`.
pub code: String,
pub date: NaiveDate,
pub previous: Option<i64>,
pub high: Option<i64>,
pub low: Option<i64>,
pub close: Option<i64>,
pub volume: Option<i64>,
/// Value traded on the regular board, whole IDR.
pub value: Option<i64>,
pub frequency: Option<i64>,
/// Shares bought by foreign investors.
pub foreign_buy: Option<i64>,
/// Shares sold by foreign investors.
pub foreign_sell: Option<i64>,
/// Shares traded outside the regular board (negotiated and cash).
pub non_regular_volume: Option<i64>,
/// Value traded outside the regular board, whole IDR.
pub non_regular_value: Option<i64>,
}
/// Which sessions a foreign flow covers.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FlowRange {
/// The latest `sessions` sessions with trades.
Latest { sessions: usize },
/// Every session in `from..=to` (WIB dates).
Dates { from: NaiveDate, to: NaiveDate },
}
impl FlowRange {
/// From a frontend's `days`, or `from` with an optional `to` (default
/// today) as `YYYY-MM-DD`; neither means the latest
/// [`DEFAULT_SESSIONS`].
pub fn from_args(
days: Option<usize>,
from: Option<&str>,
to: Option<&str>,
today: NaiveDate,
) -> Result<Self, IdxError> {
let parse = |name: &str, value: &str| {
NaiveDate::parse_from_str(value.trim(), "%Y-%m-%d").map_err(|_| {
IdxError::InvalidInput(format!("invalid {name} '{value}'; use YYYY-MM-DD"))
})
};
match (days, from, to) {
(days, None, None) => Ok(Self::Latest {
sessions: days.unwrap_or(DEFAULT_SESSIONS),
}),
(None, Some(from), to) => Ok(Self::Dates {
from: parse("from", from)?,
to: to.map(|to| parse("to", to)).transpose()?.unwrap_or(today),
}),
(None, None, Some(_)) => Err(IdxError::InvalidInput("to needs from".to_string())),
(Some(_), _, _) => Err(IdxError::InvalidInput(
"days cannot be combined with from or to".to_string(),
)),
}
}
}
/// One session. Which figures are present depends on the source: IPOT gives
/// values across all boards, IDX gives exact shares on the regular board (and
/// values estimated from them) with the close and the non-regular share.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForeignFlowDay {
pub date: NaiveDate,
/// Close, whole IDR (IDX only).
pub close: Option<i64>,
/// Close against the previous close, basis points (IDX only).
pub change_bps: Option<i64>,
/// Shares traded: the regular board (IDX), or all boards from lots
/// (IPOT).
pub volume: Option<i64>,
/// Value traded, whole IDR: the regular board (IDX) or all boards (IPOT).
pub value: Option<i64>,
/// Shares foreign investors bought (IDX only).
pub foreign_buy: Option<i64>,
/// Shares foreign investors sold (IDX only).
pub foreign_sell: Option<i64>,
/// `foreign_buy - foreign_sell`, shares (IDX only).
pub foreign_net: Option<i64>,
/// Value foreign investors bought, whole IDR: as published (IPOT), or
/// estimated at the session's average price (IDX).
pub foreign_buy_value: Option<i64>,
/// Value foreign investors sold, whole IDR: their buying less their net
/// (IPOT), or estimated at the session's average price (IDX).
pub foreign_sell_value: Option<i64>,
/// Foreign buy value minus sell value, whole IDR: as published (IPOT), or
/// net shares at the session's average price (IDX, an estimate).
pub foreign_net_value: Option<i64>,
/// Foreign buying and selling over twice the session's trading: foreign
/// investors' share of it, basis points.
pub foreign_share_bps: Option<i64>,
/// Value traded outside the regular board (negotiated and cash), basis
/// points of all value traded (IDX only).
pub non_regular_bps: Option<i64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ForeignFlow {
/// Resolved ticker, e.g. `BBCA.JK`.
pub symbol: String,
pub source: FlowSource,
/// First and last session covered.
pub from: NaiveDate,
pub to: NaiveDate,
/// Sessions the stock traded, oldest first.
pub days: Vec<ForeignFlowDay>,
/// Net shares over the sessions (IDX only); `None` when a session lacks
/// a figure.
pub foreign_net: Option<i64>,
/// Foreign buy value over the sessions, whole IDR.
pub foreign_buy_value: Option<i64>,
/// Foreign sell value over the sessions, whole IDR.
pub foreign_sell_value: Option<i64>,
/// Foreign net value over the sessions, whole IDR.
pub foreign_net_value: Option<i64>,
/// Foreign investors' share of trading over the sessions, basis points.
pub foreign_share_bps: Option<i64>,
/// The source rounded figures of a million and more to one decimal.
pub rounded: bool,
}

View file

@ -4,12 +4,14 @@
pub mod analysis; pub mod analysis;
pub mod api; pub mod api;
pub mod broker;
pub mod cache; pub mod cache;
mod client; mod client;
pub mod config; pub mod config;
pub mod curl_impersonate; pub mod curl_impersonate;
pub mod error; pub mod error;
pub mod financial; pub mod financial;
pub mod flow;
#[cfg(feature = "ownership")] #[cfg(feature = "ownership")]
pub mod investor; pub mod investor;
pub mod market; pub mod market;

View file

@ -4,8 +4,10 @@ use std::cmp::Ordering;
use std::fmt; use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday};
use crate::api::types::{Interval, Ohlc, Period, Quote}; use crate::api::types::{Interval, Ohlc, Period, Quote};
use crate::api::{history_provider, history_source}; use crate::api::{history_provider, history_source, idx_offset};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch}; use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::config::{HistoryProviderKind, ProviderKind}; use crate::config::{HistoryProviderKind, ProviderKind};
use crate::error::IdxError; use crate::error::IdxError;
@ -32,10 +34,11 @@ impl Market<'_> {
) -> Result<Vec<Fetched<Quote>>, IdxError> { ) -> Result<Vec<Fetched<Quote>>, IdxError> {
let idx = self.idx; let idx = self.idx;
let bucket = idx.bucket("quote"); let bucket = idx.bucket("quote");
let session = latest_session(Utc::now());
let mut quotes = Vec::with_capacity(symbols.len()); let mut quotes = Vec::with_capacity(symbols.len());
for symbol in symbols { for symbol in symbols {
let resolved = idx.resolve(symbol.as_ref())?; let resolved = idx.resolve(symbol.as_ref())?;
quotes.push(cached_fetch( let mut quote = cached_fetch(
&idx.cache, &idx.cache,
CacheSpec { CacheSpec {
bucket: &bucket, bucket: &bucket,
@ -47,7 +50,12 @@ impl Market<'_> {
policy, policy,
|| idx.provider.market().quote(&resolved), || idx.provider.market().quote(&resolved),
Ok, Ok,
)?); )?;
// The session rule is IDX's; other exchanges keep other hours.
if resolved.ends_with(".JK") {
mark_behind_session(&mut quote.data, session);
}
quotes.push(quote);
} }
Ok(quotes) Ok(quotes)
} }
@ -131,10 +139,40 @@ impl Market<'_> {
)?; )?;
sort_screener_quotes(&mut fetched.data, filter); sort_screener_quotes(&mut fetched.data, filter);
fetched.data.truncate(limit); fetched.data.truncate(limit);
if region == ScreenRegion::Id {
let session = latest_session(Utc::now());
for quote in &mut fetched.data {
mark_behind_session(quote, session);
}
}
Ok(fetched) Ok(fetched)
} }
} }
/// When IDX's post-trading session ends, in WIB; MSN stamps a day's closing
/// quote with this time.
const SESSION_END: NaiveTime = NaiveTime::from_hms_opt(16, 15, 0).expect("valid time");
/// The latest weekday whose IDX session has ended by `now`. Exchange holidays
/// are not known offline, so on a weekday holiday this is the holiday itself.
pub(crate) fn latest_session(now: DateTime<Utc>) -> NaiveDate {
let now = now.with_timezone(&idx_offset());
let mut day = now.date_naive();
if now.time() < SESSION_END {
day -= Duration::days(1);
}
while matches!(day.weekday(), Weekday::Sat | Weekday::Sun) {
day -= Duration::days(1);
}
day
}
fn mark_behind_session(quote: &mut Quote, session: NaiveDate) {
quote.behind_session = quote
.as_of
.is_some_and(|as_of| as_of.with_timezone(&idx_offset()).date_naive() < session);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScreenQuery { pub struct ScreenQuery {
pub filter: ScreenFilter, pub filter: ScreenFilter,
@ -320,7 +358,9 @@ fn sort_screener_quotes(quotes: &mut [Quote], filter: ScreenFilter) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ScreenFilter, ScreenRegion, sort_screener_quotes}; use super::{
ScreenFilter, ScreenRegion, latest_session, mark_behind_session, sort_screener_quotes,
};
use crate::api::types::Quote; use crate::api::types::Quote;
use crate::error::IdxError; use crate::error::IdxError;
@ -339,6 +379,53 @@ mod tests {
prev_close: None, prev_close: None,
avg_volume: None, avg_volume: None,
as_of: None, as_of: None,
behind_session: false,
}
}
fn wib(raw: &str) -> chrono::DateTime<chrono::Utc> {
chrono::DateTime::parse_from_rfc3339(&format!("{raw}+07:00"))
.unwrap()
.to_utc()
}
fn day(raw: &str) -> chrono::NaiveDate {
chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d").unwrap()
}
#[test]
fn latest_session_is_the_last_weekday_past_the_post_trading_close() {
// 2026-10-16 is a Friday.
for (now, session) in [
("2026-10-16T16:53:00", "2026-10-16"),
("2026-10-16T16:15:00", "2026-10-16"),
("2026-10-16T16:14:59", "2026-10-15"),
("2026-10-16T00:30:00", "2026-10-15"),
("2026-10-17T12:00:00", "2026-10-16"),
("2026-10-18T23:59:59", "2026-10-16"),
("2026-10-19T10:00:00", "2026-10-16"),
("2026-10-19T16:15:00", "2026-10-19"),
] {
assert_eq!(latest_session(wib(now)), day(session), "now {now}");
}
}
#[test]
fn quote_from_before_the_latest_session_is_behind_it() {
// Issue #50: at 16:53 WIB on the 16th, MSN still served the 15th.
let session = latest_session(wib("2026-10-16T16:53:00"));
for (as_of, behind) in [
(Some("2026-10-15T16:15:00+07:00"), true),
(Some("2026-10-16T16:15:00+07:00"), false),
(Some("2026-10-16T09:15:00Z"), false),
(Some("2026-10-15T17:30:00Z"), false),
(Some("2025-02-17T16:14:58+07:00"), true),
(None, false),
] {
let mut quote = sample_quote("BBCA.JK", None);
quote.as_of = as_of.map(|t| chrono::DateTime::parse_from_rfc3339(t).unwrap());
mark_behind_session(&mut quote, session);
assert_eq!(quote.behind_session, behind, "as_of {as_of:?}");
} }
} }

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":4,"recordsFiltered":4,"data":[{"No":98,"IDStockSummary":4111663,"Date":"2026-10-12T00:00:00","StockCode":"BBCA","StockName":"Bank Central Asia Tbk.","Remarks":"--SO1UQNCNU100G111------------","Previous":6050.0,"OpenPrice":6050.0,"FirstTrade":6050.0,"High":6100.0,"Low":5950.0,"Close":5950.0,"Change":-100.0,"Volume":97965100.0,"Value":586491892500.0,"Frequency":25910.0,"IndexIndividual":16988.4,"Offer":5950.0,"OfferVolume":2201900.0,"Bid":5925.0,"BidVolume":3571200.0,"ListedShares":122042299500.0,"TradebleShares":122042299500.0,"WeightForIndex":39458951953.0,"ForeignSell":60883100.0,"ForeignBuy":51499200.0,"DelistingDate":"","NonRegularVolume":110246.0,"NonRegularValue":798569400.0,"NonRegularFrequency":10.0,"persen":null,"percentage":null},{"No":104,"IDStockSummary":4111669,"Date":"2026-10-12T00:00:00","StockCode":"BBRI","StockName":"Bank Rakyat Indonesia (Persero) Tbk.","Remarks":"--MO1UQSPNU100G111------------","Previous":3100.0,"OpenPrice":3100.0,"FirstTrade":3100.0,"High":3140.0,"Low":3070.0,"Close":3110.0,"Change":10.0,"Volume":129810600.0,"Value":402372587000.0,"Frequency":22542.0,"IndexIndividual":3647.6,"Offer":3110.0,"OfferVolume":668400.0,"Bid":3100.0,"BidVolume":247900.0,"ListedShares":150043411587.0,"TradebleShares":150043411587.0,"WeightForIndex":63813462948.0,"ForeignSell":55331400.0,"ForeignBuy":81277900.0,"DelistingDate":"","NonRegularVolume":29917536.0,"NonRegularValue":92714443560.0,"NonRegularFrequency":6.0,"persen":null,"percentage":null},{"No":350,"IDStockSummary":4111915,"Date":"2026-10-12T00:00:00","StockCode":"GOTO","StockName":"GoTo Gojek Tokopedia Tbk.","Remarks":"--UO2H40544000I111----------N-","Previous":30.0,"OpenPrice":29.0,"FirstTrade":29.0,"High":30.0,"Low":26.0,"Close":27.0,"Change":-3.0,"Volume":28057826200.0,"Value":773057901000.0,"Frequency":49329.0,"IndexIndividual":8.0,"Offer":27.0,"OfferVolume":1907171500.0,"Bid":26.0,"BidVolume":4019082600.0,"ListedShares":1140573267220.0,"TradebleShares":1140573267220.0,"WeightForIndex":799998089628.0,"ForeignSell":5861571800.0,"ForeignBuy":10748736800.0,"DelistingDate":"","NonRegularVolume":700900700.0,"NonRegularValue":20301801400.0,"NonRegularFrequency":2.0,"persen":null,"percentage":null},{"No":877,"IDStockSummary":4112442,"Date":"2026-10-12T00:00:00","StockCode":"TLKM","StockName":"Telkom Indonesia (Persero) Tbk.","Remarks":"--MO1XDSWW0000J312------------","Previous":2320.0,"OpenPrice":2330.0,"FirstTrade":2330.0,"High":2340.0,"Low":2260.0,"Close":2280.0,"Change":-40.0,"Volume":149200000.0,"Value":342019043000.0,"Frequency":14057.0,"IndexIndividual":1198.2,"Offer":2290.0,"OfferVolume":973500.0,"Bid":2280.0,"BidVolume":194400.0,"ListedShares":99062216600.0,"TradebleShares":99062216600.0,"WeightForIndex":39902260846.0,"ForeignSell":118523800.0,"ForeignBuy":106560700.0,"DelistingDate":"","NonRegularVolume":15000273.0,"NonRegularValue":34245625670.0,"NonRegularFrequency":5.0,"persen":null,"percentage":null}]}

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":4,"recordsFiltered":4,"data":[{"No":98,"IDStockSummary":4112626,"Date":"2026-10-13T00:00:00","StockCode":"BBCA","StockName":"Bank Central Asia Tbk.","Remarks":"--SO1UQNCNU100G111------------","Previous":5950.0,"OpenPrice":5925.0,"FirstTrade":5950.0,"High":5950.0,"Low":5850.0,"Close":5950.0,"Change":0.0,"Volume":122716900.0,"Value":726575115000.0,"Frequency":23340.0,"IndexIndividual":16988.4,"Offer":5950.0,"OfferVolume":994700.0,"Bid":5925.0,"BidVolume":177900.0,"ListedShares":122042299500.0,"TradebleShares":122042299500.0,"WeightForIndex":39458951953.0,"ForeignSell":83738900.0,"ForeignBuy":88772100.0,"DelistingDate":"","NonRegularVolume":1988532.0,"NonRegularValue":11743985775.0,"NonRegularFrequency":5.0,"persen":null,"percentage":null},{"No":104,"IDStockSummary":4112632,"Date":"2026-10-13T00:00:00","StockCode":"BBRI","StockName":"Bank Rakyat Indonesia (Persero) Tbk.","Remarks":"--MO1UQSPNU100G111------------","Previous":3110.0,"OpenPrice":3100.0,"FirstTrade":3090.0,"High":3100.0,"Low":3030.0,"Close":3080.0,"Change":-30.0,"Volume":102010600.0,"Value":312155170000.0,"Frequency":22096.0,"IndexIndividual":3612.4,"Offer":3080.0,"OfferVolume":2006200.0,"Bid":3070.0,"BidVolume":1886500.0,"ListedShares":150043411587.0,"TradebleShares":150043411587.0,"WeightForIndex":63813462948.0,"ForeignSell":58087200.0,"ForeignBuy":36737800.0,"DelistingDate":"","NonRegularVolume":38643164.0,"NonRegularValue":118209441300.0,"NonRegularFrequency":4.0,"persen":null,"percentage":null},{"No":350,"IDStockSummary":4112878,"Date":"2026-10-13T00:00:00","StockCode":"GOTO","StockName":"GoTo Gojek Tokopedia Tbk.","Remarks":"--UO2H40544000I111----------N-","Previous":27.0,"OpenPrice":27.0,"FirstTrade":26.0,"High":27.0,"Low":25.0,"Close":27.0,"Change":0.0,"Volume":11323075500.0,"Value":298601974900.0,"Frequency":21340.0,"IndexIndividual":8.0,"Offer":27.0,"OfferVolume":1668009300.0,"Bid":26.0,"BidVolume":2243966200.0,"ListedShares":1140573267220.0,"TradebleShares":1140573267220.0,"WeightForIndex":799998089628.0,"ForeignSell":5847077100.0,"ForeignBuy":3865517000.0,"DelistingDate":"","NonRegularVolume":208738.0,"NonRegularValue":1530356.0,"NonRegularFrequency":3.0,"persen":null,"percentage":null},{"No":877,"IDStockSummary":4113405,"Date":"2026-10-13T00:00:00","StockCode":"TLKM","StockName":"Telkom Indonesia (Persero) Tbk.","Remarks":"--MO1XDSWW0000J312------------","Previous":2280.0,"OpenPrice":2280.0,"FirstTrade":2270.0,"High":2280.0,"Low":2210.0,"Close":2240.0,"Change":-40.0,"Volume":109424100.0,"Value":244244887000.0,"Frequency":15860.0,"IndexIndividual":1177.2,"Offer":2250.0,"OfferVolume":408200.0,"Bid":2240.0,"BidVolume":24800.0,"ListedShares":99062216600.0,"TradebleShares":99062216600.0,"WeightForIndex":39902260846.0,"ForeignSell":94827700.0,"ForeignBuy":61323200.0,"DelistingDate":"","NonRegularVolume":308339.0,"NonRegularValue":922008120.0,"NonRegularFrequency":4.0,"persen":null,"percentage":null}]}

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":4,"recordsFiltered":4,"data":[{"No":98,"IDStockSummary":4113589,"Date":"2026-10-14T00:00:00","StockCode":"BBCA","StockName":"Bank Central Asia Tbk.","Remarks":"--SO1UQNCNU100G111------------","Previous":5950.0,"OpenPrice":6000.0,"FirstTrade":6000.0,"High":6125.0,"Low":5950.0,"Close":6125.0,"Change":175.0,"Volume":136303100.0,"Value":828862772500.0,"Frequency":18178.0,"IndexIndividual":17488.0,"Offer":6125.0,"OfferVolume":1376900.0,"Bid":6100.0,"BidVolume":41700.0,"ListedShares":122042299500.0,"TradebleShares":122042299500.0,"WeightForIndex":39458951953.0,"ForeignSell":84298000.0,"ForeignBuy":119876900.0,"DelistingDate":"","NonRegularVolume":43235191.0,"NonRegularValue":263446538075.0,"NonRegularFrequency":15.0,"persen":null,"percentage":null},{"No":104,"IDStockSummary":4113595,"Date":"2026-10-14T00:00:00","StockCode":"BBRI","StockName":"Bank Rakyat Indonesia (Persero) Tbk.","Remarks":"--MO1UQSPNU100G111------------","Previous":3080.0,"OpenPrice":3090.0,"FirstTrade":3090.0,"High":3140.0,"Low":3070.0,"Close":3090.0,"Change":10.0,"Volume":109152100.0,"Value":338481482000.0,"Frequency":18138.0,"IndexIndividual":3624.1,"Offer":3090.0,"OfferVolume":4530000.0,"Bid":3080.0,"BidVolume":3641200.0,"ListedShares":150043411587.0,"TradebleShares":150043411587.0,"WeightForIndex":63813462948.0,"ForeignSell":68198900.0,"ForeignBuy":49831700.0,"DelistingDate":"","NonRegularVolume":55577571.0,"NonRegularValue":172327653286.0,"NonRegularFrequency":13.0,"persen":null,"percentage":null},{"No":350,"IDStockSummary":4113841,"Date":"2026-10-14T00:00:00","StockCode":"GOTO","StockName":"GoTo Gojek Tokopedia Tbk.","Remarks":"--UO2H40544000I111----------N-","Previous":27.0,"OpenPrice":27.0,"FirstTrade":27.0,"High":28.0,"Low":26.0,"Close":28.0,"Change":1.0,"Volume":9369136400.0,"Value":256667124200.0,"Frequency":19390.0,"IndexIndividual":8.3,"Offer":28.0,"OfferVolume":2173468100.0,"Bid":27.0,"BidVolume":1340613300.0,"ListedShares":1140573267220.0,"TradebleShares":1140573267220.0,"WeightForIndex":799998089628.0,"ForeignSell":3475737500.0,"ForeignBuy":3444546600.0,"DelistingDate":"","NonRegularVolume":700006960.0,"NonRegularValue":20300235380.0,"NonRegularFrequency":4.0,"persen":null,"percentage":null},{"No":877,"IDStockSummary":4114368,"Date":"2026-10-14T00:00:00","StockCode":"TLKM","StockName":"Telkom Indonesia (Persero) Tbk.","Remarks":"--MO1XDSWW0000J312------------","Previous":2240.0,"OpenPrice":2260.0,"FirstTrade":2260.0,"High":2310.0,"Low":2250.0,"Close":2250.0,"Change":10.0,"Volume":87159900.0,"Value":197725738000.0,"Frequency":13661.0,"IndexIndividual":1182.4,"Offer":2260.0,"OfferVolume":1297300.0,"Bid":2250.0,"BidVolume":331200.0,"ListedShares":99062216600.0,"TradebleShares":99062216600.0,"WeightForIndex":39902260846.0,"ForeignSell":60811200.0,"ForeignBuy":63030200.0,"DelistingDate":"","NonRegularVolume":9457362.0,"NonRegularValue":21449294102.0,"NonRegularFrequency":6.0,"persen":null,"percentage":null}]}

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":4,"recordsFiltered":4,"data":[{"No":98,"IDStockSummary":4114552,"Date":"2026-10-15T00:00:00","StockCode":"BBCA","StockName":"Bank Central Asia Tbk.","Remarks":"--SO1UQNCNU100G111------------","Previous":6125.0,"OpenPrice":6075.0,"FirstTrade":6075.0,"High":6125.0,"Low":6025.0,"Close":6075.0,"Change":-50.0,"Volume":134763200.0,"Value":816554920000.0,"Frequency":17286.0,"IndexIndividual":17345.2,"Offer":6075.0,"OfferVolume":135400.0,"Bid":6050.0,"BidVolume":47300.0,"ListedShares":122042299500.0,"TradebleShares":122042299500.0,"WeightForIndex":39458951953.0,"ForeignSell":73638900.0,"ForeignBuy":120850000.0,"DelistingDate":"","NonRegularVolume":15720537.0,"NonRegularValue":95408071050.0,"NonRegularFrequency":38.0,"persen":null,"percentage":null},{"No":104,"IDStockSummary":4114558,"Date":"2026-10-15T00:00:00","StockCode":"BBRI","StockName":"Bank Rakyat Indonesia (Persero) Tbk.","Remarks":"--MO1UQSPNU100G111------------","Previous":3090.0,"OpenPrice":3080.0,"FirstTrade":3080.0,"High":3110.0,"Low":3060.0,"Close":3100.0,"Change":10.0,"Volume":107210100.0,"Value":330799545000.0,"Frequency":16751.0,"IndexIndividual":3635.9,"Offer":3100.0,"OfferVolume":175700.0,"Bid":3090.0,"BidVolume":71000.0,"ListedShares":150043411587.0,"TradebleShares":150043411587.0,"WeightForIndex":63813462948.0,"ForeignSell":47516000.0,"ForeignBuy":72896800.0,"DelistingDate":"","NonRegularVolume":6266786.0,"NonRegularValue":19574409750.0,"NonRegularFrequency":10.0,"persen":null,"percentage":null},{"No":350,"IDStockSummary":4114804,"Date":"2026-10-15T00:00:00","StockCode":"GOTO","StockName":"GoTo Gojek Tokopedia Tbk.","Remarks":"--UO2H40544000I111----------N-","Previous":28.0,"OpenPrice":27.0,"FirstTrade":27.0,"High":27.0,"Low":26.0,"Close":27.0,"Change":-1.0,"Volume":5443007700.0,"Value":146026601200.0,"Frequency":15162.0,"IndexIndividual":8.0,"Offer":28.0,"OfferVolume":1445696200.0,"Bid":27.0,"BidVolume":374663700.0,"ListedShares":1140573267220.0,"TradebleShares":1140573267220.0,"WeightForIndex":799998089628.0,"ForeignSell":1724301600.0,"ForeignBuy":882307700.0,"DelistingDate":"","NonRegularVolume":43.0,"NonRegularValue":1204.0,"NonRegularFrequency":1.0,"persen":null,"percentage":null},{"No":877,"IDStockSummary":4115331,"Date":"2026-10-15T00:00:00","StockCode":"TLKM","StockName":"Telkom Indonesia (Persero) Tbk.","Remarks":"--MO1XDSWW0000J312------------","Previous":2250.0,"OpenPrice":2250.0,"FirstTrade":2260.0,"High":2260.0,"Low":2170.0,"Close":2230.0,"Change":-20.0,"Volume":150485300.0,"Value":333287991000.0,"Frequency":21026.0,"IndexIndividual":1171.9,"Offer":2230.0,"OfferVolume":1787000.0,"Bid":2220.0,"BidVolume":1084100.0,"ListedShares":99062216600.0,"TradebleShares":99062216600.0,"WeightForIndex":39902260846.0,"ForeignSell":105521800.0,"ForeignBuy":97385100.0,"DelistingDate":"","NonRegularVolume":1177287.0,"NonRegularValue":2606515710.0,"NonRegularFrequency":3.0,"persen":null,"percentage":null}]}

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":4,"recordsFiltered":4,"data":[{"No":98,"IDStockSummary":4115515,"Date":"2026-10-16T00:00:00","StockCode":"BBCA","StockName":"Bank Central Asia Tbk.","Remarks":"--SO1UQNCNU100G111------------","Previous":6075.0,"OpenPrice":5175.0,"FirstTrade":5225.0,"High":5825.0,"Low":5175.0,"Close":5175.0,"Change":-900.0,"Volume":1241098200.0,"Value":6474934315000.0,"Frequency":185072.0,"IndexIndividual":14775.6,"Offer":5175.0,"OfferVolume":183825200.0,"Bid":0.0,"BidVolume":0.0,"ListedShares":122042299500.0,"TradebleShares":122042299500.0,"WeightForIndex":39458951953.0,"ForeignSell":757227900.0,"ForeignBuy":309392800.0,"DelistingDate":"","NonRegularVolume":16501628.0,"NonRegularValue":100183151475.0,"NonRegularFrequency":15.0,"persen":null,"percentage":null},{"No":104,"IDStockSummary":4115521,"Date":"2026-10-16T00:00:00","StockCode":"BBRI","StockName":"Bank Rakyat Indonesia (Persero) Tbk.","Remarks":"--MO1UQSPNU100G111------------","Previous":3100.0,"OpenPrice":2640.0,"FirstTrade":2730.0,"High":2890.0,"Low":2640.0,"Close":2640.0,"Change":-460.0,"Volume":1193916400.0,"Value":3234746482000.0,"Frequency":219483.0,"IndexIndividual":3096.3,"Offer":2640.0,"OfferVolume":20839200.0,"Bid":0.0,"BidVolume":0.0,"ListedShares":150043411587.0,"TradebleShares":150043411587.0,"WeightForIndex":63813462948.0,"ForeignSell":687811000.0,"ForeignBuy":262656800.0,"DelistingDate":"","NonRegularVolume":23371386.0,"NonRegularValue":62017203290.0,"NonRegularFrequency":12.0,"persen":null,"percentage":null},{"No":350,"IDStockSummary":4115767,"Date":"2026-10-16T00:00:00","StockCode":"GOTO","StockName":"GoTo Gojek Tokopedia Tbk.","Remarks":"--UO2H40544000I111----------N-","Previous":27.0,"OpenPrice":23.0,"FirstTrade":23.0,"High":27.0,"Low":23.0,"Close":25.0,"Change":-2.0,"Volume":42657972100.0,"Value":1026997626200.0,"Frequency":61645.0,"IndexIndividual":7.4,"Offer":25.0,"OfferVolume":186996300.0,"Bid":24.0,"BidVolume":362311000.0,"ListedShares":1140573267220.0,"TradebleShares":1140573267220.0,"WeightForIndex":799998089628.0,"ForeignSell":10011994300.0,"ForeignBuy":21910169500.0,"DelistingDate":"","NonRegularVolume":49.0,"NonRegularValue":1127.0,"NonRegularFrequency":1.0,"persen":null,"percentage":null},{"No":877,"IDStockSummary":4116294,"Date":"2026-10-16T00:00:00","StockCode":"TLKM","StockName":"Telkom Indonesia (Persero) Tbk.","Remarks":"--MO1XDSWW0000J312------------","Previous":2230.0,"OpenPrice":1900.0,"FirstTrade":1970.0,"High":1985.0,"Low":1900.0,"Close":1900.0,"Change":-330.0,"Volume":963614200.0,"Value":1855341292500.0,"Frequency":68042.0,"IndexIndividual":998.5,"Offer":1900.0,"OfferVolume":860400.0,"Bid":0.0,"BidVolume":0.0,"ListedShares":99062216600.0,"TradebleShares":99062216600.0,"WeightForIndex":39902260846.0,"ForeignSell":727289800.0,"ForeignBuy":679004400.0,"DelistingDate":"","NonRegularVolume":165000429.0,"NonRegularValue":323596857244.0,"NonRegularFrequency":10.0,"persen":null,"percentage":null}]}

View file

@ -0,0 +1 @@
{"draw":0,"recordsTotal":0,"recordsFiltered":0,"data":[]}

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/12/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/12/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" >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">ZP</span></td>
<td align="right"><span class="text-up">319,263 </span></td>
<td align="right"><span class="text-up">191.3 B</span></td>
<td align="right"><span class="text-up">5,993 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">248,149 </span></td>
<td align="right"><span class="text-down">148.6 B</span></td>
<td align="right"><span class="text-down">5,988 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">91,178 </span></td>
<td align="right"><span class="text-up">54.7 B</span></td>
<td align="right"><span class="text-up">5,998 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">169,689 </span></td>
<td align="right"><span class="text-down">101.8 B</span></td>
<td align="right"><span class="text-down">6,000 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">82,364 </span></td>
<td align="right"><span class="text-up">49.3 B</span></td>
<td align="right"><span class="text-up">5,991 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">142,270 </span></td>
<td align="right"><span class="text-down">85.1 B</span></td>
<td align="right"><span class="text-down">5,980 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">70,119 </span></td>
<td align="right"><span class="text-up">41.9 B</span></td>
<td align="right"><span class="text-up">5,973 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">109,896 </span></td>
<td align="right"><span class="text-down">65.7 B</span></td>
<td align="right"><span class="text-down">5,978 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">66,085 </span></td>
<td align="right"><span class="text-up">39.5 B</span></td>
<td align="right"><span class="text-up">5,982 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">51,291 </span></td>
<td align="right"><span class="text-down">30.7 B</span></td>
<td align="right"><span class="text-down">5,977 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-up">46,535 </span></td>
<td align="right"><span class="text-up">27.9 B</span></td>
<td align="right"><span class="text-up">5,989 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">35,962 </span></td>
<td align="right"><span class="text-down">21.5 B</span></td>
<td align="right"><span class="text-down">5,988 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">31,891 </span></td>
<td align="right"><span class="text-up">19.1 B</span></td>
<td align="right"><span class="text-up">5,983 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">33,257 </span></td>
<td align="right"><span class="text-down">19.9 B</span></td>
<td align="right"><span class="text-down">5,987 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XC</span></td>
<td align="right"><span class="text-up">31,403 </span></td>
<td align="right"><span class="text-up">18.8 B</span></td>
<td align="right"><span class="text-up">5,983 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-local">CP</span></td>
<td align="right"><span class="text-down">29,928 </span></td>
<td align="right"><span class="text-down">17.8 B</span></td>
<td align="right"><span class="text-down">5,962 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">24,560 </span></td>
<td align="right"><span class="text-up">14.7 B</span></td>
<td align="right"><span class="text-up">5,967 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">PD</span></td>
<td align="right"><span class="text-down">29,063 </span></td>
<td align="right"><span class="text-down">17.5 B</span></td>
<td align="right"><span class="text-down">6,023 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">20,056 </span></td>
<td align="right"><span class="text-up">12.0 B</span></td>
<td align="right"><span class="text-up">5,970 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-down">23,464 </span></td>
<td align="right"><span class="text-down">14.1 B</span></td>
<td align="right"><span class="text-down">6,002 </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 : 587.1 B</span>
<span class="fsize12 ml5 mr5">F. NVal : -55.9 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 980,415 </span>
<span class="fsize12 ml5 mr5">Avg : 5,988 </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/12/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" >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.7 M</span></td>
<td align="right"><span class="text-up">963.0 B</span></td>
<td align="right"><span class="text-up">5,573 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">5.1 M</span></td>
<td align="right"><span class="text-down">2.7 T</span></td>
<td align="right"><span class="text-down">5,336 </span></td>
</tr>
<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">842.0 B</span></td>
<td align="right"><span class="text-up">5,192 </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.8 M</span></td>
<td align="right"><span class="text-down">928.5 B</span></td>
<td align="right"><span class="text-down">5,295 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">1.6 M</span></td>
<td align="right"><span class="text-up">900.9 B</span></td>
<td align="right"><span class="text-up">5,780 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">1.6 M</span></td>
<td align="right"><span class="text-down">942.1 B</span></td>
<td align="right"><span class="text-down">5,756 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">1.4 M</span></td>
<td align="right"><span class="text-up">781.7 B</span></td>
<td align="right"><span class="text-up">5,626 </span></td>
<td align="center">4</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">800.5 B</span></td>
<td align="right"><span class="text-down">5,239 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">1.4 M</span></td>
<td align="right"><span class="text-up">737.9 B</span></td>
<td align="right"><span class="text-up">5,408 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">1.4 M</span></td>
<td align="right"><span class="text-down">786.5 B</span></td>
<td align="right"><span class="text-down">5,785 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">1.3 M</span></td>
<td align="right"><span class="text-up">687.2 B</span></td>
<td align="right"><span class="text-up">5,313 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">852,704 </span></td>
<td align="right"><span class="text-down">457.7 B</span></td>
<td align="right"><span class="text-down">5,368 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">1.1 M</span></td>
<td align="right"><span class="text-up">643.9 B</span></td>
<td align="right"><span class="text-up">5,771 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">830,940 </span></td>
<td align="right"><span class="text-down">466.7 B</span></td>
<td align="right"><span class="text-down">5,617 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">973,921 </span></td>
<td align="right"><span class="text-up">519.4 B</span></td>
<td align="right"><span class="text-up">5,333 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">709,658 </span></td>
<td align="right"><span class="text-down">393.3 B</span></td>
<td align="right"><span class="text-down">5,542 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">919,655 </span></td>
<td align="right"><span class="text-up">487.1 B</span></td>
<td align="right"><span class="text-up">5,296 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">621,425 </span></td>
<td align="right"><span class="text-down">363.3 B</span></td>
<td align="right"><span class="text-down">5,847 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">PD</span></td>
<td align="right"><span class="text-up">646,162 </span></td>
<td align="right"><span class="text-up">341.2 B</span></td>
<td align="right"><span class="text-up">5,280 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">441,903 </span></td>
<td align="right"><span class="text-down">242.6 B</span></td>
<td align="right"><span class="text-down">5,490 </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 : 9.9 T</span>
<span class="fsize12 ml5 mr5">F. NVal : -4.1 T</span>
<span class="fsize12 ml5 mr5">T.Lot : 18.1 M</span>
<span class="fsize12 ml5 mr5">Avg : 5,471 </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/12/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/12/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">ZP</span></td>
<td align="right"><span class="text-up">316,144 </span></td>
<td align="right"><span class="text-up">189.5 B</span></td>
<td align="right"><span class="text-up">5,993 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">201,030 </span></td>
<td align="right"><span class="text-down">120.4 B</span></td>
<td align="right"><span class="text-down">5,992 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">82,364 </span></td>
<td align="right"><span class="text-up">49.3 B</span></td>
<td align="right"><span class="text-up">5,991 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">137,297 </span></td>
<td align="right"><span class="text-down">82.1 B</span></td>
<td align="right"><span class="text-down">5,981 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-up">25,000 </span></td>
<td align="right"><span class="text-up">15.0 B</span></td>
<td align="right"><span class="text-up">5,989 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">123,872 </span></td>
<td align="right"><span class="text-down">74.4 B</span></td>
<td align="right"><span class="text-down">6,007 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">24,560 </span></td>
<td align="right"><span class="text-up">14.7 B</span></td>
<td align="right"><span class="text-up">5,967 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">89,396 </span></td>
<td align="right"><span class="text-down">53.5 B</span></td>
<td align="right"><span class="text-down">5,980 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">20,056 </span></td>
<td align="right"><span class="text-up">12.0 B</span></td>
<td align="right"><span class="text-up">5,970 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">25,685 </span></td>
<td align="right"><span class="text-down">15.4 B</span></td>
<td align="right"><span class="text-down">5,985 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">19,945 </span></td>
<td align="right"><span class="text-up">12.1 B</span></td>
<td align="right"><span class="text-up">6,069 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-down">20,942 </span></td>
<td align="right"><span class="text-down">12.5 B</span></td>
<td align="right"><span class="text-down">5,950 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-up">8,553 </span></td>
<td align="right"><span class="text-up">5.2 B</span></td>
<td align="right"><span class="text-up">6,075 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">7,476 </span></td>
<td align="right"><span class="text-down">4.5 B</span></td>
<td align="right"><span class="text-down">5,989 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BQ</span></td>
<td align="right"><span class="text-up">5,000 </span></td>
<td align="right"><span class="text-up">3.0 B</span></td>
<td align="right"><span class="text-up">5,950 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-down">1,084 </span></td>
<td align="right"><span class="text-down">647.7 M</span></td>
<td align="right"><span class="text-down">5,975 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AG</span></td>
<td align="right"><span class="text-up">4,315 </span></td>
<td align="right"><span class="text-up">2.6 B</span></td>
<td align="right"><span class="text-up">5,951 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">782 </span></td>
<td align="right"><span class="text-down">465.3 M</span></td>
<td align="right"><span class="text-down">5,950 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">3,957 </span></td>
<td align="right"><span class="text-up">2.4 B</span></td>
<td align="right"><span class="text-up">5,979 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-foreign">AG</span></td>
<td align="right"><span class="text-down">696 </span></td>
<td align="right"><span class="text-down">417.3 M</span></td>
<td align="right"><span class="text-down">5,995 </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 : 308.7 B</span>
<span class="fsize12 ml5 mr5">F. NVal : -55.9 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 514,995 </span>
<span class="fsize12 ml5 mr5">Avg : 5,994 </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/13/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/13/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" >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">ZP</span></td>
<td align="right"><span class="text-up">353,553 </span></td>
<td align="right"><span class="text-up">209.7 B</span></td>
<td align="right"><span class="text-up">5,931 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">362,463 </span></td>
<td align="right"><span class="text-down">215.0 B</span></td>
<td align="right"><span class="text-down">5,933 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">123,885 </span></td>
<td align="right"><span class="text-up">73.3 B</span></td>
<td align="right"><span class="text-up">5,919 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">170,087 </span></td>
<td align="right"><span class="text-down">100.6 B</span></td>
<td align="right"><span class="text-down">5,914 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">119,389 </span></td>
<td align="right"><span class="text-up">70.6 B</span></td>
<td align="right"><span class="text-up">5,915 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">131,973 </span></td>
<td align="right"><span class="text-down">78.2 B</span></td>
<td align="right"><span class="text-down">5,922 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">TP</span></td>
<td align="right"><span class="text-up">114,823 </span></td>
<td align="right"><span class="text-up">68.1 B</span></td>
<td align="right"><span class="text-up">5,931 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">AG</span></td>
<td align="right"><span class="text-down">113,249 </span></td>
<td align="right"><span class="text-down">67.1 B</span></td>
<td align="right"><span class="text-down">5,923 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">90,525 </span></td>
<td align="right"><span class="text-up">53.7 B</span></td>
<td align="right"><span class="text-up">5,931 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">99,863 </span></td>
<td align="right"><span class="text-down">59.0 B</span></td>
<td align="right"><span class="text-down">5,904 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">84,572 </span></td>
<td align="right"><span class="text-up">50.1 B</span></td>
<td align="right"><span class="text-up">5,920 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">87,628 </span></td>
<td align="right"><span class="text-down">51.8 B</span></td>
<td align="right"><span class="text-down">5,915 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">54,296 </span></td>
<td align="right"><span class="text-up">32.2 B</span></td>
<td align="right"><span class="text-up">5,928 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">58,039 </span></td>
<td align="right"><span class="text-down">34.4 B</span></td>
<td align="right"><span class="text-down">5,926 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">44,062 </span></td>
<td align="right"><span class="text-up">26.0 B</span></td>
<td align="right"><span class="text-up">5,907 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">31,516 </span></td>
<td align="right"><span class="text-down">18.6 B</span></td>
<td align="right"><span class="text-down">5,905 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">38,109 </span></td>
<td align="right"><span class="text-up">22.5 B</span></td>
<td align="right"><span class="text-up">5,901 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-down">21,672 </span></td>
<td align="right"><span class="text-down">12.8 B</span></td>
<td align="right"><span class="text-down">5,917 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">22,957 </span></td>
<td align="right"><span class="text-up">13.5 B</span></td>
<td align="right"><span class="text-up">5,898 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">BQ</span></td>
<td align="right"><span class="text-down">19,658 </span></td>
<td align="right"><span class="text-down">11.7 B</span></td>
<td align="right"><span class="text-down">5,937 </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 : 738.2 B</span>
<span class="fsize12 ml5 mr5">F. NVal : 30.1 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 1.2 M</span>
<span class="fsize12 ml5 mr5">Avg : 5,921 </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/13/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/13/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">ZP</span></td>
<td align="right"><span class="text-up">347,552 </span></td>
<td align="right"><span class="text-up">206.1 B</span></td>
<td align="right"><span class="text-up">5,931 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">361,884 </span></td>
<td align="right"><span class="text-down">214.7 B</span></td>
<td align="right"><span class="text-down">5,933 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">119,389 </span></td>
<td align="right"><span class="text-up">70.6 B</span></td>
<td align="right"><span class="text-up">5,915 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">134,566 </span></td>
<td align="right"><span class="text-down">79.6 B</span></td>
<td align="right"><span class="text-down">5,918 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">TP</span></td>
<td align="right"><span class="text-up">112,063 </span></td>
<td align="right"><span class="text-up">66.5 B</span></td>
<td align="right"><span class="text-up">5,932 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">AG</span></td>
<td align="right"><span class="text-down">113,249 </span></td>
<td align="right"><span class="text-down">67.1 B</span></td>
<td align="right"><span class="text-down">5,923 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">90,525 </span></td>
<td align="right"><span class="text-up">53.7 B</span></td>
<td align="right"><span class="text-up">5,931 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">99,863 </span></td>
<td align="right"><span class="text-down">59.0 B</span></td>
<td align="right"><span class="text-down">5,904 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">84,572 </span></td>
<td align="right"><span class="text-up">50.1 B</span></td>
<td align="right"><span class="text-up">5,920 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">57,345 </span></td>
<td align="right"><span class="text-down">34.0 B</span></td>
<td align="right"><span class="text-down">5,927 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">82,117 </span></td>
<td align="right"><span class="text-up">48.7 B</span></td>
<td align="right"><span class="text-up">5,925 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">49,256 </span></td>
<td align="right"><span class="text-down">29.1 B</span></td>
<td align="right"><span class="text-down">5,918 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">48,065 </span></td>
<td align="right"><span class="text-up">28.5 B</span></td>
<td align="right"><span class="text-up">5,932 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">DP</span></td>
<td align="right"><span class="text-down">12,747 </span></td>
<td align="right"><span class="text-down">7.5 B</span></td>
<td align="right"><span class="text-down">5,917 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-up">12,342 </span></td>
<td align="right"><span class="text-up">7.3 B</span></td>
<td align="right"><span class="text-up">5,905 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">11,973 </span></td>
<td align="right"><span class="text-down">7.1 B</span></td>
<td align="right"><span class="text-down">5,924 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-up">3,683 </span></td>
<td align="right"><span class="text-up">2.2 B</span></td>
<td align="right"><span class="text-up">5,929 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-down">8,420 </span></td>
<td align="right"><span class="text-down">5.0 B</span></td>
<td align="right"><span class="text-down">5,925 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">EP</span></td>
<td align="right"><span class="text-up">3,000 </span></td>
<td align="right"><span class="text-up">1.8 B</span></td>
<td align="right"><span class="text-up">5,925 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">BQ</span></td>
<td align="right"><span class="text-down">5,000 </span></td>
<td align="right"><span class="text-down">3.0 B</span></td>
<td align="right"><span class="text-down">5,925 </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 : 537.3 B</span>
<span class="fsize12 ml5 mr5">F. NVal : 30.1 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 906,459 </span>
<span class="fsize12 ml5 mr5">Avg : 5,927 </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/14/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/14/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" >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">554,786 </span></td>
<td align="right"><span class="text-up">338.9 B</span></td>
<td align="right"><span class="text-up">6,109 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">525,906 </span></td>
<td align="right"><span class="text-down">319.8 B</span></td>
<td align="right"><span class="text-down">6,081 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">485,138 </span></td>
<td align="right"><span class="text-up">294.7 B</span></td>
<td align="right"><span class="text-up">6,075 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">377,446 </span></td>
<td align="right"><span class="text-down">230.0 B</span></td>
<td align="right"><span class="text-down">6,094 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">187,056 </span></td>
<td align="right"><span class="text-up">113.8 B</span></td>
<td align="right"><span class="text-up">6,085 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">164,627 </span></td>
<td align="right"><span class="text-down">100.4 B</span></td>
<td align="right"><span class="text-down">6,096 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">160,092 </span></td>
<td align="right"><span class="text-up">97.1 B</span></td>
<td align="right"><span class="text-up">6,062 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-down">103,273 </span></td>
<td align="right"><span class="text-down">62.8 B</span></td>
<td align="right"><span class="text-down">6,082 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">79,012 </span></td>
<td align="right"><span class="text-up">48.0 B</span></td>
<td align="right"><span class="text-up">6,071 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">98,516 </span></td>
<td align="right"><span class="text-down">59.8 B</span></td>
<td align="right"><span class="text-down">6,070 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">70,651 </span></td>
<td align="right"><span class="text-up">43.1 B</span></td>
<td align="right"><span class="text-up">6,096 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">73,085 </span></td>
<td align="right"><span class="text-down">44.6 B</span></td>
<td align="right"><span class="text-down">6,098 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-up">65,000 </span></td>
<td align="right"><span class="text-up">39.5 B</span></td>
<td align="right"><span class="text-up">6,073 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-down">51,697 </span></td>
<td align="right"><span class="text-down">31.4 B</span></td>
<td align="right"><span class="text-down">6,075 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BQ</span></td>
<td align="right"><span class="text-up">31,865 </span></td>
<td align="right"><span class="text-up">19.2 B</span></td>
<td align="right"><span class="text-up">6,027 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">48,169 </span></td>
<td align="right"><span class="text-down">29.3 B</span></td>
<td align="right"><span class="text-down">6,082 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">30,758 </span></td>
<td align="right"><span class="text-up">18.7 B</span></td>
<td align="right"><span class="text-up">6,066 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">46,536 </span></td>
<td align="right"><span class="text-down">28.3 B</span></td>
<td align="right"><span class="text-down">6,074 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">27,270 </span></td>
<td align="right"><span class="text-up">16.6 B</span></td>
<td align="right"><span class="text-up">6,085 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">BQ</span></td>
<td align="right"><span class="text-down">39,660 </span></td>
<td align="right"><span class="text-down">24.1 B</span></td>
<td align="right"><span class="text-down">6,070 </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.1 T</span>
<span class="fsize12 ml5 mr5">F. NVal : 216.6 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 1.8 M</span>
<span class="fsize12 ml5 mr5">Avg : 6,084 </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/14/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/14/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">554,012 </span></td>
<td align="right"><span class="text-up">338.5 B</span></td>
<td align="right"><span class="text-up">6,109 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">525,906 </span></td>
<td align="right"><span class="text-down">319.8 B</span></td>
<td align="right"><span class="text-down">6,081 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">485,138 </span></td>
<td align="right"><span class="text-up">294.7 B</span></td>
<td align="right"><span class="text-up">6,075 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">368,595 </span></td>
<td align="right"><span class="text-down">224.6 B</span></td>
<td align="right"><span class="text-down">6,094 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">187,042 </span></td>
<td align="right"><span class="text-up">113.8 B</span></td>
<td align="right"><span class="text-up">6,085 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">163,864 </span></td>
<td align="right"><span class="text-down">99.9 B</span></td>
<td align="right"><span class="text-down">6,096 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">156,660 </span></td>
<td align="right"><span class="text-up">95.0 B</span></td>
<td align="right"><span class="text-up">6,063 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">98,516 </span></td>
<td align="right"><span class="text-down">59.8 B</span></td>
<td align="right"><span class="text-down">6,070 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">79,012 </span></td>
<td align="right"><span class="text-up">48.0 B</span></td>
<td align="right"><span class="text-up">6,071 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">59,747 </span></td>
<td align="right"><span class="text-down">36.5 B</span></td>
<td align="right"><span class="text-down">6,103 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">70,651 </span></td>
<td align="right"><span class="text-up">43.1 B</span></td>
<td align="right"><span class="text-up">6,096 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">20,374 </span></td>
<td align="right"><span class="text-down">12.4 B</span></td>
<td align="right"><span class="text-down">6,074 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-up">65,000 </span></td>
<td align="right"><span class="text-up">39.5 B</span></td>
<td align="right"><span class="text-up">6,073 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-down">17,554 </span></td>
<td align="right"><span class="text-down">10.6 B</span></td>
<td align="right"><span class="text-down">6,056 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">17,593 </span></td>
<td align="right"><span class="text-up">10.7 B</span></td>
<td align="right"><span class="text-up">6,067 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">12,729 </span></td>
<td align="right"><span class="text-down">7.8 B</span></td>
<td align="right"><span class="text-down">6,094 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-up">13,632 </span></td>
<td align="right"><span class="text-up">8.2 B</span></td>
<td align="right"><span class="text-up">6,050 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">EP</span></td>
<td align="right"><span class="text-down">3,000 </span></td>
<td align="right"><span class="text-down">1.8 B</span></td>
<td align="right"><span class="text-down">6,075 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">DP</span></td>
<td align="right"><span class="text-up">474 </span></td>
<td align="right"><span class="text-up">288.2 M</span></td>
<td align="right"><span class="text-up">6,080 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">IU</span></td>
<td align="right"><span class="text-down">1,200 </span></td>
<td align="right"><span class="text-down">729.0 M</span></td>
<td align="right"><span class="text-down">6,075 </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 : 991.8 B</span>
<span class="fsize12 ml5 mr5">F. NVal : 216.6 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 1.6 M</span>
<span class="fsize12 ml5 mr5">Avg : 6,087 </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/15/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/15/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" >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">YU</span></td>
<td align="right"><span class="text-up">447,791 </span></td>
<td align="right"><span class="text-up">271.5 B</span></td>
<td align="right"><span class="text-up">6,064 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">335,078 </span></td>
<td align="right"><span class="text-down">203.1 B</span></td>
<td align="right"><span class="text-down">6,062 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">284,818 </span></td>
<td align="right"><span class="text-up">172.7 B</span></td>
<td align="right"><span class="text-up">6,062 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">229,539 </span></td>
<td align="right"><span class="text-down">139.1 B</span></td>
<td align="right"><span class="text-down">6,061 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">DX</span></td>
<td align="right"><span class="text-up">194,491 </span></td>
<td align="right"><span class="text-up">117.8 B</span></td>
<td align="right"><span class="text-up">6,059 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">225,482 </span></td>
<td align="right"><span class="text-down">136.6 B</span></td>
<td align="right"><span class="text-down">6,059 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-up">138,619 </span></td>
<td align="right"><span class="text-up">84.0 B</span></td>
<td align="right"><span class="text-up">6,058 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">179,064 </span></td>
<td align="right"><span class="text-down">108.5 B</span></td>
<td align="right"><span class="text-down">6,059 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">123,061 </span></td>
<td align="right"><span class="text-up">74.5 B</span></td>
<td align="right"><span class="text-up">6,057 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">143,526 </span></td>
<td align="right"><span class="text-down">86.9 B</span></td>
<td align="right"><span class="text-down">6,053 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">78,820 </span></td>
<td align="right"><span class="text-up">47.7 B</span></td>
<td align="right"><span class="text-up">6,050 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">68,285 </span></td>
<td align="right"><span class="text-down">41.3 B</span></td>
<td align="right"><span class="text-down">6,049 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">72,951 </span></td>
<td align="right"><span class="text-up">44.2 B</span></td>
<td align="right"><span class="text-up">6,055 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-down">63,839 </span></td>
<td align="right"><span class="text-down">38.6 B</span></td>
<td align="right"><span class="text-down">6,050 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">36,383 </span></td>
<td align="right"><span class="text-up">22.1 B</span></td>
<td align="right"><span class="text-up">6,066 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-local">CP</span></td>
<td align="right"><span class="text-down">49,275 </span></td>
<td align="right"><span class="text-down">29.9 B</span></td>
<td align="right"><span class="text-down">6,059 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">26,718 </span></td>
<td align="right"><span class="text-up">16.1 B</span></td>
<td align="right"><span class="text-up">6,039 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">34,325 </span></td>
<td align="right"><span class="text-down">20.8 B</span></td>
<td align="right"><span class="text-down">6,068 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">24,529 </span></td>
<td align="right"><span class="text-up">14.8 B</span></td>
<td align="right"><span class="text-up">6,052 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">PD</span></td>
<td align="right"><span class="text-down">27,152 </span></td>
<td align="right"><span class="text-down">16.4 B</span></td>
<td align="right"><span class="text-down">6,053 </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 : 912.0 B</span>
<span class="fsize12 ml5 mr5">F. NVal : 286.1 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 1.5 M</span>
<span class="fsize12 ml5 mr5">Avg : 6,060 </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/15/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/15/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">YU</span></td>
<td align="right"><span class="text-up">445,640 </span></td>
<td align="right"><span class="text-up">270.2 B</span></td>
<td align="right"><span class="text-up">6,064 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">224,007 </span></td>
<td align="right"><span class="text-down">135.8 B</span></td>
<td align="right"><span class="text-down">6,061 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">283,676 </span></td>
<td align="right"><span class="text-up">172.0 B</span></td>
<td align="right"><span class="text-up">6,062 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">197,862 </span></td>
<td align="right"><span class="text-down">119.9 B</span></td>
<td align="right"><span class="text-down">6,058 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">DX</span></td>
<td align="right"><span class="text-up">192,771 </span></td>
<td align="right"><span class="text-up">116.8 B</span></td>
<td align="right"><span class="text-up">6,059 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">185,436 </span></td>
<td align="right"><span class="text-down">112.5 B</span></td>
<td align="right"><span class="text-down">6,066 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-up">138,619 </span></td>
<td align="right"><span class="text-up">84.0 B</span></td>
<td align="right"><span class="text-up">6,058 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">118,526 </span></td>
<td align="right"><span class="text-down">71.7 B</span></td>
<td align="right"><span class="text-down">6,053 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">122,365 </span></td>
<td align="right"><span class="text-up">74.1 B</span></td>
<td align="right"><span class="text-up">6,057 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">109,064 </span></td>
<td align="right"><span class="text-down">66.1 B</span></td>
<td align="right"><span class="text-down">6,064 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">72,951 </span></td>
<td align="right"><span class="text-up">44.2 B</span></td>
<td align="right"><span class="text-up">6,055 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">25,617 </span></td>
<td align="right"><span class="text-down">15.5 B</span></td>
<td align="right"><span class="text-down">6,068 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">65,609 </span></td>
<td align="right"><span class="text-up">39.7 B</span></td>
<td align="right"><span class="text-up">6,051 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">15,460 </span></td>
<td align="right"><span class="text-down">9.3 B</span></td>
<td align="right"><span class="text-down">6,044 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">36,383 </span></td>
<td align="right"><span class="text-up">22.1 B</span></td>
<td align="right"><span class="text-up">6,066 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-down">11,033 </span></td>
<td align="right"><span class="text-down">6.7 B</span></td>
<td align="right"><span class="text-down">6,069 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-up">6,018 </span></td>
<td align="right"><span class="text-up">3.6 B</span></td>
<td align="right"><span class="text-up">6,053 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">HD</span></td>
<td align="right"><span class="text-down">4,283 </span></td>
<td align="right"><span class="text-down">2.6 B</span></td>
<td align="right"><span class="text-down">6,058 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AI</span></td>
<td align="right"><span class="text-up">200 </span></td>
<td align="right"><span class="text-up">120.5 M</span></td>
<td align="right"><span class="text-up">6,025 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-down">550 </span></td>
<td align="right"><span class="text-down">332.9 M</span></td>
<td align="right"><span class="text-down">6,052 </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 : 826.9 B</span>
<span class="fsize12 ml5 mr5">F. NVal : 286.1 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 1.4 M</span>
<span class="fsize12 ml5 mr5">Avg : 6,061 </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" >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-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-bumn">CC</span></td>
<td align="right"><span class="text-down">4.3 M</span></td>
<td align="right"><span class="text-down">2.2 T</span></td>
<td align="right"><span class="text-down">5,211 </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">600.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-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-bumn">CC</span></td>
<td align="right"><span class="text-up">1.0 M</span></td>
<td align="right"><span class="text-up">543.5 B</span></td>
<td align="right"><span class="text-up">5,227 </span></td>
<td align="center">3</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-foreign">KZ</span></td>
<td align="right"><span class="text-up">1.0 M</span></td>
<td align="right"><span class="text-up">522.4 B</span></td>
<td align="right"><span class="text-up">5,198 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-local">BB</span></td>
<td align="right"><span class="text-down">687,067 </span></td>
<td align="right"><span class="text-down">357.4 B</span></td>
<td align="right"><span class="text-down">5,202 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">828,163 </span></td>
<td align="right"><span class="text-up">432.4 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-foreign">ZP</span></td>
<td align="right"><span class="text-down">529,223 </span></td>
<td align="right"><span class="text-down">275.4 B</span></td>
<td align="right"><span class="text-down">5,203 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">827,340 </span></td>
<td align="right"><span class="text-up">431.7 B</span></td>
<td align="right"><span class="text-up">5,218 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">444,696 </span></td>
<td align="right"><span class="text-down">233.5 B</span></td>
<td align="right"><span class="text-down">5,251 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">687,603 </span></td>
<td align="right"><span class="text-up">356.6 B</span></td>
<td align="right"><span class="text-up">5,186 </span></td>
<td align="center">7</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-local">PD</span></td>
<td align="right"><span class="text-up">592,667 </span></td>
<td align="right"><span class="text-up">309.2 B</span></td>
<td align="right"><span class="text-up">5,218 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">418,986 </span></td>
<td align="right"><span class="text-down">218.0 B</span></td>
<td align="right"><span class="text-down">5,202 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">441,000 </span></td>
<td align="right"><span class="text-up">230.2 B</span></td>
<td align="right"><span class="text-up">5,220 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-bumn">NI</span></td>
<td align="right"><span class="text-down">357,177 </span></td>
<td align="right"><span class="text-down">186.1 B</span></td>
<td align="right"><span class="text-down">5,212 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">398,474 </span></td>
<td align="right"><span class="text-up">209.9 B</span></td>
<td align="right"><span class="text-up">5,268 </span></td>
<td align="center">10</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>
</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 : 6.6 T</span>
<span class="fsize12 ml5 mr5">F. NVal : -2.3 T</span>
<span class="fsize12 ml5 mr5">T.Lot : 12.6 M</span>
<span class="fsize12 ml5 mr5">Avg : 5,228 </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" >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

@ -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" selected>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.6 M</span></td>
<td align="right"><span class="text-down">820.1 B</span></td>
<td align="right"><span class="text-down">5,208 </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,265 </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-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">7</td>
<td align="center"><span class="text-bumn">DX</span></td>
<td align="right"><span class="text-down">252,099 </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">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">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-foreign">RX</span></td>
<td align="right"><span class="text-up">17,322 </span></td>
<td align="right"><span class="text-up">9.0 B</span></td>
<td align="right"><span class="text-up">5,202 </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.6 T</span>
<span class="fsize12 ml5 mr5">F. NVal : -2.3 T</span>
<span class="fsize12 ml5 mr5">T.Lot : 3.1 M</span>
<span class="fsize12 ml5 mr5">Avg : 5,225 </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/17/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/17/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" >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-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">1</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">2</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">3</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">4</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">5</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">6</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">7</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">8</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">9</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
</tr>
<tr>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="right"><span class="text-up"></span></td>
<td align="center">10</td>
<td align="center"><span class="text-"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></span></td>
<td align="right"><span class="text-down"></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 : 0 </span>
<span class="fsize12 ml5 mr5">F. NVal : 0 </span>
<span class="fsize12 ml5 mr5">T.Lot : 0 </span>
<span class="fsize12 ml5 mr5">Avg : 0 </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" >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-bumn">CC</span></td>
<td align="right"><span class="text-up">1.9 M</span></td>
<td align="right"><span class="text-up">504.3 B</span></td>
<td align="right"><span class="text-up">2,709 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">3.7 M</span></td>
<td align="right"><span class="text-down">1.0 T</span></td>
<td align="right"><span class="text-down">2,716 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">1.6 M</span></td>
<td align="right"><span class="text-up">436.0 B</span></td>
<td align="right"><span class="text-up">2,720 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">1.8 M</span></td>
<td align="right"><span class="text-down">481.4 B</span></td>
<td align="right"><span class="text-down">2,672 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">1.2 M</span></td>
<td align="right"><span class="text-up">322.4 B</span></td>
<td align="right"><span class="text-up">2,719 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">1.3 M</span></td>
<td align="right"><span class="text-down">358.2 B</span></td>
<td align="right"><span class="text-down">2,717 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">1.0 M</span></td>
<td align="right"><span class="text-up">281.3 B</span></td>
<td align="right"><span class="text-up">2,694 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">933,299 </span></td>
<td align="right"><span class="text-down">253.5 B</span></td>
<td align="right"><span class="text-down">2,716 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">SQ</span></td>
<td align="right"><span class="text-up">712,722 </span></td>
<td align="right"><span class="text-up">193.1 B</span></td>
<td align="right"><span class="text-up">2,709 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">697,845 </span></td>
<td align="right"><span class="text-down">189.9 B</span></td>
<td align="right"><span class="text-down">2,721 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">OD</span></td>
<td align="right"><span class="text-up">518,938 </span></td>
<td align="right"><span class="text-up">140.6 B</span></td>
<td align="right"><span class="text-up">2,709 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">477,399 </span></td>
<td align="right"><span class="text-down">129.4 B</span></td>
<td align="right"><span class="text-down">2,711 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">NI</span></td>
<td align="right"><span class="text-up">496,800 </span></td>
<td align="right"><span class="text-up">133.9 B</span></td>
<td align="right"><span class="text-up">2,695 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">447,840 </span></td>
<td align="right"><span class="text-down">120.9 B</span></td>
<td align="right"><span class="text-down">2,700 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">420,264 </span></td>
<td align="right"><span class="text-up">112.3 B</span></td>
<td align="right"><span class="text-up">2,672 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-down">431,663 </span></td>
<td align="right"><span class="text-down">117.6 B</span></td>
<td align="right"><span class="text-down">2,725 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">PD</span></td>
<td align="right"><span class="text-up">403,468 </span></td>
<td align="right"><span class="text-up">109.5 B</span></td>
<td align="right"><span class="text-up">2,713 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-bumn">NI</span></td>
<td align="right"><span class="text-down">234,641 </span></td>
<td align="right"><span class="text-down">63.1 B</span></td>
<td align="right"><span class="text-down">2,690 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XC</span></td>
<td align="right"><span class="text-up">381,871 </span></td>
<td align="right"><span class="text-up">103.7 B</span></td>
<td align="right"><span class="text-up">2,717 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-local">XA</span></td>
<td align="right"><span class="text-down">217,888 </span></td>
<td align="right"><span class="text-down">59.1 B</span></td>
<td align="right"><span class="text-down">2,714 </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 : 3.3 T</span>
<span class="fsize12 ml5 mr5">F. NVal : -1.1 T</span>
<span class="fsize12 ml5 mr5">T.Lot : 12.2 M</span>
<span class="fsize12 ml5 mr5">Avg : 2,708 </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" >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.9 M</span></td>
<td align="right"><span class="text-up">381.6 B</span></td>
<td align="right"><span class="text-up">1,960 </span></td>
<td align="center">1</td>
<td align="center"><span class="text-foreign">KZ</span></td>
<td align="right"><span class="text-down">3.7 M</span></td>
<td align="right"><span class="text-down">721.6 B</span></td>
<td align="right"><span class="text-down">1,933 </span></td>
</tr>
<tr>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-up">1.5 M</span></td>
<td align="right"><span class="text-up">292.8 B</span></td>
<td align="right"><span class="text-up">1,932 </span></td>
<td align="center">2</td>
<td align="center"><span class="text-bumn">CC</span></td>
<td align="right"><span class="text-down">1.8 M</span></td>
<td align="right"><span class="text-down">353.9 B</span></td>
<td align="right"><span class="text-down">1,933 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-up">1.4 M</span></td>
<td align="right"><span class="text-up">274.7 B</span></td>
<td align="right"><span class="text-up">1,917 </span></td>
<td align="center">3</td>
<td align="center"><span class="text-foreign">BK</span></td>
<td align="right"><span class="text-down">1.6 M</span></td>
<td align="right"><span class="text-down">312.2 B</span></td>
<td align="right"><span class="text-down">1,926 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">RX</span></td>
<td align="right"><span class="text-up">1.0 M</span></td>
<td align="right"><span class="text-up">200.0 B</span></td>
<td align="right"><span class="text-up">1,933 </span></td>
<td align="center">4</td>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-down">1.0 M</span></td>
<td align="right"><span class="text-down">198.1 B</span></td>
<td align="right"><span class="text-down">1,926 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">ZP</span></td>
<td align="right"><span class="text-up">992,588 </span></td>
<td align="right"><span class="text-up">192.3 B</span></td>
<td align="right"><span class="text-up">1,937 </span></td>
<td align="center">5</td>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-down">704,361 </span></td>
<td align="right"><span class="text-down">136.7 B</span></td>
<td align="right"><span class="text-down">1,941 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-up">944,581 </span></td>
<td align="right"><span class="text-up">180.4 B</span></td>
<td align="right"><span class="text-up">1,910 </span></td>
<td align="center">6</td>
<td align="center"><span class="text-local">MG</span></td>
<td align="right"><span class="text-down">529,257 </span></td>
<td align="right"><span class="text-down">101.4 B</span></td>
<td align="right"><span class="text-down">1,916 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">AK</span></td>
<td align="right"><span class="text-up">715,278 </span></td>
<td align="right"><span class="text-up">138.2 B</span></td>
<td align="right"><span class="text-up">1,932 </span></td>
<td align="center">7</td>
<td align="center"><span class="text-foreign">YU</span></td>
<td align="right"><span class="text-down">324,973 </span></td>
<td align="right"><span class="text-down">62.5 B</span></td>
<td align="right"><span class="text-down">1,924 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">MG</span></td>
<td align="right"><span class="text-up">528,650 </span></td>
<td align="right"><span class="text-up">100.6 B</span></td>
<td align="right"><span class="text-up">1,903 </span></td>
<td align="center">8</td>
<td align="center"><span class="text-bumn">NI</span></td>
<td align="right"><span class="text-down">208,165 </span></td>
<td align="right"><span class="text-down">39.8 B</span></td>
<td align="right"><span class="text-down">1,911 </span></td>
</tr>
<tr>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-up">331,760 </span></td>
<td align="right"><span class="text-up">64.1 B</span></td>
<td align="right"><span class="text-up">1,931 </span></td>
<td align="center">9</td>
<td align="center"><span class="text-local">XL</span></td>
<td align="right"><span class="text-down">173,939 </span></td>
<td align="right"><span class="text-down">33.7 B</span></td>
<td align="right"><span class="text-down">1,937 </span></td>
</tr>
<tr>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-up">234,664 </span></td>
<td align="right"><span class="text-up">44.9 B</span></td>
<td align="right"><span class="text-up">1,913 </span></td>
<td align="center">10</td>
<td align="center"><span class="text-foreign">YP</span></td>
<td align="right"><span class="text-down">168,848 </span></td>
<td align="right"><span class="text-down">32.7 B</span></td>
<td align="right"><span class="text-down">1,938 </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 : 2.2 T</span>
<span class="fsize12 ml5 mr5">F. NVal : -90.2 B</span>
<span class="fsize12 ml5 mr5">T.Lot : 11.3 M</span>
<span class="fsize12 ml5 mr5">Avg : 1,931 </span>
</div>
</th>
</tr>
</tfoot>
</table>
</div>
<!-- =======================
======SCRIPT MODULE=========
============================-->
<script src="../../../../module/saham/include/js/data-brokersummary.js"></script>

View file

@ -0,0 +1 @@
Invalid stock code!

View file

@ -1,7 +1,10 @@
//! Tool inputs. String enums are validated by idx-core's `FromStr`, so the //! Tool inputs. String enums are validated by idx-core's `FromStr`, so the
//! accepted values and error messages match the CLI; the schemas list them. //! accepted values and error messages match the CLI; the schemas list them.
use idx_core::analysis::bandar::TIERS;
use idx_core::api::types::{Interval, Period}; use idx_core::api::types::{Interval, Period};
use idx_core::broker::types::{BrokerBoard, BrokerInvestor};
use idx_core::flow::types::FlowSource;
use idx_core::market::{ScreenFilter, ScreenRegion}; use idx_core::market::{ScreenFilter, ScreenRegion};
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema}; use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::Deserialize; use serde::Deserialize;
@ -114,6 +117,70 @@ pub struct AnalyzeParams {
pub kinds: Option<Vec<AnalysisKind>>, pub kinds: Option<Vec<AnalysisKind>>,
} }
/// Which sessions and trades a broker tool covers.
#[derive(Debug, Deserialize, JsonSchema)]
pub struct BrokerScope {
/// One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades.
#[serde(default)]
pub date: Option<String>,
/// First session of a range, YYYY-MM-DD (at most 366 days).
#[serde(default)]
pub from: Option<String>,
/// Last session of the range, YYYY-MM-DD (default today).
#[serde(default)]
pub to: Option<String>,
/// Whose trades to count, by investor domicile (default "all").
#[serde(default)]
#[schemars(schema_with = "investor_schema")]
pub investor: Option<String>,
/// Which board's trades to count (default "all"); "regular" leaves out
/// negotiated block trades.
#[serde(default)]
#[schemars(schema_with = "board_schema")]
pub board: Option<String>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct BrokerParams {
/// IDX ticker, e.g. "BBCA".
pub symbol: String,
#[serde(flatten)]
pub scope: BrokerScope,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct BandarParams {
/// IDX tickers, e.g. ["BBCA", "BBRI"]; the `.JK` suffix is optional. At most 20.
pub symbols: Vec<String>,
#[serde(flatten)]
pub scope: BrokerScope,
/// Tier behind the headline signal: 1, 3, 5 or 10 brokers per side (default 5).
#[serde(default)]
#[schemars(schema_with = "top_schema")]
pub top: Option<u8>,
}
#[derive(Debug, Deserialize, JsonSchema)]
pub struct FlowParams {
/// IDX ticker, e.g. "BBCA".
pub symbol: String,
/// Latest sessions (default 10, at most 60). Omit when giving from/to.
#[serde(default)]
pub days: Option<usize>,
/// First session of a range, YYYY-MM-DD (at most 92 days).
#[serde(default)]
pub from: Option<String>,
/// Last session of the range, YYYY-MM-DD (default today).
#[serde(default)]
pub to: Option<String>,
/// Where the figures come from (default "ipot"): "ipot" gives foreign buy, sell and net
/// value across all boards; "idx" gives exact share counts, the close and the non-regular
/// share, but needs curl-impersonate and may be blocked (HTTP 403).
#[serde(default)]
#[schemars(schema_with = "flow_source_schema")]
pub source: Option<String>,
}
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
pub struct ShareholdersParams { pub struct ShareholdersParams {
/// IDX ticker, e.g. "BBCA". /// IDX ticker, e.g. "BBCA".
@ -178,3 +245,19 @@ fn filter_schema(_: &mut SchemaGenerator) -> Schema {
fn region_schema(_: &mut SchemaGenerator) -> Schema { fn region_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(ScreenRegion::ALL.map(ScreenRegion::as_str).to_vec()) string_enum(ScreenRegion::ALL.map(ScreenRegion::as_str).to_vec())
} }
fn investor_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(BrokerInvestor::ALL.map(BrokerInvestor::as_str).to_vec())
}
fn board_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(BrokerBoard::ALL.map(BrokerBoard::as_str).to_vec())
}
fn flow_source_schema(_: &mut SchemaGenerator) -> Schema {
string_enum(FlowSource::ALL.map(FlowSource::as_str).to_vec())
}
fn top_schema(_: &mut SchemaGenerator) -> Schema {
json_schema!({ "type": "integer", "enum": TIERS })
}

View file

@ -4,10 +4,14 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use idx_core::analysis::BandarQuery;
use idx_core::analysis::bandar::{BandarReport, DEFAULT_TOP};
use idx_core::analysis::fundamental::FundamentalReport; use idx_core::analysis::fundamental::FundamentalReport;
use idx_core::broker::types::BrokerRange;
use idx_core::config::CoreConfig; use idx_core::config::CoreConfig;
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::financial::{EarningsFilter, StatementFilter}; use idx_core::financial::{EarningsFilter, StatementFilter};
use idx_core::flow::types::FlowRange;
use idx_core::investor::{HolderSource, Investor}; use idx_core::investor::{HolderSource, Investor};
use idx_core::market::{ScreenFilter, ScreenQuery, ScreenRegion}; use idx_core::market::{ScreenFilter, ScreenQuery, ScreenRegion};
use idx_core::ownership::types::ChangeRow; use idx_core::ownership::types::ChangeRow;
@ -21,8 +25,9 @@ use serde_json::{Map, Value, json};
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use crate::params::{ use crate::params::{
AnalysisKind, AnalyzeParams, ChangesParams, CompanyParams, CompanySection, EntityParams, AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
HistoryParams, ScreenParams, SearchParams, ShareholdersParams, SymbolsParams, SyncParams, CompanySection, EntityParams, FlowParams, HistoryParams, ScreenParams, SearchParams,
ShareholdersParams, SymbolsParams, SyncParams,
}; };
const SYNC_TOOL: &str = "ownership_sync"; const SYNC_TOOL: &str = "ownership_sync";
@ -110,7 +115,9 @@ impl IdxServer {
impl IdxServer { impl IdxServer {
#[tool( #[tool(
description = "Latest quotes (price, change, volume, 52-week range, market cap) \ description = "Latest quotes (price, change, volume, 52-week range, market cap) \
for one or more IDX tickers." for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true \
when that is before the latest IDX session, so the price is not that session's close: the \
provider lags, the stock did not trade, or the day was an exchange holiday."
)] )]
async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult { async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult {
let symbols = match checked_symbols(params.0.symbols) { let symbols = match checked_symbols(params.0.symbols) {
@ -143,7 +150,8 @@ for one or more IDX tickers."
#[tool( #[tool(
description = "Run a stock screen (top/worst performers, high dividend, low P/E, \ description = "Run a stock screen (top/worst performers, high dividend, low P/E, \
52-week highs/lows, high volume, large cap)." 52-week highs/lows, high volume, large cap). Rows carry `as_of` and, for the IDX region, \
`behind_session` as in get_quotes."
)] )]
async fn screen_stocks(&self, params: Parameters<ScreenParams>) -> ToolResult { async fn screen_stocks(&self, params: Parameters<ScreenParams>) -> ToolResult {
let ScreenParams { let ScreenParams {
@ -165,6 +173,106 @@ for one or more IDX tickers."
.await .await
} }
#[tool(
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."
)]
async fn get_broker_summary(&self, params: Parameters<BrokerParams>) -> ToolResult {
let BrokerParams { symbol, scope } = params.0;
self.live(move |idx| {
let broker = idx.broker();
let range = BrokerRange::from_args(
scope.date.as_deref(),
scope.from.as_deref(),
scope.to.as_deref(),
broker.today(),
)?;
broker.fetch(
&symbol,
range,
scope.investor.as_deref().unwrap_or("all").parse()?,
scope.board.as_deref().unwrap_or("all").parse()?,
CachePolicy::Default,
)
})
.await
}
#[tool(
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."
)]
async fn analyze_bandar(&self, params: Parameters<BandarParams>) -> ToolResult {
let BandarParams {
scope,
symbols,
top,
} = params.0;
let symbols = match checked_symbols(symbols) {
Ok(symbols) => symbols,
Err(err) => return err,
};
self.live(move |idx| {
let query = BandarQuery {
range: BrokerRange::from_args(
scope.date.as_deref(),
scope.from.as_deref(),
scope.to.as_deref(),
idx.broker().today(),
)?,
investor: scope.investor.as_deref().unwrap_or("all").parse()?,
board: scope.board.as_deref().unwrap_or("all").parse()?,
top: top.unwrap_or(DEFAULT_TOP),
};
let screen = idx
.analysis()
.bandar_screen(&symbols, &query, CachePolicy::Default)?;
let reports: &[Fetched<BandarReport>] = &screen.reports;
Ok(json!({ "reports": reports, "failures": failures_json(&screen.failures) }))
})
.await
}
#[tool(
description = "Foreign investors' buying and selling of one IDX ticker, session by \
session (oldest first), with range totals; end of day. With `source` \"ipot\" (default): \
`foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, \
`value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and \
`non_regular_bps` are null, and `rounded` = true means one decimal of precision. With \
\"idx\": exact foreign share counts on the regular board, values estimated at the session's \
average price, the close, `change_bps` and `non_regular_bps`."
)]
async fn get_foreign_flow(&self, params: Parameters<FlowParams>) -> ToolResult {
let FlowParams {
symbol,
days,
from,
to,
source,
} = params.0;
self.live(move |idx| {
let flow = idx.flow();
let range = FlowRange::from_args(days, from.as_deref(), to.as_deref(), flow.today())?;
let source = source.as_deref().unwrap_or("ipot").parse()?;
flow.foreign(&symbol, range, source, CachePolicy::Default)
})
.await
}
#[tool( #[tool(
description = "Company data for one IDX ticker: profile, financial statements, \ description = "Company data for one IDX ticker: profile, financial statements, \
earnings, analyst sentiment, insights, and news. News items carry `relevance`: \ earnings, analyst sentiment, insights, and news. News items carry `relevance`: \
@ -256,17 +364,8 @@ tickers. Tickers that fail are listed under `failures`."
}; };
self.live(move |idx| { self.live(move |idx| {
let comparison = idx.analysis().compare(&symbols, CachePolicy::Default)?; let comparison = idx.analysis().compare(&symbols, CachePolicy::Default)?;
let failures: Vec<Value> = comparison
.failures
.iter()
.map(|(symbol, err)| {
let mut body = error_body(err);
body.insert("symbol".into(), json!(symbol));
Value::Object(body)
})
.collect();
let reports: &[Fetched<FundamentalReport>] = &comparison.reports; let reports: &[Fetched<FundamentalReport>] = &comparison.reports;
Ok(json!({ "reports": reports, "failures": failures })) Ok(json!({ "reports": reports, "failures": failures_json(&comparison.failures) }))
}) })
.await .await
} }
@ -442,6 +541,18 @@ where
}) })
} }
/// Per-symbol failures as error bodies carrying their symbol.
fn failures_json(failures: &[(String, IdxError)]) -> Vec<Value> {
failures
.iter()
.map(|(symbol, err)| {
let mut body = error_body(err);
body.insert("symbol".into(), json!(symbol));
Value::Object(body)
})
.collect()
}
fn input_error(message: String) -> ToolResult { fn input_error(message: String) -> ToolResult {
let err = IdxError::InvalidInput(message); let err = IdxError::InvalidInput(message);
Ok(CallToolResult::structured_error(Value::Object(error_body( Ok(CallToolResult::structured_error(Value::Object(error_body(

View file

@ -1,4 +1,80 @@
[ [
{
"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": {
"board": {
"default": null,
"description": "Which board's trades to count (default \"all\"); \"regular\" leaves out\nnegotiated block trades.",
"enum": [
"all",
"regular",
"cash",
"negotiated"
],
"type": "string"
},
"date": {
"default": null,
"description": "One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades.",
"type": [
"string",
"null"
]
},
"from": {
"default": null,
"description": "First session of a range, YYYY-MM-DD (at most 366 days).",
"type": [
"string",
"null"
]
},
"investor": {
"default": null,
"description": "Whose trades to count, by investor domicile (default \"all\").",
"enum": [
"all",
"foreign",
"domestic"
],
"type": "string"
},
"symbols": {
"description": "IDX tickers, e.g. [\"BBCA\", \"BBRI\"]; the `.JK` suffix is optional. At most 20.",
"items": {
"type": "string"
},
"type": "array"
},
"to": {
"default": null,
"description": "Last session of the range, YYYY-MM-DD (default today).",
"type": [
"string",
"null"
]
},
"top": {
"default": null,
"description": "Tier behind the headline signal: 1, 3, 5 or 10 brokers per side (default 5).",
"enum": [
1,
3,
5,
10
],
"type": "integer"
}
},
"required": [
"symbols"
],
"type": "object"
},
"name": "analyze_bandar"
},
{ {
"description": "Analyze one IDX ticker: technical signals (trend, RSI, MACD, moving averages) and fundamental growth, valuation, and risk ratings. Analyses that fail are listed under `errors`; the call fails only if every analysis fails.", "description": "Analyze one IDX ticker: technical signals (trend, RSI, MACD, moving averages) and fundamental growth, valuation, and risk ratings. Analyses that fail are listed under `errors`; the call fails only if every analysis fails.",
"inputSchema": { "inputSchema": {
@ -57,6 +133,68 @@
}, },
"name": "compare_stocks" "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'. 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": {
"board": {
"default": null,
"description": "Which board's trades to count (default \"all\"); \"regular\" leaves out\nnegotiated block trades.",
"enum": [
"all",
"regular",
"cash",
"negotiated"
],
"type": "string"
},
"date": {
"default": null,
"description": "One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades.",
"type": [
"string",
"null"
]
},
"from": {
"default": null,
"description": "First session of a range, YYYY-MM-DD (at most 366 days).",
"type": [
"string",
"null"
]
},
"investor": {
"default": null,
"description": "Whose trades to count, by investor domicile (default \"all\").",
"enum": [
"all",
"foreign",
"domestic"
],
"type": "string"
},
"symbol": {
"description": "IDX ticker, e.g. \"BBCA\".",
"type": "string"
},
"to": {
"default": null,
"description": "Last session of the range, YYYY-MM-DD (default today).",
"type": [
"string",
"null"
]
}
},
"required": [
"symbol"
],
"type": "object"
},
"name": "get_broker_summary"
},
{ {
"description": "Company data for one IDX ticker: profile, financial statements, earnings, analyst sentiment, insights, and news. News items carry `relevance`: `company` (about this ticker) first, then `market` (general market news). Sections that fail are listed under `errors`; the call fails only if every section fails.", "description": "Company data for one IDX ticker: profile, financial statements, earnings, analyst sentiment, insights, and news. News items carry `relevance`: `company` (about this ticker) first, then `market` (general market news). Sections that fail are listed under `errors`; the call fails only if every section fails.",
"inputSchema": { "inputSchema": {
@ -97,6 +235,58 @@
}, },
"name": "get_company" "name": "get_company"
}, },
{
"description": "Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` \"ipot\" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With \"idx\": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"days": {
"default": null,
"description": "Latest sessions (default 10, at most 60). Omit when giving from/to.",
"format": "uint",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"from": {
"default": null,
"description": "First session of a range, YYYY-MM-DD (at most 92 days).",
"type": [
"string",
"null"
]
},
"source": {
"default": null,
"description": "Where the figures come from (default \"ipot\"): \"ipot\" gives foreign buy, sell and net\nvalue across all boards; \"idx\" gives exact share counts, the close and the non-regular\nshare, but needs curl-impersonate and may be blocked (HTTP 403).",
"enum": [
"ipot",
"idx"
],
"type": "string"
},
"symbol": {
"description": "IDX ticker, e.g. \"BBCA\".",
"type": "string"
},
"to": {
"default": null,
"description": "Last session of the range, YYYY-MM-DD (default today).",
"type": [
"string",
"null"
]
}
},
"required": [
"symbol"
],
"type": "object"
},
"name": "get_foreign_flow"
},
{ {
"description": "Every IDX holding (above 1%) of one investor, matched by name.", "description": "Every IDX holding (above 1%) of one investor, matched by name.",
"inputSchema": { "inputSchema": {
@ -207,7 +397,7 @@
"name": "get_price_history" "name": "get_price_history"
}, },
{ {
"description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers.", "description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday.",
"inputSchema": { "inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "properties": {
@ -267,7 +457,7 @@
"name": "ownership_sync" "name": "ownership_sync"
}, },
{ {
"description": "Run a stock screen (top/worst performers, high dividend, low P/E, 52-week highs/lows, high volume, large cap).", "description": "Run a stock screen (top/worst performers, high dividend, low P/E, 52-week highs/lows, high volume, large cap). Rows carry `as_of` and, for the IDX region, `behind_session` as in get_quotes.",
"inputSchema": { "inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "properties": {

View file

@ -9,10 +9,13 @@ use idx_core::config::CoreConfig;
use idx_core::investor::{ImportSource, Investor}; use idx_core::investor::{ImportSource, Investor};
use serde_json::{Value, json}; use serde_json::{Value, json};
const TOOLS: [&str; 11] = [ const TOOLS: [&str; 14] = [
"analyze_bandar",
"analyze_stock", "analyze_stock",
"compare_stocks", "compare_stocks",
"get_broker_summary",
"get_company", "get_company",
"get_foreign_flow",
"get_investor_holdings", "get_investor_holdings",
"get_ownership_changes", "get_ownership_changes",
"get_price_history", "get_price_history",
@ -247,6 +250,50 @@ fn live_tools_wrap_data_with_freshness() {
assert_fetched(&analysis[kind]); assert_fetched(&analysis[kind]);
} }
let brokers = client.ok(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16"}),
);
assert_fetched(&brokers);
assert_eq!(brokers["source"], "ipot");
assert_eq!(brokers["data"]["buyers"][0]["code"], "MG");
assert_eq!(brokers["data"]["sellers"][0]["name"], "MANDIRI SEKURITAS");
assert_eq!(brokers["data"]["foreign_net_value"], -2_300_000_000_000_i64);
let bandar = client.ok(
"analyze_bandar",
json!({"symbols": ["BBCA", "BBRI", "ZZZZ"], "date": "2026-10-16", "top": 3}),
);
let reports = bandar["reports"].as_array().unwrap();
assert_eq!(reports.len(), 2);
assert_fetched(&reports[0]);
assert_eq!(reports[0]["data"]["top"], 3);
assert_eq!(reports[0]["data"]["method"], "top_concentration_v1");
assert!(
reports[0]["data"]["imbalance_bps"].as_i64()
>= reports[1]["data"]["imbalance_bps"].as_i64()
);
assert_eq!(bandar["failures"][0]["symbol"], "ZZZZ.JK");
assert_eq!(bandar["failures"][0]["code"], "SYMBOLNOTFOUND");
let flow = client.ok(
"get_foreign_flow",
json!({"symbol": "BBCA", "from": "2026-10-12", "to": "2026-10-16"}),
);
assert_fetched(&flow);
assert_eq!(flow["source"], "ipot");
assert_eq!(flow["data"]["days"].as_array().unwrap().len(), 5);
assert_eq!(
flow["data"]["days"][4]["foreign_sell_value"],
4_000_000_000_000_i64
);
let idx_flow = client.ok(
"get_foreign_flow",
json!({"symbol": "BBCA", "from": "2026-10-12", "to": "2026-10-16", "source": "idx"}),
);
assert_eq!(idx_flow["source"], "idx");
assert_eq!(idx_flow["data"]["days"][4]["close"], 5175);
let compared = client.ok("compare_stocks", json!({"symbols": ["BBCA", "BBRI"]})); let compared = client.ok("compare_stocks", json!({"symbols": ["BBCA", "BBRI"]}));
assert_eq!(compared["reports"].as_array().unwrap().len(), 2); assert_eq!(compared["reports"].as_array().unwrap().len(), 2);
assert_eq!(compared["failures"], json!([])); assert_eq!(compared["failures"], json!([]));
@ -273,6 +320,25 @@ fn errors_carry_cli_codes() {
("compare_stocks", json!({"symbols": []})), ("compare_stocks", json!({"symbols": []})),
("get_company", json!({"symbol": "BBCA", "sections": []})), ("get_company", json!({"symbol": "BBCA", "sections": []})),
("analyze_stock", json!({"symbol": "BBCA", "kinds": []})), ("analyze_stock", json!({"symbol": "BBCA", "kinds": []})),
(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16", "from": "2026-10-12"}),
),
(
"get_broker_summary",
json!({"symbol": "BBCA", "date": "2026-10-16", "board": "dark"}),
),
("analyze_bandar", json!({"symbols": ["BBCA"], "top": 4})),
("analyze_bandar", json!({"symbols": []})),
("get_foreign_flow", json!({"symbol": "BBCA", "days": 61})),
(
"get_foreign_flow",
json!({"symbol": "BBCA", "source": "rti"}),
),
(
"get_foreign_flow",
json!({"symbol": "BBCA", "days": 3, "from": "2026-10-12"}),
),
] { ] {
let (is_error, body) = client.call(tool, arguments); let (is_error, body) = client.call(tool, arguments);
assert!(is_error, "{tool}: {body}"); assert!(is_error, "{tool}: {body}");

View file

@ -51,6 +51,10 @@ command parses arguments, makes one idx-core call, and renders the result.
### Dual Provider Model ### Dual Provider Model
- **MSN Finance** — default provider. Rich data: quotes, fundamentals, profile, earnings, financials, sentiment, insights, news, screener, and explicit price-only chart history for supported IDX windows. - **MSN Finance** — default provider. Rich data: quotes, fundamentals, profile, earnings, financials, sentiment, insights, news, screener, and explicit price-only chart history for supported IDX windows.
- **Yahoo Finance** — default/auto history source. Reliable OHLCV data via `/v8/finance/chart/`. - **Yahoo Finance** — default/auto history source. Reliable OHLCV data via `/v8/finance/chart/`.
- **IPOT (Indo Premier)** — broker summaries, whichever provider is selected
(`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries).
- **IDX (idx.co.id)** — the daily trading summary behind `stocks flow
--source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow).
### MSN Endpoints ### MSN Endpoints
- `https://assets.msn.com/service/`: quotes, charts, equities, earnings, - `https://assets.msn.com/service/`: quotes, charts, equities, earnings,
@ -71,8 +75,13 @@ MSN candle dates use WIB (UTC+7); Yahoo candle dates use `meta.gmtoffset`, with
WIB as the fallback. WIB as the fallback.
### Quote and Screener Data ### Quote and Screener Data
- Quotes expose nullable `as_of` last-trade timestamps in WIB. Table output flags - Quotes expose nullable `as_of` last-trade timestamps in WIB, and
quotes whose last trade was more than seven days ago. `behind_session` when `as_of` is before the latest weekday whose session has
ended (16:15 WIB). It is computed on return, not cached; MSN can lag the close
by hours, and on an exchange holiday every quote reads as behind. Only `.JK`
quotes and `--region id` screens are marked; other exchanges keep other hours.
Table output notes behind quotes, and calls those over seven days old not
current.
- `change`, `change_pct`, and `volume` are nullable when the provider omits them; - `change`, `change_pct`, and `volume` are nullable when the provider omits them;
JSON consumers must not assume these fields are always numeric. JSON consumers must not assume these fields are always numeric.
- Listed MSN instruments without a price return `NOMARKETDATA`; unknown symbols - Listed MSN instruments without a price return `NOMARKETDATA`; unknown symbols
@ -83,6 +92,64 @@ WIB as the fallback.
when the initial batch is incomplete, and rejects a still-truncated response. when the initial batch is incomplete, and rejects a still-truncated response.
- Full-list screen caches use an `all` key, separate from older limited results. - Full-list screen caches use an `all` key, separate from older limited results.
### Broker Summaries
- `GET https://www.indopremier.com/module/saham/include/data-brokersummary.php`
takes `code`, `start`/`end` (`MM/DD/YYYY`), `fd` (`all|F|D`) and `board`
(`all|RG|TN|NG`). It needs no login and answers an HTML fragment: the top 10
buyers and top 10 sellers ranked by lots (gross, so a broker can be on both
sides), plus range totals. `api/ipot/parse.rs` parses it purely, with
html5ever (`scraper`), so it reads the cells, date inputs and footer a
browser would build: a hand-written reader kept disagreeing with HTML on
crafted markup in the 0.4.0 review. A table whose rows don't line up
(missing cells, ranks out of order, a broker after a blank row, a second
body) rejects the page, and the dates it echoes must match the request.
- 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 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.
- Data is end of day: IDX hides broker codes during trading. Summaries that
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, 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.
### Foreign Flow
- The default source is IPOT: per session, the all-investor broker summary
gives the value traded and the foreign one (`fd=F`) gives foreign buying
(its footer), the foreign net, and selling (buying less the net), all
boards, through the broker summary cache. IPOT's foreign net matched IDX's
estimate within 0.3 B on BBCA 2026-10-12 to 16. An empty all-investor
summary counts as no session.
- `--source idx`: `GET https://www.idx.co.id/primary/TradingSummary/GetStockSummary`
`?length=9999&start=0&date=YYYYMMDD` answers one JSON row per stock for the
session: prices, volume, value, `ForeignBuy`/`ForeignSell` (shares) and
non-regular volume and value. `Volume`, `Value` and the foreign figures are
the regular board only: IPOT's regular-board total for BBCA on 2026-10-16
matches `Value`, and in a week of every stock no foreign figure exceeded
regular volume. `non_regular_bps` is therefore over regular plus
non-regular value. A day without trading has no rows; rows dated
another day are dropped. `api/idx/parse.rs` parses it purely.
- Cloudflare rejects most TLS fingerprints (in October 2026 only
`curl_safari184` passed), so the request uses curl-impersonate's profile
fallback with a 30 s limit. A challenge page fails that profile, not the
parse.
- Each session is cached whole (every stock) by date: 30 days once past,
5 minutes for today. `Flow::foreign` walks weekdays without requesting
weekends; offline, `--days` skips uncached sessions while a `--from/--to`
range fails on one. `foreign_net_value` is net shares at the session's
average price (value over volume), an estimate.
### Capability Gating ### Capability Gating
``` ```
@ -92,6 +159,8 @@ HistoryProvider = separate trait, not all providers implement
Factory functions: Factory functions:
default_provider(kind) → Box<dyn MarketDataProvider> default_provider(kind) → Box<dyn MarketDataProvider>
history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)> history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)>
broker_provider() → Box<dyn BrokerSummaryProvider>
trading_summary_provider() → Box<dyn TradingSummaryProvider>
``` ```
## Ownership Module (SQLite-backed) ## Ownership Module (SQLite-backed)

View file

@ -54,7 +54,9 @@ nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B again
scripts/live-smoke.sh --dry-run scripts/live-smoke.sh --dry-run
``` ```
- `stocks`: every `stocks` command for one ticker, plus `compare BUMI,ADRO,AIMS`, - `stocks`: every `stocks` command for one ticker (`broker`, `bandar`,
`flow` and `flow --source idx` on the new binary only, so an older `--compare` build that predates
them is fine), plus `compare BUMI,ADRO,AIMS`,
whose MSN key ratios contain non-finite values. whose MSN key ratios contain non-finite values.
- `ownership`: `discover`, then imports the newest supported report into a temp - `ownership`: `discover`, then imports the newest supported report into a temp
DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many

View file

@ -288,7 +288,7 @@
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
sleep 2 sleep 2
} | timeout 30 ${mcpPackage}/bin/idx-mcp > out.jsonl } | timeout 30 ${mcpPackage}/bin/idx-mcp > out.jsonl
jq -se 'map(select(.id == 2))[0].result.tools | length == 11' out.jsonl jq -se 'map(select(.id == 2))[0].result.tools | length == 14' out.jsonl
touch "$out" touch "$out"
''; '';
# The website's generated pages match the CLI help and the MCP # The website's generated pages match the CLI help and the MCP

View file

@ -123,6 +123,14 @@ contract() {
echo '(.highlights | type == "array") and (.risks | type == "array")' ;; echo '(.highlights | type == "array") and (.risks | type == "array")' ;;
"stocks news "*) "stocks news "*)
echo 'type == "array" and all(.[]; (.title | type == "string") and (.url | type == "string"))' ;; echo 'type == "array" and all(.[]; (.title | type == "string") and (.url | type == "string"))' ;;
"stocks broker "*)
echo '(.symbol | type == "string") and (.buyers | length > 0) and (.sellers | length > 0) and all(.buyers[]; (.code | type == "string") and (.value | type == "number")) and (.total_value | type == "number")' ;;
"stocks bandar "*)
echo 'type == "array" and length > 0 and all(.[]; (.method | type == "string") and (.tiers | length == 4) and (.imbalance_bps | type == "number"))' ;;
"stocks flow "*"--source idx"*)
echo '(.source == "idx") and (.days | length > 0) and all(.days[]; (.date | type == "string") and (.foreign_buy | type == "number") and (.foreign_sell | type == "number")) and (.foreign_net | type == "number")' ;;
"stocks flow "*)
echo '(.source == "ipot") and (.days | length > 0) and all(.days[]; (.date | type == "string") and (.foreign_buy_value | type == "number") and (.foreign_sell_value | type == "number")) and (.foreign_net_value | type == "number")' ;;
"ownership discover"*) "ownership discover"*)
echo 'type == "array" and length > 0 and (.[0].pdf_url | type == "string")' ;; echo 'type == "array" and length > 0 and (.[0].pdf_url | type == "string")' ;;
"ownership releases") "ownership releases")
@ -194,6 +202,12 @@ run_case() {
echo " PASS" echo " PASS"
} }
# A command the --compare binary may predate runs on the new binary only.
run_new_case() {
local COMPARE_BIN=""
run_case "$@"
}
if (( ! DRY_RUN )); then if (( ! DRY_RUN )); then
command -v jq >/dev/null 2>&1 || { echo "jq is required; run inside nix develop" >&2; exit 1; } command -v jq >/dev/null 2>&1 || { echo "jq is required; run inside nix develop" >&2; exit 1; }
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
@ -217,6 +231,11 @@ if selected stocks; then
run_case compare "stocks compare $SYMBOL,BBRI" run_case compare "stocks compare $SYMBOL,BBRI"
# BUMI/ADRO/AIMS return non-finite MSN key ratios (Infinity/NaN). # BUMI/ADRO/AIMS return non-finite MSN key ratios (Infinity/NaN).
run_case compare-nonfinite "stocks compare BUMI,ADRO,AIMS" run_case compare-nonfinite "stocks compare BUMI,ADRO,AIMS"
# Broker summaries and foreign flow (IPOT; --source idx via curl-impersonate).
run_new_case broker "stocks broker $SYMBOL"
run_new_case bandar "stocks bandar $SYMBOL,BBRI,TLKM"
run_new_case flow "stocks flow $SYMBOL --days 3"
run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx"
fi fi
if selected ownership; then if selected ownership; then

View file

@ -62,5 +62,35 @@
"prompt": "Search the KSEI ownership database for investors named Salim.", "prompt": "Search the KSEI ownership database for investors named Salim.",
"expect_tools": [{"name": "search_investors", "args": {"query": "Salim"}}], "expect_tools": [{"name": "search_investors", "args": {"query": "Salim"}}],
"expect_text": [] "expect_text": []
},
{
"id": "broker",
"prompt": "Which brokers bought and sold the most BBCA on 2026-10-16?",
"expect_tools": [{"name": "get_broker_summary", "args": {"symbol": "BBCA", "date": "2026-10-16"}}],
"expect_text": ["\\bMG\\b"],
"live": {
"prompt": "Which brokers bought and sold the most BBCA in the latest session?",
"expect_tools": [{"name": "get_broker_summary", "args": {"symbol": "BBCA"}}]
}
},
{
"id": "bandar",
"prompt": "Were the big brokers (bandar) accumulating or distributing BBCA on 2026-10-16?",
"expect_tools": [{"name": "analyze_bandar", "args": {"symbols": ["BBCA"], "date": "2026-10-16"}}],
"expect_text": ["(?i)distribut"],
"live": {
"prompt": "Are the big brokers (bandar) accumulating or distributing BBCA lately?",
"expect_tools": [{"name": "analyze_bandar", "args": {"symbols": ["BBCA"]}}]
}
},
{
"id": "flow",
"prompt": "What was the net foreign flow in BBCA from 2026-10-12 to 2026-10-16?",
"expect_tools": [{"name": "get_foreign_flow", "args": {"symbol": "BBCA", "from": "2026-10-12", "to": "2026-10-16"}}],
"expect_text": ["(?i)foreign"],
"live": {
"prompt": "What has the net foreign flow in BBCA been over the last 10 sessions?",
"expect_tools": [{"name": "get_foreign_flow", "args": {"symbol": "BBCA"}}]
}
} }
] ]

View file

@ -66,6 +66,9 @@ Commands:
valuation Run valuation analysis on a stock valuation Run valuation analysis on a stock
risk Run risk analysis on a stock risk Run risk analysis on a stock
fundamental Run full fundamental analysis on a stock fundamental Run full fundamental analysis on a stock
broker Which brokers bought and sold a stock
bandar Bandarmology: is buying more concentrated than selling
flow Foreign buying and selling of a stock, session by session
profile Get company profile profile Get company profile
financials Get financial statements financials Get financial statements
earnings Get earnings report earnings Get earnings report
@ -278,6 +281,216 @@ Examples:
idx -o json stocks fundamental BBCA idx -o json stocks fundamental BBCA
``` ```
### `idx stocks broker`
```text
Which brokers bought and sold a stock: the top 10 buyers and top 10 sellers by lots, with value and average price, from Indo Premier (IPOT). End of day: IDX does not show broker codes during trading. Without dates, the latest session with trades.
Usage: idx stocks broker [OPTIONS] <SYMBOL>
Arguments:
<SYMBOL>
Single ticker symbol (e.g. BBCA)
Options:
--date <DATE>
One session, YYYY-MM-DD. Default: the latest session with trades
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
[possible values: auto, table, json, jsonl]
--from <FROM>
First session of a range, YYYY-MM-DD (at most 366 days)
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
--to <TO>
Last session of the range, YYYY-MM-DD. Default: today
--investor <INVESTOR>
Whose trades to count
Possible values:
- all: Every trade
- foreign: Trades by foreign investors only
- domestic: Trades by domestic investors only
[default: all]
-v, --verbose...
Increase verbosity (-v, -vv)
--board <BOARD>
Which board's trades to count; `regular` leaves out negotiated block trades
Possible values:
- all: Every board
- regular: Regular market (RG), the continuous order book
- cash: Cash market (TN)
- negotiated: Negotiated market (NG): pre-arranged block trades
[default: all]
--offline
Run without network requests
--no-cache
Bypass local cache
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks broker BBCA
idx stocks broker BBCA --date 2026-10-16
idx stocks broker BBCA --from 2026-10-12 --to 2026-10-16 --board regular
idx -o json stocks broker BBCA --investor foreign
```
### `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). `--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>...
Arguments:
<SYMBOLS>...
One or more symbols, comma-separated or space-separated; `-` reads them from stdin
Options:
--date <DATE>
One session, YYYY-MM-DD. Default: the latest session with trades
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
[possible values: auto, table, json, jsonl]
--from <FROM>
First session of a range, YYYY-MM-DD (at most 366 days)
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
--to <TO>
Last session of the range, YYYY-MM-DD. Default: today
--investor <INVESTOR>
Whose trades to count
Possible values:
- all: Every trade
- foreign: Trades by foreign investors only
- domestic: Trades by domestic investors only
[default: all]
-v, --verbose...
Increase verbosity (-v, -vv)
--board <BOARD>
Which board's trades to count; `regular` leaves out negotiated block trades
Possible values:
- all: Every board
- regular: Regular market (RG), the continuous order book
- cash: Cash market (TN)
- negotiated: Negotiated market (NG): pre-arranged block trades
[default: all]
--offline
Run without network requests
--no-cache
Bypass local cache
--top <TOP>
Tier behind the headline signal: 1, 3, 5 or 10 brokers per side
[default: 5]
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks bandar BBCA
idx stocks bandar BBCA --from 2026-10-12 --to 2026-10-16 --top 3
idx stocks bandar BBCA,BBRI,BMRI,TLKM
idx stocks screen --filter high-volume | jq -r '.[].symbol' | idx stocks bandar -
```
### `idx stocks flow`
```text
Foreign buying and selling of a stock, session by session, end of day. By default from Indo Premier (IPOT): foreign buy, sell and net value across all boards and foreign investors' share of trading, two plain requests per session. `--source idx` reads IDX's own trading summary instead: exact foreign share counts, the close and the share traded outside the regular board, one request per session to idx.co.id, which needs curl-impersonate and a network IDX's Cloudflare lets through.
Usage: idx stocks flow [OPTIONS] <SYMBOL>
Arguments:
<SYMBOL>
Single ticker symbol (e.g. BBCA)
Options:
--days <DAYS>
Latest sessions to show (default 10, at most 60)
-o, --output <OUTPUT>
Output format [default: auto = table on a terminal, json when piped]
[possible values: auto, table, json, jsonl]
--from <FROM>
First session of a range, YYYY-MM-DD (at most 92 days)
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
--to <TO>
Last session of the range, YYYY-MM-DD. Default: today
--source <SOURCE>
Where the figures come from
Possible values:
- ipot: Indo Premier's broker summary: foreign buy, sell and net value across all boards, two plain requests per session. Figures of a million and more are rounded to one decimal
- idx: IDX's own trading summary: exact foreign share counts, close and the non-regular share, regular board. Needs curl-impersonate and a network IDX's Cloudflare lets through
[default: ipot]
-v, --verbose...
Increase verbosity (-v, -vv)
--offline
Run without network requests
--no-cache
Bypass local cache
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks flow BBCA
idx stocks flow BBCA --days 20
idx stocks flow BBCA --from 2026-10-01 --to 2026-10-16
idx stocks flow BBCA --source idx
idx -o json stocks flow BBCA | jq '.foreign_net_value'
```
### `idx stocks profile` ### `idx stocks profile`
```text ```text

View file

@ -1,6 +1,6 @@
--- ---
title: Stocks title: Stocks
description: Quotes, history, company data, analysis and screening. description: Quotes, history, company data, broker summaries, analysis and screening.
--- ---
All `stocks` commands take IDX tickers (`BBCA`, or `BBCA.JK`). Every flag is All `stocks` commands take IDX tickers (`BBCA`, or `BBCA.JK`). Every flag is
@ -28,6 +28,83 @@ them from stdin. `history` takes `--period` (`1d` to `5y`) and `--interval`
| `insights` | AI insights from the provider | | `insights` | AI insights from the provider |
| `news` | recent news, this company's first (`ABOUT` = `company`), then general market news (`market`) | | `news` | recent news, this company's first (`ABOUT` = `company`), then general market news (`market`) |
## Broker summary
```bash
idx stocks broker BBCA
idx stocks broker BBCA --date 2026-10-16
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 (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
lots, so a broker can appear on both sides. The source rounds figures of a
million and more to one decimal, and the output says so.
## Foreign flow
```bash
idx stocks flow BBCA
idx stocks flow BBCA --days 20
idx stocks flow BBCA --from 2026-10-01 --to 2026-10-16
idx stocks flow BBCA --source idx
```
What foreign investors bought and sold, session by session, with the totals
over the range. Without dates it shows the latest 10 sessions (`--days`, at
most 60).
By default the figures come from Indo Premier (IPOT), like `broker`: foreign
buy, sell and net value across all boards and foreign investors' share of
trading. It needs nothing installed, takes two requests per session, and
rounds figures of a million and more to one decimal.
`--source idx` reads IDX's own daily trading summary instead: exact foreign
share counts, the close and day change, and the share of value traded
outside the regular board. Those figures are the regular board, and values
are estimated at each session's average price. Each session is one request
to idx.co.id, cached for every stock, and needs
[curl-impersonate](/start/install/) and a network IDX's Cloudflare lets
through; even then it can answer 403.
## Bandarmology
```bash
idx stocks bandar BBCA
idx stocks bandar BBCA --from 2026-10-12 --to 2026-10-16 --top 3
idx stocks screen --filter high-volume | jq -r '.[].symbol' | idx stocks bandar -
```
`bandar` reads the broker summary: for the top 1, 3, 5 and 10 brokers on each
side, the buyers' value minus the sellers' value as a share of the value
traded. The headline tier (`--top`, default 5) gives the signal:
| Top-N buy minus sell, % of value | Signal |
| --- | --- |
| +15% or more | big accumulation |
| +5% to +15% | accumulation |
| within ±5% | neutral |
| -15% to -5% | distribution |
| -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 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`).
## Analysis ## Analysis
| Command | What it computes | | Command | What it computes |
@ -62,6 +139,7 @@ idx stocks screen --filter high-dividend | jq -r '.[].symbol' | idx stocks compa
## Providers ## Providers
MSN is the default for quotes, company data and screening; Yahoo serves MSN is the default for quotes, company data and screening; Yahoo serves
price history. Switch with `IDX_PROVIDER=msn|yahoo` and price history. Indo Premier (IPOT) serves broker summaries and IDX's own site
serves foreign flow, whichever provider is selected. Switch with `IDX_PROVIDER=msn|yahoo` and
`IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the matching `IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the matching
[config keys](/start/config/). [config keys](/start/config/).

View file

@ -10,6 +10,20 @@ starts with `--allow-sync`. Results that come from a live provider are wrapped
as `{data, source, fetched_at, stale}`; failures return `{error, code, as `{data, source, fetched_at, stale}`; failures return `{error, code,
message}` with `isError` set. 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. 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 |
| --- | --- | --- | --- |
| `board` | `all` / `regular` / `cash` / `negotiated` | no | Which board's trades to count (default "all"); "regular" leaves out negotiated block trades. |
| `date` | string | no | One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades. |
| `from` | string | no | First session of a range, YYYY-MM-DD (at most 366 days). |
| `investor` | `all` / `foreign` / `domestic` | no | Whose trades to count, by investor domicile (default "all"). |
| `symbols` | array of string | yes | IDX tickers, e.g. ["BBCA", "BBRI"]; the `.JK` suffix is optional. At most 20. |
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
| `top` | `1` / `3` / `5` / `10` | no | Tier behind the headline signal: 1, 3, 5 or 10 brokers per side (default 5). |
## `analyze_stock` ## `analyze_stock`
Analyze one IDX ticker: technical signals (trend, RSI, MACD, moving averages) and fundamental growth, valuation, and risk ratings. Analyses that fail are listed under `errors`; the call fails only if every analysis fails. Analyze one IDX ticker: technical signals (trend, RSI, MACD, moving averages) and fundamental growth, valuation, and risk ratings. Analyses that fail are listed under `errors`; the call fails only if every analysis fails.
@ -27,6 +41,19 @@ Compare fundamental reports (growth, valuation, risk) across IDX tickers. Ticker
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `symbols` | array of string | yes | IDX tickers, e.g. ["BBCA", "BBRI"]; the `.JK` suffix is optional. At most 20. | | `symbols` | array of string | yes | IDX tickers, e.g. ["BBCA", "BBRI"]; the `.JK` suffix is optional. At most 20. |
## `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'. 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 |
| --- | --- | --- | --- |
| `board` | `all` / `regular` / `cash` / `negotiated` | no | Which board's trades to count (default "all"); "regular" leaves out negotiated block trades. |
| `date` | string | no | One session, YYYY-MM-DD. Omit date, from and to for the latest session with trades. |
| `from` | string | no | First session of a range, YYYY-MM-DD (at most 366 days). |
| `investor` | `all` / `foreign` / `domestic` | no | Whose trades to count, by investor domicile (default "all"). |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
## `get_company` ## `get_company`
Company data for one IDX ticker: profile, financial statements, earnings, analyst sentiment, insights, and news. News items carry `relevance`: `company` (about this ticker) first, then `market` (general market news). Sections that fail are listed under `errors`; the call fails only if every section fails. Company data for one IDX ticker: profile, financial statements, earnings, analyst sentiment, insights, and news. News items carry `relevance`: `company` (about this ticker) first, then `market` (general market news). Sections that fail are listed under `errors`; the call fails only if every section fails.
@ -36,6 +63,18 @@ Company data for one IDX ticker: profile, financial statements, earnings, analys
| `sections` | array of `profile` / `financials` / `earnings` / `sentiment` / `insights` / `news` | no | Sections to fetch (default: all). | | `sections` | array of `profile` / `financials` / `earnings` / `sentiment` / `insights` / `news` | no | Sections to fetch (default: all). |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". | | `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
## `get_foreign_flow`
Foreign investors' buying and selling of one IDX ticker, session by session (oldest first), with range totals; end of day. With `source` "ipot" (default): `foreign_buy_value`, `foreign_sell_value` and `foreign_net_value` in IDR across all boards, `value` traded, and `foreign_share_bps` (foreign share of trading); share counts, close and `non_regular_bps` are null, and `rounded` = true means one decimal of precision. With "idx": exact foreign share counts on the regular board, values estimated at the session's average price, the close, `change_bps` and `non_regular_bps`.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `days` | integer | no | Latest sessions (default 10, at most 60). Omit when giving from/to. |
| `from` | string | no | First session of a range, YYYY-MM-DD (at most 92 days). |
| `source` | `ipot` / `idx` | no | Where the figures come from (default "ipot"): "ipot" gives foreign buy, sell and net value across all boards; "idx" gives exact share counts, the close and the non-regular share, but needs curl-impersonate and may be blocked (HTTP 403). |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
## `get_investor_holdings` ## `get_investor_holdings`
Every IDX holding (above 1%) of one investor, matched by name. Every IDX holding (above 1%) of one investor, matched by name.
@ -68,7 +107,7 @@ OHLCV price history for one IDX ticker.
## `get_quotes` ## `get_quotes`
Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday.
| Input | Type | Required | Description | | Input | Type | Required | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
@ -98,7 +137,7 @@ Download or refresh the KSEI ownership snapshot (several MB) into the local data
## `screen_stocks` ## `screen_stocks`
Run a stock screen (top/worst performers, high dividend, low P/E, 52-week highs/lows, high volume, large cap). Run a stock screen (top/worst performers, high dividend, low P/E, 52-week highs/lows, high volume, large cap). Rows carry `as_of` and, for the IDX region, `behind_session` as in get_quotes.
| Input | Type | Required | Description | | Input | Type | Required | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |

View file

@ -74,12 +74,14 @@ idx version
## Helper tools ## Helper tools
Everyday use needs nothing else: stock data with the default MSN provider, Everyday use needs nothing else: stock data with the default MSN provider,
price history, `ownership sync` and every local ownership query. A few price history, broker summaries, bandarmology and foreign flow, `ownership sync`
commands call external tools: and every
local ownership query. A few commands call external tools:
| Tool | Needed for | | Tool | Needed for |
| --- | --- | | --- | --- |
| `curl_chrome*` from [curl-impersonate](https://github.com/lexiforest/curl-impersonate), or `IDX_CURL_IMPERSONATE_BIN` | `ownership discover` and `ownership import --url` (IDX sits behind Cloudflare), and Yahoo fundamentals with `IDX_PROVIDER=yahoo` | | `curl_chrome*` from [curl-impersonate](https://github.com/lexiforest/curl-impersonate), or `IDX_CURL_IMPERSONATE_BIN` | `ownership discover` and `ownership import --url` (IDX sits behind Cloudflare), and Yahoo fundamentals with `IDX_PROVIDER=yahoo` |
| the full curl-impersonate set, with its `curl_safari*` and `curl_firefox*` profiles | `stocks flow --source idx`: IDX's Cloudflare rules pass only some browser fingerprints, and idx tries each installed profile (the default source, IPOT, needs nothing) |
| `mutool` from MuPDF | importing legacy PDF ownership reports | | `mutool` from MuPDF | importing legacy PDF ownership reports |
## The MCP server ## The MCP server