test: add golden output parity gate and live A/B compare

Phase 0 of docs/DESIGN.md, before any refactor code:

- tests/golden.rs runs every case in tests/golden/cases.toml (126
  outputs across help, completions, config, cache, all stocks commands
  on MSN and Yahoo mocks, error paths, and ownership queries on a seeded
  fixture DB) and compares exit code, stdout, and stderr byte for byte.
  Regenerate only with UPDATE_GOLDEN=1 for a reviewed output change.
- scripts/live-smoke.sh --compare <old-idx> runs each live case with
  both binaries and fails on differing exit codes, added/missing/retyped
  JSON keys, or changed values for slow-moving data.
- Exclude golden files from the whitespace/EOF pre-commit fixers, which
  would otherwise rewrite pinned help output.

Both gates were fault-injected: a one-word table change fails exactly
the three history goldens, and a binary dropping profile.long_name fails
only the profile live comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
hermes 2026-10-14 08:33:25 +00:00
commit fe627496dd
133 changed files with 6033 additions and 23 deletions

View file

@ -1,8 +1,11 @@
repos: repos:
- repo: builtin - repo: builtin
hooks: hooks:
# Golden files pin exact CLI output, including trailing spaces.
- id: trailing-whitespace - id: trailing-whitespace
exclude: ^tests/golden/.*\.golden$
- id: end-of-file-fixer - id: end-of-file-fixer
exclude: ^tests/golden/.*\.golden$
- id: check-merge-conflict - id: check-merge-conflict
- repo: local - repo: local

View file

@ -13,7 +13,7 @@ src/
├── ownership/ xlsx + pdf parsers, archive (ZIP/TXT), remote discovery, ├── ownership/ xlsx + pdf parsers, archive (ZIP/TXT), remote discovery,
│ snapshot sync, db (SQLite/FTS5), entities, search, graph │ snapshot sync, db (SQLite/FTS5), entities, search, graph
└── output/ table.rs, json.rs └── output/ table.rs, json.rs
tests/cli.rs CLI integration tests; fixtures in tests/fixtures/ tests/ cli.rs (behavior), golden.rs + golden/ (output parity), fixtures/
scripts/ smoke, installer, and ownership snapshot publisher scripts/ smoke, installer, and ownership snapshot publisher
``` ```
@ -42,5 +42,5 @@ Docs:
```bash ```bash
nix flake check -L # canonical gate (fmt, clippy, tests, package) nix flake check -L # canonical gate (fmt, clippy, tests, package)
nix develop --command cargo test <name> # focused loop nix develop --command cargo test <name> # focused loop
scripts/live-smoke.sh # opt-in live provider drift check scripts/live-smoke.sh --compare <old> # live drift check, A/B against an older build
``` ```

View file

@ -162,7 +162,8 @@ is another 20-30% smaller than pretty JSON.
- stdout carries only data; diagnostics and warnings go to stderr. Already true. - stdout carries only data; diagnostics and warnings go to stderr. Already true.
- Errors in `json`/`jsonl` mode are one JSON object on stderr with stdout empty. - Errors in `json`/`jsonl` mode are one JSON object on stderr with stdout empty.
Already true. Already true.
- No ANSI color when stdout is not a terminal. - No ANSI color when stdout is not a terminal. Today some table headers emit
bold codes even when piped.
- Commands that take symbols accept `-` to read them from stdin, one per line or - Commands that take symbols accept `-` to read them from stdin, one per line or
whitespace-separated: whitespace-separated:
`idx stocks screen --filter low-pe | jq -r '.[].symbol' | idx stocks compare -` `idx stocks screen --filter low-pe | jq -r '.[].symbol' | idx stocks compare -`
@ -264,14 +265,16 @@ Before any refactor code lands, capture the current CLI as golden files:
fixture-built ownership DB fixture-built ownership DB
- the documented error cases (invalid input, unsupported provider, offline - the documented error cases (invalid input, unsupported provider, offline
without cache, empty ownership DB) without cache, empty ownership DB)
- stored as `tests/golden/<case>.{stdout,stderr,exit}` and compared byte for - cases listed in `tests/golden/cases.toml`; each result stored as
byte by one data-driven test in `tests/golden.rs` `tests/golden/<case>.<format>.golden` (exit code, stdout, stderr) and
compared byte for byte by one data-driven test in `tests/golden.rs`
Regenerating goldens (`UPDATE_GOLDEN=1`) is only allowed for an intentional, Regenerating goldens (`UPDATE_GOLDEN=1`) is only allowed for an intentional,
reviewed output change, and the diff must be in the same commit with the reason reviewed output change, and the diff must be in the same commit with the reason
in the message. Phases 1-3 must not change any golden file. Phase 0 changes only in the message. Phases 2-4 must not change any golden file. Phase 1 changes only
what it documents: default format resolution, JSON whitespace when piped, and what it documents: default format resolution, JSON whitespace when piped, ANSI
exit codes; JSON content and table bytes stay identical. color when piped (table output emits bold headers into pipes today), and exit
codes; JSON content and table text stay identical.
`tests/cli.rs` stays the behavioral suite and must keep passing unchanged. `tests/cli.rs` stays the behavioral suite and must keep passing unchanged.

View file

@ -8,7 +8,7 @@ offline with pinned tools:
| Check | Covers | | Check | Covers |
| --- | --- | | --- | --- |
| `fmt`, `clippy` | formatting and lints (`--all-targets`) | | `fmt`, `clippy` | formatting and lints (`--all-targets`) |
| `test` | unit/module tests and `tests/cli.rs` CLI integration tests | | `test` | unit/module tests, `tests/cli.rs` CLI integration tests, and `tests/golden.rs` output parity |
| `default`, `package` | release build and `cargo package` verification | | `default`, `package` | release build and `cargo package` verification |
| `packaged-cli` | the wrapped Nix binary starts, emits JSON, and generates completions | | `packaged-cli` | the wrapped Nix binary starts, emits JSON, and generates completions |
| `ownership-publisher`, `publisher-test` | packaged publisher entry point and its regression scenarios | | `ownership-publisher`, `publisher-test` | packaged publisher entry point and its regression scenarios |
@ -19,6 +19,12 @@ fixture-backed mock providers (`IDX_USE_MOCK_PROVIDER=1`) and covers output
contracts, cache/offline transitions, provider routing, error codes, ownership contracts, cache/offline transitions, provider routing, error codes, ownership
imports and queries, and snapshot sync. imports and queries, and snapshot sync.
`tests/golden.rs` pins the exact output (exit code, stdout, stderr) of every
command listed in `tests/golden/cases.toml`, in table and JSON. A mismatch means
user-visible output changed. Regenerate with
`UPDATE_GOLDEN=1 cargo test --test golden` only when the change is intended,
and commit the golden diff with the reason.
For a focused loop, run one check or filter Cargo tests: For a focused loop, run one check or filter Cargo tests:
```bash ```bash
@ -39,6 +45,7 @@ validates each JSON output shape. It is opt-in and never part of the gate.
nix develop --command scripts/live-smoke.sh # stocks + ownership nix develop --command scripts/live-smoke.sh # stocks + ownership
nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI
nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx
nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B against an older build
scripts/live-smoke.sh --dry-run scripts/live-smoke.sh --dry-run
``` ```
@ -48,6 +55,12 @@ scripts/live-smoke.sh --dry-run
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
datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts. datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts.
`--compare <old-idx>` runs every case with both binaries back to back, each
with its own config and cache, and fails on a different exit code, any added,
missing, or retyped JSON key, or changed values for `profile`, `financials`,
`earnings`, and ownership results. Build the old side with, for example,
`nix build "git+file://$PWD?ref=main" -o old`.
Each run isolates config, cache, and data under `tmp/live-smoke/<timestamp>/` Each run isolates config, cache, and data under `tmp/live-smoke/<timestamp>/`
and keeps per-case `.stdout`, `.stderr`, and `.validation` logs there. and keeps per-case `.stdout`, `.stderr`, and `.validation` logs there.

View file

@ -1,8 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Live provider drift check: runs shipped commands against real MSN, Yahoo, # Live provider drift check: runs shipped commands against real MSN, Yahoo,
# and IDX endpoints and validates the JSON output shape. Deterministic CLI # and IDX endpoints and validates the JSON output shape. Deterministic CLI
# behavior is covered by tests/cli.rs; this only answers "do the upstreams # behavior is covered by tests/cli.rs and tests/golden.rs; this answers "do the
# still look the way our parsers expect?" # upstreams still look the way our parsers expect?"
#
# With --compare <old-idx>, every case also runs the old binary back to back
# and fails on any difference in exit code or JSON structure, and on any value
# difference for slow-moving data (see "Live A/B check" in docs/DESIGN.md).
set -u set -u
set -o pipefail set -o pipefail
@ -10,6 +14,7 @@ set -o pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SYMBOL="BBCA" SYMBOL="BBCA"
BIN_PATH="" BIN_PATH=""
COMPARE_BIN=""
DRY_RUN=0 DRY_RUN=0
declare -a GROUPS_SELECTED=() declare -a GROUPS_SELECTED=()
declare -a FAILURES=() declare -a FAILURES=()
@ -24,6 +29,7 @@ Options:
--group <stocks|ownership> Restrict to a group. Repeatable. Default: both --group <stocks|ownership> Restrict to a group. Repeatable. Default: both
--symbol <ticker> Primary ticker. Default: BBCA --symbol <ticker> Primary ticker. Default: BBCA
--bin <path> Use an existing idx binary (skips cargo build) --bin <path> Use an existing idx binary (skips cargo build)
--compare <old-idx> Also run an older idx binary and diff the results
--dry-run Print cases without running them --dry-run Print cases without running them
--help Show this help --help Show this help
EOF EOF
@ -34,6 +40,7 @@ while [[ $# -gt 0 ]]; do
--group) GROUPS_SELECTED+=("${2:-}"); shift 2 ;; --group) GROUPS_SELECTED+=("${2:-}"); shift 2 ;;
--symbol) SYMBOL="${2:-}"; shift 2 ;; --symbol) SYMBOL="${2:-}"; shift 2 ;;
--bin) BIN_PATH="${2:-}"; shift 2 ;; --bin) BIN_PATH="${2:-}"; shift 2 ;;
--compare) COMPARE_BIN="${2:-}"; shift 2 ;;
--dry-run) DRY_RUN=1; shift ;; --dry-run) DRY_RUN=1; shift ;;
--help|-h) usage; exit 0 ;; --help|-h) usage; exit 0 ;;
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
@ -58,14 +65,36 @@ selected() {
WORKDIR="$ROOT_DIR/tmp/live-smoke/$(date +%Y%m%d-%H%M%S)" WORKDIR="$ROOT_DIR/tmp/live-smoke/$(date +%Y%m%d-%H%M%S)"
LOG_DIR="$WORKDIR/logs" LOG_DIR="$WORKDIR/logs"
BASE_ENV=( COMMON_ENV=("IDX_PROVIDER=msn" "IDX_HISTORY_PROVIDER=auto" "IDX_NO_COLOR=1")
"XDG_CONFIG_HOME=$WORKDIR/config"
"XDG_CACHE_HOME=$WORKDIR/cache" # Each binary gets its own config, cache, and data so both really fetch.
"XDG_DATA_HOME=$WORKDIR/data" side_env() {
"IDX_PROVIDER=msn" local home="$WORKDIR/$1"
"IDX_HISTORY_PROVIDER=auto" printf '%s\n' "XDG_CONFIG_HOME=$home/config" "XDG_CACHE_HOME=$home/cache" \
"IDX_NO_COLOR=1" "XDG_DATA_HOME=$home/data" "${COMMON_ENV[@]}"
) }
# Values of these commands should not move between two back-to-back runs.
stable_values() {
case "$1" in
"stocks profile "*|"stocks financials "*|"stocks earnings "*|"ownership ticker "*|"ownership releases") return 0 ;;
*) return 1 ;;
esac
}
# Key paths (array indices collapsed) that disappeared or appeared, and keys
# whose non-null type changed. Empty lists mean the structure matches.
# shellcheck disable=SC2016
SHAPE_DIFF='
def norm: map(if type == "number" then "*" else . end) | join(".");
def keyset: [paths | norm] | unique;
def types: [paths(scalars) as $p | select(getpath($p) != null)
| {k: ($p | norm), t: (getpath($p) | type)}]
| group_by(.k) | map({key: .[0].k, value: (map(.t) | unique)}) | from_entries;
($old[0] | keyset) as $ko | ($new[0] | keyset) as $kn
| ($old[0] | types) as $to | ($new[0] | types) as $tn
| {missing: ($ko - $kn), added: ($kn - $ko),
type_changed: [$to | keys[] as $k | select($tn[$k] != null and $tn[$k] != $to[$k]) | $k]}'
# jq filter describing the JSON shape each command must keep. # jq filter describing the JSON shape each command must keep.
contract() { contract() {
@ -103,8 +132,16 @@ contract() {
esac esac
} }
run_bin() {
local side="$1" bin="$2" out="$3"
shift 3
local -a envs
mapfile -t envs < <(side_env "$side")
env "${envs[@]}" "$bin" -o json "$@" >"$out.stdout" 2>"$out.stderr"
}
run_case() { run_case() {
local name="$1" cmd="$2" filter log local name="$1" cmd="$2" filter log status old_status diff
local -a args local -a args
TOTAL=$((TOTAL + 1)) TOTAL=$((TOTAL + 1))
printf '[%02d] %s\n' "$TOTAL" "$name" printf '[%02d] %s\n' "$TOTAL" "$name"
@ -115,8 +152,19 @@ run_case() {
log="$LOG_DIR/$(printf '%02d' "$TOTAL")_$name" log="$LOG_DIR/$(printf '%02d' "$TOTAL")_$name"
read -r -a args <<< "$cmd" read -r -a args <<< "$cmd"
if ! env "${BASE_ENV[@]}" "$BIN_PATH" -o json "${args[@]}" >"$log.stdout" 2>"$log.stderr"; then run_bin new "$BIN_PATH" "$log" "${args[@]}"
FAILURES+=("$name: non-zero exit (see $log.stderr)") status=$?
if [[ -n "$COMPARE_BIN" ]]; then
run_bin old "$COMPARE_BIN" "$log.old" "${args[@]}"
old_status=$?
if (( status != old_status )); then
FAILURES+=("$name: exit $status, old binary exited $old_status (see $log.stderr, $log.old.stderr)")
echo " FAIL exit differs from old binary"
return
fi
fi
if (( status != 0 )); then
FAILURES+=("$name: exit $status (see $log.stderr)")
echo " FAIL exit" echo " FAIL exit"
return return
fi fi
@ -126,6 +174,22 @@ run_case() {
echo " FAIL shape" echo " FAIL shape"
return return
fi fi
if [[ -n "$COMPARE_BIN" ]]; then
diff="$(jq -n --slurpfile old "$log.old.stdout" --slurpfile new "$log.stdout" "$SHAPE_DIFF")"
if [[ "$(jq -c '[.[] | length] | add' <<< "$diff")" != 0 ]]; then
printf '%s\n' "$diff" >"$log.diff"
FAILURES+=("$name: JSON structure differs from old binary (see $log.diff)")
echo " FAIL structure differs"
return
fi
if stable_values "$cmd" && ! cmp -s \
<(jq -S 'del(.. | .imported_at?)' "$log.old.stdout") \
<(jq -S 'del(.. | .imported_at?)' "$log.stdout"); then
FAILURES+=("$name: values differ from old binary (diff $log.old.stdout $log.stdout)")
echo " FAIL values differ"
return
fi
fi
PASSED=$((PASSED + 1)) PASSED=$((PASSED + 1))
echo " PASS" echo " PASS"
} }
@ -137,7 +201,10 @@ if (( ! DRY_RUN )); then
BIN_PATH="$ROOT_DIR/target/debug/idx" BIN_PATH="$ROOT_DIR/target/debug/idx"
(cd "$ROOT_DIR" && cargo build --quiet --bin idx) || { echo "cargo build failed" >&2; exit 1; } (cd "$ROOT_DIR" && cargo build --quiet --bin idx) || { echo "cargo build failed" >&2; exit 1; }
fi fi
env "${BASE_ENV[@]}" "$BIN_PATH" config set ownership.db_path "$WORKDIR/ownership.db" >/dev/null if [[ -n "$COMPARE_BIN" && ! -x "$COMPARE_BIN" ]]; then
echo "--compare binary not found or not executable: $COMPARE_BIN" >&2
exit 2
fi
fi fi
if selected stocks; then if selected stocks; then
@ -158,10 +225,12 @@ if selected ownership; then
url="$(jq -r '.[0].pdf_url' "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout")" url="$(jq -r '.[0].pdf_url' "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout")"
run_case import "ownership import --url $url" run_case import "ownership import --url $url"
run_case releases "ownership releases" run_case releases "ownership releases"
run_case ticker "ownership ticker $SYMBOL"
fi fi
fi fi
(( DRY_RUN )) && { echo "dry-run: $TOTAL case(s)"; exit 0; } (( DRY_RUN )) && { echo "dry-run: $TOTAL case(s)"; exit 0; }
[[ -n "$COMPARE_BIN" ]] && printf '\ncompared against: %s\n' "$COMPARE_BIN"
printf '\nsummary: %d passed, %d failed\nlogs: %s\n' "$PASSED" "$((TOTAL - PASSED))" "$LOG_DIR" printf '\nsummary: %d passed, %d failed\nlogs: %s\n' "$PASSED" "$((TOTAL - PASSED))" "$LOG_DIR"
if (( ${#FAILURES[@]} > 0 )); then if (( ${#FAILURES[@]} > 0 )); then
printf ' - %s\n' "${FAILURES[@]}" printf ' - %s\n' "${FAILURES[@]}"

208
tests/golden.rs Normal file
View file

@ -0,0 +1,208 @@
//! Golden output tests: byte-for-byte parity of CLI stdout, stderr, and exit
//! code for every shipped command (see "Parity and Verification" in
//! docs/DESIGN.md).
//!
//! Cases live in `tests/golden/cases.toml`; expected output in
//! `tests/golden/<case>.golden`. Regenerate only for an intentional, reviewed
//! output change, and commit the diff with the reason:
//!
//! UPDATE_GOLDEN=1 cargo test --test golden
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use serde::Deserialize;
const IDX: &str = env!("CARGO_BIN_EXE_idx");
const REPO: &str = env!("CARGO_MANIFEST_DIR");
#[derive(Deserialize)]
struct CaseFile {
case: Vec<Case>,
}
#[derive(Deserialize)]
struct Case {
name: String,
args: Vec<String>,
/// Output formats to expand the case into (`-o <format>` is prepended and
/// `.<format>` appended to the name). Empty means run the args as given.
#[serde(default = "default_formats")]
formats: Vec<String>,
#[serde(default)]
env: BTreeMap<String, String>,
/// Seed the fixture ownership DB before running.
#[serde(default)]
ownership: bool,
/// Commands run first in the same environment; their output is ignored.
#[serde(default)]
before: Vec<Vec<String>>,
}
fn default_formats() -> Vec<String> {
vec!["table".into(), "json".into()]
}
fn golden_dir() -> PathBuf {
Path::new(REPO).join("tests").join("golden")
}
fn work_dir() -> PathBuf {
std::env::temp_dir().join(format!("idx-golden-{}", std::process::id()))
}
/// Runs idx with an isolated home under `root` and only the given IDX_* env.
fn idx(root: &Path, args: &[String], env: &BTreeMap<String, String>) -> std::process::Output {
let mut cmd = Command::new(IDX);
cmd.current_dir(REPO).args(args);
for (key, _) in std::env::vars_os() {
if key.to_string_lossy().starts_with("IDX_") || key == "NO_COLOR" {
cmd.env_remove(key);
}
}
cmd.env("HOME", root)
.env("XDG_CONFIG_HOME", root.join("config"))
.env("XDG_CACHE_HOME", root.join("cache"))
.env("XDG_DATA_HOME", root.join("data"))
.env("IDX_USE_MOCK_PROVIDER", "1")
.envs(env);
cmd.output().expect("run idx")
}
/// Fixture ownership DB: the above-1% XLSX (2026-08-31) plus two KSEI archive
/// months (2026-01-30, 2026-02-27), with wall-clock columns pinned.
fn seeded_ownership_db() -> &'static Path {
static DB: OnceLock<PathBuf> = OnceLock::new();
DB.get_or_init(|| {
let root = work_dir().join("seed");
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("create seed dir");
// The importer derives the release from the IDX file name.
let xlsx = root.join("peng-2026-08-00017-satu-persen.xlsx");
fs::copy(
Path::new(REPO).join("tests/fixtures/ksei_above1_20260831_excerpt.xlsx"),
&xlsx,
)
.expect("copy xlsx fixture");
let inputs = [
xlsx,
Path::new(REPO).join("tests/fixtures/ksei_balancepos_20260130_excerpt.txt"),
Path::new(REPO).join("tests/fixtures/ksei_balancepos_20260227_excerpt.txt"),
];
for input in inputs {
let args = ["ownership", "import", "--file", input.to_str().unwrap()]
.map(String::from)
.to_vec();
let out = idx(&root, &args, &BTreeMap::new());
assert!(out.status.success(), "seed import failed: {out:?}");
}
let db = root.join("data/idx/ownership.db");
let conn = rusqlite::Connection::open(&db).expect("open seed db");
conn.execute_batch(
"UPDATE ownership_releases SET imported_at = 0;
UPDATE entities SET created_at = 0, updated_at = 0;
UPDATE bing_holdings SET fetched_at = 0;",
)
.expect("pin timestamps");
db
})
}
fn render(root: &Path, out: &std::process::Output) -> String {
let normalize = |bytes: &[u8]| {
String::from_utf8_lossy(bytes)
.replace(root.to_str().unwrap(), "<ROOT>")
.replace(REPO, "<REPO>")
};
format!(
"exit: {}\n--- stdout\n{}--- stderr\n{}",
out.status.code().map_or("signal".into(), |c| c.to_string()),
normalize(&out.stdout),
normalize(&out.stderr),
)
}
#[test]
fn cli_output_matches_golden_files() {
let raw = fs::read_to_string(golden_dir().join("cases.toml")).expect("read cases.toml");
let cases: CaseFile = toml::from_str(&raw).expect("parse cases.toml");
let update = std::env::var_os("UPDATE_GOLDEN").is_some();
let mut seen = BTreeSet::new();
let mut failures = Vec::new();
for case in &cases.case {
let runs: Vec<(String, Vec<String>)> = if case.formats.is_empty() {
vec![(case.name.clone(), case.args.clone())]
} else {
case.formats
.iter()
.map(|format| {
let mut args = vec!["-o".to_string(), format.clone()];
args.extend(case.args.iter().cloned());
(format!("{}.{format}", case.name), args)
})
.collect()
};
for (name, args) in runs {
assert!(seen.insert(name.clone()), "duplicate golden case {name}");
let root = work_dir().join(&name);
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).expect("create case dir");
if case.ownership {
let target = root.join("data/idx/ownership.db");
fs::create_dir_all(target.parent().unwrap()).expect("create data dir");
fs::copy(seeded_ownership_db(), &target).expect("copy seeded db");
}
for before in &case.before {
idx(&root, before, &case.env);
}
let actual = render(&root, &idx(&root, &args, &case.env));
let path = golden_dir().join(format!("{name}.golden"));
if update {
fs::write(&path, &actual).expect("write golden");
} else {
match fs::read_to_string(&path) {
Ok(expected) if expected == actual => {}
Ok(expected) => failures.push(format!(
"{name}: output differs\n--- expected\n{expected}\n--- actual\n{actual}"
)),
Err(_) => failures.push(format!("{name}: missing {}", path.display())),
}
}
}
}
for entry in fs::read_dir(golden_dir()).expect("read golden dir") {
let path = entry.expect("dir entry").path();
let Some(stem) = path
.file_name()
.unwrap()
.to_str()
.unwrap()
.strip_suffix(".golden")
else {
continue;
};
if !seen.contains(stem) {
if update {
fs::remove_file(&path).expect("remove orphan golden");
} else {
failures.push(format!("{stem}: golden file has no case in cases.toml"));
}
}
}
let _ = fs::remove_dir_all(work_dir());
assert!(
failures.is_empty(),
"{} golden mismatch(es); if the change is intentional, rerun with UPDATE_GOLDEN=1 \
and commit the diff with the reason\n\n{}",
failures.len(),
failures.join("\n\n")
);
}

View file

@ -0,0 +1,7 @@
exit: 0
--- stdout
{
"failed": [],
"removed": 0
}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
cleared 0 files
--- stderr

View file

@ -0,0 +1,10 @@
exit: 0
--- stdout
{
"files": 0,
"newest": null,
"oldest": null,
"path": "<ROOT>/cache/idx",
"size_bytes": 0
}
--- stderr

View file

@ -0,0 +1,8 @@
exit: 0
--- stdout
path: <ROOT>/cache/idx
files: 0
size_bytes: 0
oldest: -
newest: -
--- stderr

327
tests/golden/cases.toml Normal file
View file

@ -0,0 +1,327 @@
# Golden output cases for tests/golden.rs.
#
# Each case runs once per entry in `formats` (default: table and json) with
# `-o <format>` prepended; `formats = []` runs the args as given. Every run uses
# the mock provider (IDX_USE_MOCK_PROVIDER=1) and an isolated home directory.
# `ownership = true` seeds the fixture DB; `before` runs setup commands first.
#
# `idx version` is excluded on purpose: its output changes with every release.
# --- help and shell completions ---------------------------------------------
[[case]]
name = "help-root"
args = ["--help"]
formats = []
[[case]]
name = "help-stocks"
args = ["stocks", "--help"]
formats = []
[[case]]
name = "help-ownership"
args = ["ownership", "--help"]
formats = []
[[case]]
name = "help-config"
args = ["config", "--help"]
formats = []
[[case]]
name = "help-cache"
args = ["cache", "--help"]
formats = []
[[case]]
name = "completions-bash"
args = ["completions", "bash"]
formats = []
# --- config and cache ---------------------------------------------------------
[[case]]
name = "config-path"
args = ["config", "path"]
formats = []
[[case]]
name = "config-init"
args = ["config", "init"]
formats = []
[[case]]
name = "config-get"
args = ["config", "get", "general.provider"]
formats = []
before = [["config", "init"]]
[[case]]
name = "config-set"
args = ["config", "set", "general.provider", "yahoo"]
formats = []
[[case]]
name = "cache-info-empty"
args = ["cache", "info"]
[[case]]
name = "cache-clear"
args = ["cache", "clear"]
# --- stocks: MSN (default provider) --------------------------------------------
[[case]]
name = "stocks-quote"
args = ["stocks", "quote", "BBCA"]
[[case]]
name = "stocks-quote-multi"
args = ["stocks", "quote", "BBCA,BBRI"]
[[case]]
name = "stocks-history"
args = ["stocks", "history", "BBCA"]
[[case]]
name = "stocks-history-msn"
args = ["stocks", "history", "BBCA", "--history-provider", "msn"]
[[case]]
name = "stocks-technical"
args = ["stocks", "technical", "BBCA"]
[[case]]
name = "stocks-growth"
args = ["stocks", "growth", "BBCA"]
[[case]]
name = "stocks-valuation"
args = ["stocks", "valuation", "BBCA"]
[[case]]
name = "stocks-valuation-nonfinite"
args = ["stocks", "valuation", "BBCA"]
env = { IDX_MOCK_MSN_KEYRATIOS_FIXTURE = "tests/fixtures/msn_keyratios_infinity.json" }
[[case]]
name = "stocks-risk"
args = ["stocks", "risk", "BBCA"]
[[case]]
name = "stocks-fundamental"
args = ["stocks", "fundamental", "BBCA"]
[[case]]
name = "stocks-compare"
args = ["stocks", "compare", "BBCA", "BBRI"]
[[case]]
name = "stocks-profile"
args = ["stocks", "profile", "BBCA"]
[[case]]
name = "stocks-financials"
args = ["stocks", "financials", "BBCA"]
[[case]]
name = "stocks-financials-income"
args = ["stocks", "financials", "BBCA", "--statement", "income"]
[[case]]
name = "stocks-earnings"
args = ["stocks", "earnings", "BBCA"]
[[case]]
name = "stocks-earnings-history-quarterly"
args = ["stocks", "earnings", "BBCA", "--history", "--quarterly"]
[[case]]
name = "stocks-earnings-forecast-annual"
args = ["stocks", "earnings", "BBCA", "--forecast", "--annual"]
[[case]]
name = "stocks-sentiment"
args = ["stocks", "sentiment", "BBCA"]
[[case]]
name = "stocks-insights"
args = ["stocks", "insights", "BBCA"]
[[case]]
name = "stocks-news"
args = ["stocks", "news", "BBCA", "--limit", "5"]
[[case]]
name = "stocks-screen"
args = ["stocks", "screen"]
[[case]]
name = "stocks-screen-worst"
args = ["stocks", "screen", "--filter", "worst-performers", "--limit", "5"]
[[case]]
name = "stocks-screen-high-volume"
args = ["stocks", "screen", "--filter", "high-volume"]
[[case]]
name = "stocks-screen-large-cap"
args = ["stocks", "screen", "--filter", "large-cap"]
[[case]]
name = "stocks-quote-offline-warm"
args = ["--offline", "stocks", "quote", "BBCA"]
before = [["stocks", "quote", "BBCA"]]
# --- stocks: Yahoo provider -----------------------------------------------------
[[case]]
name = "yahoo-quote"
args = ["stocks", "quote", "BBCA"]
env = { IDX_PROVIDER = "yahoo" }
[[case]]
name = "yahoo-history"
args = ["stocks", "history", "BBCA"]
env = { IDX_PROVIDER = "yahoo" }
[[case]]
name = "yahoo-fundamental"
args = ["stocks", "fundamental", "BBCA"]
env = { IDX_PROVIDER = "yahoo" }
# --- errors ---------------------------------------------------------------------
[[case]]
name = "error-usage-missing-symbol"
args = ["stocks", "quote"]
[[case]]
name = "error-invalid-provider"
args = ["stocks", "quote", "BBCA"]
env = { IDX_PROVIDER = "bogus" }
[[case]]
name = "error-yahoo-profile-unsupported"
args = ["stocks", "profile", "BBCA"]
env = { IDX_PROVIDER = "yahoo" }
[[case]]
name = "error-industry-only-fundamentals"
args = ["stocks", "valuation", "BBCA"]
env = { IDX_MOCK_MSN_KEYRATIOS_FIXTURE = "tests/fixtures/msn_keyratios_industry_only.json" }
[[case]]
name = "error-screen-region"
args = ["stocks", "screen", "--region", "eu"]
[[case]]
name = "error-offline-no-cache-flags"
args = ["--offline", "--no-cache", "stocks", "quote", "BBCA"]
[[case]]
name = "error-offline-cache-miss"
args = ["--offline", "stocks", "quote", "BBCA"]
[[case]]
name = "error-provider-failure"
args = ["stocks", "quote", "BBCA"]
env = { IDX_MOCK_ERROR = "1" }
[[case]]
name = "error-compare-total-failure"
args = ["stocks", "compare", "BBCA", "BBRI"]
env = { IDX_MOCK_ERROR = "1" }
[[case]]
name = "error-fetch-bing-unsupported"
args = ["ownership", "import", "--fetch-bing", "BBCA"]
# --- ownership --------------------------------------------------------------------
[[case]]
name = "ownership-releases-empty"
args = ["ownership", "releases"]
[[case]]
name = "ownership-ticker-empty"
args = ["ownership", "ticker", "BBCA"]
[[case]]
name = "ownership-import-xlsx"
args = ["ownership", "import", "--file", "tests/fixtures/ksei_above1_20260831_excerpt.xlsx"]
[[case]]
name = "ownership-import-archive-txt"
args = ["ownership", "import", "--file", "tests/fixtures/ksei_balancepos_20260227_excerpt.txt"]
[[case]]
name = "ownership-releases"
args = ["ownership", "releases"]
ownership = true
[[case]]
name = "ownership-ticker"
args = ["ownership", "ticker", "BBCA"]
ownership = true
[[case]]
name = "ownership-ticker-ksei"
args = ["ownership", "ticker", "BBCA", "--source", "ksei"]
ownership = true
[[case]]
name = "ownership-entity"
args = ["ownership", "entity", "ANTHONI SALIM"]
ownership = true
[[case]]
name = "ownership-search"
args = ["ownership", "search", "salim"]
ownership = true
[[case]]
name = "ownership-cross-holders"
args = ["ownership", "cross-holders", "--min-tickers", "1"]
ownership = true
[[case]]
name = "ownership-concentration"
args = ["ownership", "concentration"]
ownership = true
[[case]]
name = "ownership-flow"
args = ["ownership", "flow", "BBCA"]
ownership = true
[[case]]
name = "ownership-changes"
args = ["ownership", "changes", "--from", "2026-01-30", "--to", "2026-02-27"]
ownership = true
[[case]]
name = "ownership-graph"
args = ["ownership", "graph", "BBCA"]
ownership = true
[[case]]
name = "ownership-graph-dot"
args = ["ownership", "graph", "BBCA", "--format", "dot"]
ownership = true
[[case]]
name = "ownership-resolve-unresolved"
args = ["ownership", "resolve", "unresolved"]
ownership = true
[[case]]
name = "ownership-resolve-map"
args = ["ownership", "resolve", "map", "SALIM ANTHONI", "ANTHONI SALIM"]
ownership = true
[[case]]
name = "ownership-resolve-merge"
args = ["ownership", "resolve", "merge", "1", "2"]
ownership = true

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
msn
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
<ROOT>/config/idx/config.toml
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
<ROOT>/config/idx/config.toml
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
ok
--- stderr

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "PROVIDERUNAVAILABLE",
"error": true,
"message": "provider unavailable"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: provider unavailable

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "UNSUPPORTED",
"error": true,
"message": "unsupported: --fetch-bing import is not implemented yet"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: unsupported: --fetch-bing import is not implemented yet

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "PARSEERROR",
"error": true,
"message": "parse error: unsupported: company fundamentals unavailable from MSN; industry fallback is disabled"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: parse error: unsupported: company fundamentals unavailable from MSN; industry fallback is disabled

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "CONFIGERROR",
"error": true,
"message": "config error: invalid provider 'bogus' (expected yahoo or msn)"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: config error: invalid provider 'bogus' (expected yahoo or msn)

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "CACHEMISS",
"error": true,
"message": "cache miss: msn-quote/BBCA.JK"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: cache miss: msn-quote/BBCA.JK

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "INVALIDINPUT",
"error": true,
"message": "invalid input: cannot combine --offline with --no-cache"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: invalid input: cannot combine --offline with --no-cache

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "PROVIDERUNAVAILABLE",
"error": true,
"message": "provider unavailable"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: provider unavailable

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "INVALIDINPUT",
"error": true,
"message": "invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)

View file

@ -0,0 +1,9 @@
exit: 2
--- stdout
--- stderr
error: the following required arguments were not provided:
<SYMBOLS>...
Usage: idx stocks quote <SYMBOLS>...
For more information, try '--help'.

View file

@ -0,0 +1,9 @@
exit: 2
--- stdout
--- stderr
error: the following required arguments were not provided:
<SYMBOLS>...
Usage: idx stocks quote <SYMBOLS>...
For more information, try '--help'.

View file

@ -0,0 +1,8 @@
exit: 1
--- stdout
--- stderr
{
"code": "UNSUPPORTED",
"error": true,
"message": "unsupported: BBCA.JK: command requires --provider msn"
}

View file

@ -0,0 +1,4 @@
exit: 1
--- stdout
--- stderr
Error: unsupported: BBCA.JK: command requires --provider msn

View file

@ -0,0 +1,20 @@
exit: 0
--- stdout
Manage local cache
Usage: idx cache [OPTIONS] <COMMAND>
Commands:
info Show cache usage information
clear Clear all cached files
help Print this message or the help of the given subcommand(s)
Options:
-o, --output <OUTPUT> Output format [possible values: table, json]
--no-color Disable colored output
-q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests
--no-cache Bypass local cache
-h, --help Print help
--- stderr

View file

@ -0,0 +1,22 @@
exit: 0
--- stdout
Manage configuration
Usage: idx config [OPTIONS] <COMMAND>
Commands:
init Create default configuration file
get Get a configuration value by key
set Set a configuration value by key
path Print configuration file path
help Print this message or the help of the given subcommand(s)
Options:
-o, --output <OUTPUT> Output format [possible values: table, json]
--no-color Disable colored output
-q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests
--no-cache Bypass local cache
-h, --help Print help
--- stderr

View file

@ -0,0 +1,58 @@
exit: 0
--- stdout
Ownership intelligence (KSEI + Bing).
Preferred bootstrap path:
1. Run `idx ownership sync` to install or refresh a maintained SQLite snapshot.
2. If no snapshot manifest is available, run `idx ownership discover` and then `idx ownership import --url <pdf-url>`.
3. Local `.xlsx`, `.pdf`, `.zip`, and `.txt` imports remain available for manual or fallback workflows.
Usage: idx ownership [OPTIONS] <COMMAND>
Commands:
discover Discover the latest IDX-hosted ownership report URLs
import Import ownership data directly from source files
sync Install or refresh a maintained ownership SQLite snapshot
ticker Show all holders for a ticker (KSEI + Bing combined)
entity Show all holdings for an entity across tickers
search Search entities by name
cross-holders Rank entities by cross-ownership breadth
concentration Rank tickers by ownership concentration
flow Show Bing institutional flow for a ticker
changes Diff two KSEI releases
graph Ownership network graph
resolve Manual entity resolution workflow
releases List imported KSEI releases
help Print this message or the help of the given subcommand(s)
Options:
-o, --output <OUTPUT>
Output format
[possible values: table, json]
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
-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 ownership sync
idx ownership sync --manifest /path/to/ownership-snapshot-manifest.json
idx ownership discover --limit 1
idx ownership import --url <pdf-url>
idx ownership releases
--- stderr

View file

@ -0,0 +1,40 @@
exit: 0
--- stdout
idx-cli is a Rust command-line tool for Indonesian stock market (IDX) analysis.
It supports quote lookup, historical data, local caching, and output formats for both humans and AI agents.
Usage: idx [OPTIONS] <COMMAND>
Commands:
stocks Stock data and analysis
config Manage configuration
cache Manage local cache
ownership Ownership intelligence (bootstrap with `ownership sync`)
completions Generate shell completions
version Show idx-cli version
help Print this message or the help of the given subcommand(s)
Options:
-o, --output <OUTPUT>
Output format
[possible values: table, json]
--no-color
Disable colored output
-q, --quiet
Suppress non-essential output
-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')
--- stderr

View file

@ -0,0 +1,33 @@
exit: 0
--- stdout
Stock data and analysis
Usage: idx stocks [OPTIONS] <COMMAND>
Commands:
quote Get real-time stock quotes
history Get historical OHLC data
technical Run technical analysis on a stock
growth Run growth analysis on a stock
valuation Run valuation analysis on a stock
risk Run risk analysis on a stock
fundamental Run full fundamental analysis on a stock
profile Get company profile
financials Get financial statements
earnings Get earnings report
sentiment Get crowd sentiment
insights Get AI insights
news Get stock news
screen MSN screener
compare Compare fundamentals across stocks
help Print this message or the help of the given subcommand(s)
Options:
-o, --output <OUTPUT> Output format [possible values: table, json]
--no-color Disable colored output
-q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests
--no-cache Bypass local cache
-h, --help Print help
--- stderr

View file

@ -0,0 +1,109 @@
exit: 0
--- stdout
[
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL ID",
"change_type": "decreased",
"old_bps": 2400,
"new_bps": 2334,
"delta_bps": -66
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL IS",
"change_type": "increased",
"old_bps": 75,
"new_bps": 129,
"delta_bps": 54
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL MF",
"change_type": "increased",
"old_bps": 117,
"new_bps": 152,
"delta_bps": 35
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN MF",
"change_type": "increased",
"old_bps": 307,
"new_bps": 337,
"delta_bps": 30
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN OT",
"change_type": "decreased",
"old_bps": 91,
"new_bps": 64,
"delta_bps": -27
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN PF",
"change_type": "decreased",
"old_bps": 76,
"new_bps": 56,
"delta_bps": -20
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN CP",
"change_type": "increased",
"old_bps": 72,
"new_bps": 86,
"delta_bps": 14
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL CP",
"change_type": "decreased",
"old_bps": 6481,
"new_bps": 6467,
"delta_bps": -14
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL SC",
"change_type": "decreased",
"old_bps": 36,
"new_bps": 29,
"delta_bps": -7
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN IB",
"change_type": "decreased",
"old_bps": 267,
"new_bps": 263,
"delta_bps": -4
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN SC",
"change_type": "increased",
"old_bps": 51,
"new_bps": 55,
"delta_bps": 4
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE LOCAL PF",
"change_type": "increased",
"old_bps": 17,
"new_bps": 20,
"delta_bps": 3
},
{
"ticker_code": "AADI",
"entity_name": "KSEI AGGREGATE FOREIGN ID",
"change_type": "decreased",
"old_bps": 3,
"new_bps": 2,
"delta_bps": -1
}
]
--- stderr

View file

@ -0,0 +1,32 @@
exit: 0
--- stdout
┌────────┬───────────────────────────┬─────────────────────┬────────┬────────┬────────┐
│ TICKER ┆ ENTITY ┆ CHANGE ┆ OLD% ┆ NEW% ┆ DELTA% │
╞════════╪═══════════════════════════╪═════════════════════╪════════╪════════╪════════╡
│ AADI ┆ KSEI AGGREGATE LOCAL ID ┆ DECREASED ┆ 24.00% ┆ 23.34% ┆ -0.66% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE LOCAL IS ┆ INCREASED ┆ 0.75% ┆ 1.29% ┆ +0.54% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE LOCAL MF ┆ INCREASED ┆ 1.17% ┆ 1.52% ┆ +0.35% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN MF ┆ INCREASED ┆ 3.07% ┆ 3.37% ┆ +0.30% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN OT ┆ DECREASED ┆ 0.91% ┆ 0.64% ┆ -0.27% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN PF ┆ DECREASED ┆ 0.76% ┆ 0.56% ┆ -0.20% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN CP ┆ INCREASED ┆ 0.72% ┆ 0.86% ┆ +0.14% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE LOCAL CP ┆ DECREASED ┆ 64.81% ┆ 64.67% ┆ -0.14% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE LOCAL SC ┆ DECREASED ┆ 0.36% ┆ 0.29% ┆ -0.07% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN IB ┆ DECREASED ┆ 2.67% ┆ 2.63% ┆ -0.04% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN SC ┆ INCREASED ┆ 0.51% ┆ 0.55% ┆ +0.04% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE LOCAL PF ┆ INCREASED ┆ 0.17% ┆ 0.20% ┆ +0.03% │
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ AADI ┆ KSEI AGGREGATE FOREIGN ID ┆ DECREASED ┆ 0.03% ┆ 0.02% ┆ -0.01% │
└────────┴───────────────────────────┴─────────────────────┴────────┴────────┴────────┘
--- stderr

View file

@ -0,0 +1,45 @@
exit: 0
--- stdout
[
[
"AALI",
{
"top1_bps": 7968,
"top3_bps": 7968,
"hhi": 6348,
"free_float_bps": 2032,
"holder_count": 1
}
],
[
"BBCA",
{
"top1_bps": 5494,
"top3_bps": 5716,
"hhi": 3021,
"free_float_bps": 4182,
"holder_count": 4
}
],
[
"AADI",
{
"top1_bps": 4110,
"top3_bps": 6230,
"hhi": 2009,
"free_float_bps": 2030,
"holder_count": 10
}
],
[
"ABBA",
{
"top1_bps": 283,
"top3_bps": 418,
"hhi": 9,
"free_float_bps": 9582,
"holder_count": 2
}
]
]
--- stderr

View file

@ -0,0 +1,14 @@
exit: 0
--- stdout
┌──────┬────────┬────────┬────────┬──────┬─────────────┬─────────┐
│ RANK ┆ TICKER ┆ TOP1% ┆ TOP3% ┆ HHI ┆ FREE_FLOAT% ┆ HOLDERS │
╞══════╪════════╪════════╪════════╪══════╪═════════════╪═════════╡
│ 1 ┆ AALI ┆ 79.68% ┆ 79.68% ┆ 6348 ┆ 20.32% ┆ 1 │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 2 ┆ BBCA ┆ 54.94% ┆ 57.16% ┆ 3021 ┆ 41.82% ┆ 4 │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 3 ┆ AADI ┆ 41.10% ┆ 62.30% ┆ 2009 ┆ 20.30% ┆ 10 │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┤
│ 4 ┆ ABBA ┆ 2.83% ┆ 4.18% ┆ 9 ┆ 95.82% ┆ 2 │
└──────┴────────┴────────┴────────┴──────┴─────────────┴─────────┘
--- stderr

View file

@ -0,0 +1,175 @@
exit: 0
--- stdout
[
{
"entity": {
"id": 11,
"canonical_name": "ASTRA INTERNATIONAL",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 7968
},
{
"entity": {
"id": 14,
"canonical_name": "DWIMURIA INVESTAMA ANDALAN",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 5494
},
{
"entity": {
"id": 4,
"canonical_name": "ADARO STRATEGIC INVESTMENTS",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 4110
},
{
"entity": {
"id": 5,
"canonical_name": "ALAMTRI RESOURCES INDONESIA TBK PT",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 1537
},
{
"entity": {
"id": 7,
"canonical_name": "GARIBALDI THOHIR",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 583
},
{
"entity": {
"id": 2,
"canonical_name": "SARATOGA INVESTAMA SEDAYA",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 438
},
{
"entity": {
"id": 9,
"canonical_name": "EDWIN SOERYADJAYA",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 352
},
{
"entity": {
"id": 8,
"canonical_name": "IR. T. PERMADI RACHMAT",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 348
},
{
"entity": {
"id": 13,
"canonical_name": "YAYASAN ABDI BANGSA",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 283
},
{
"entity": {
"id": 10,
"canonical_name": "SANDIAGA SALAHUDDIN UNO",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 204
},
{
"entity": {
"id": 1,
"canonical_name": "PERSADA CAPITAL INVESTAMA",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 154
},
{
"entity": {
"id": 12,
"canonical_name": "YAYASAN BINA SEJAHTERA WARGA BULOG",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 135
},
{
"entity": {
"id": 6,
"canonical_name": "FTAG INVESTMENT BANK",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 129
},
{
"entity": {
"id": 17,
"canonical_name": "ANTHONI SALIM",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 115
},
{
"entity": {
"id": 3,
"canonical_name": "TRINUGRAHA THOHIR",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 115
},
{
"entity": {
"id": 16,
"canonical_name": "TRICIPTA MANDHALA GUMILANG",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 107
},
{
"entity": {
"id": 15,
"canonical_name": "CATURGUWIRATNA SUMAPALA",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"total_bps": 102
}
]
--- stderr

View file

@ -0,0 +1,40 @@
exit: 0
--- stdout
┌──────┬────────────────────────────────────┬─────────┬───────────┐
│ RANK ┆ ENTITY ┆ TICKERS ┆ TOTAL_BPS │
╞══════╪════════════════════════════════════╪═════════╪═══════════╡
│ 1 ┆ ASTRA INTERNATIONAL ┆ 1 ┆ 79.68% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 2 ┆ DWIMURIA INVESTAMA ANDALAN ┆ 1 ┆ 54.94% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 3 ┆ ADARO STRATEGIC INVESTMENTS ┆ 1 ┆ 41.10% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 4 ┆ ALAMTRI RESOURCES INDONESIA TBK PT ┆ 1 ┆ 15.37% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 5 ┆ GARIBALDI THOHIR ┆ 1 ┆ 5.83% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 6 ┆ SARATOGA INVESTAMA SEDAYA ┆ 1 ┆ 4.38% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 7 ┆ EDWIN SOERYADJAYA ┆ 1 ┆ 3.52% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 8 ┆ IR. T. PERMADI RACHMAT ┆ 1 ┆ 3.48% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 9 ┆ YAYASAN ABDI BANGSA ┆ 1 ┆ 2.83% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 10 ┆ SANDIAGA SALAHUDDIN UNO ┆ 1 ┆ 2.04% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 11 ┆ PERSADA CAPITAL INVESTAMA ┆ 1 ┆ 1.54% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 12 ┆ YAYASAN BINA SEJAHTERA WARGA BULOG ┆ 1 ┆ 1.35% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 13 ┆ FTAG INVESTMENT BANK ┆ 1 ┆ 1.29% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 14 ┆ ANTHONI SALIM ┆ 1 ┆ 1.15% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 15 ┆ TRINUGRAHA THOHIR ┆ 1 ┆ 1.15% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 16 ┆ TRICIPTA MANDHALA GUMILANG ┆ 1 ┆ 1.07% │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
│ 17 ┆ CATURGUWIRATNA SUMAPALA ┆ 1 ┆ 1.02% │
└──────┴────────────────────────────────────┴─────────┴───────────┘
--- stderr

View file

@ -0,0 +1,26 @@
exit: 0
--- stdout
{
"entity": {
"id": 17,
"canonical_name": "ANTHONI SALIM",
"entity_type": null,
"country": null
},
"ticker_count": 1,
"holdings": [
{
"ticker": {
"id": 4,
"code": "BBCA",
"name": "BANK CENTRAL ASIA Tbk",
"sector": null
},
"source": "ksei",
"shares": 1416306835,
"percentage_bps": 115,
"report_date": "2026-08-31"
}
]
}
--- stderr

View file

@ -0,0 +1,10 @@
exit: 0
--- stdout
Entity: ANTHONI SALIM
┌────────┬────────┬───────────────┬───────┬─────────────┐
│ TICKER ┆ SOURCE ┆ SHARES ┆ % ┆ REPORT_DATE │
╞════════╪════════╪═══════════════╪═══════╪═════════════╡
│ BBCA ┆ KSEI ┆ 1,416,306,835 ┆ 1.15% ┆ 2026-08-31 │
└────────┴────────┴───────────────┴───────┴─────────────┘
Total tickers: 1
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
null
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
No institutional flow data available for BBCA.
--- stderr

View file

@ -0,0 +1,58 @@
exit: 0
--- stdout
{
"nodes": [
{
"id": "entity:14",
"label": "DWIMURIA INVESTAMA ANDALAN",
"node_type": "entity"
},
{
"id": "entity:15",
"label": "CATURGUWIRATNA SUMAPALA",
"node_type": "entity"
},
{
"id": "entity:16",
"label": "TRICIPTA MANDHALA GUMILANG",
"node_type": "entity"
},
{
"id": "entity:17",
"label": "ANTHONI SALIM",
"node_type": "entity"
},
{
"id": "ticker:BBCA",
"label": "BBCA",
"node_type": "ticker"
}
],
"edges": [
{
"from": "entity:17",
"to": "ticker:BBCA",
"percentage_bps": 115,
"source": "ksei"
},
{
"from": "entity:15",
"to": "ticker:BBCA",
"percentage_bps": 102,
"source": "ksei"
},
{
"from": "entity:14",
"to": "ticker:BBCA",
"percentage_bps": 5494,
"source": "ksei"
},
{
"from": "entity:16",
"to": "ticker:BBCA",
"percentage_bps": 107,
"source": "ksei"
}
]
}
--- stderr

View file

@ -0,0 +1,20 @@
exit: 0
--- stdout
digraph ownership {
rankdir=LR;
graph [fontname="Helvetica"];
node [fontname="Helvetica"];
edge [fontname="Helvetica"];
"entity:14" [label="DWIMURIA INVESTAMA ANDALAN", shape=ellipse];
"entity:15" [label="CATURGUWIRATNA SUMAPALA", shape=ellipse];
"entity:16" [label="TRICIPTA MANDHALA GUMILANG", shape=ellipse];
"entity:17" [label="ANTHONI SALIM", shape=ellipse];
"ticker:BBCA" [label="BBCA", shape=box];
"entity:17" -> "ticker:BBCA" [label="1.15% (ksei)"];
"entity:15" -> "ticker:BBCA" [label="1.02% (ksei)"];
"entity:14" -> "ticker:BBCA" [label="54.94% (ksei)"];
"entity:16" -> "ticker:BBCA" [label="1.07% (ksei)"];
}
--- stderr

View file

@ -0,0 +1,58 @@
exit: 0
--- stdout
{
"nodes": [
{
"id": "entity:14",
"label": "DWIMURIA INVESTAMA ANDALAN",
"node_type": "entity"
},
{
"id": "entity:15",
"label": "CATURGUWIRATNA SUMAPALA",
"node_type": "entity"
},
{
"id": "entity:16",
"label": "TRICIPTA MANDHALA GUMILANG",
"node_type": "entity"
},
{
"id": "entity:17",
"label": "ANTHONI SALIM",
"node_type": "entity"
},
{
"id": "ticker:BBCA",
"label": "BBCA",
"node_type": "ticker"
}
],
"edges": [
{
"from": "entity:17",
"to": "ticker:BBCA",
"percentage_bps": 115,
"source": "ksei"
},
{
"from": "entity:15",
"to": "ticker:BBCA",
"percentage_bps": 102,
"source": "ksei"
},
{
"from": "entity:14",
"to": "ticker:BBCA",
"percentage_bps": 5494,
"source": "ksei"
},
{
"from": "entity:16",
"to": "ticker:BBCA",
"percentage_bps": 107,
"source": "ksei"
}
]
}
--- stderr

View file

@ -0,0 +1,10 @@
exit: 0
--- stdout
nodes: 5 edges: 4
BBCA [TICKER]
├─ DWIMURIA INVESTAMA ANDALAN (54.94%, ksei)
├─ ANTHONI SALIM (1.15%, ksei)
├─ TRICIPTA MANDHALA GUMILANG (1.07%, ksei)
└─ CATURGUWIRATNA SUMAPALA (1.02%, ksei)
--- stderr

View file

@ -0,0 +1,11 @@
exit: 0
--- stdout
{
"action": "imported",
"inserted_rows": 18,
"ticker_count": 1,
"as_of_date": "2026-02-27",
"sha256": "7b8da1d820f67062410c48988bb6c70fec2088c24961d481d1c0fce8b9372547",
"source_url": null
}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
Imported 18 rows for 1 tickers (as of 2026-02-27).
--- stderr

View file

@ -0,0 +1,11 @@
exit: 0
--- stdout
{
"action": "imported",
"inserted_rows": 17,
"ticker_count": 4,
"as_of_date": "2026-08-31",
"sha256": "7c1a4c209b8ce5bde9112436a0c64d66b042915995b584d2bbf7ca7b15ce81e3",
"source_url": null
}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
Imported 17 rows for 4 tickers (as of 2026-08-31).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[]
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
No ownership releases imported yet.
--- stderr

View file

@ -0,0 +1,29 @@
exit: 0
--- stdout
[
{
"id": 1,
"source_url": null,
"sha256": "7c1a4c209b8ce5bde9112436a0c64d66b042915995b584d2bbf7ca7b15ce81e3",
"as_of_date": "2026-08-31",
"row_count": 17,
"imported_at": 0
},
{
"id": 3,
"source_url": null,
"sha256": "7b8da1d820f67062410c48988bb6c70fec2088c24961d481d1c0fce8b9372547",
"as_of_date": "2026-02-27",
"row_count": 18,
"imported_at": 0
},
{
"id": 2,
"source_url": null,
"sha256": "efb3d8fb19c9e0d803dc1b1734c8389356605d0f78d73db6729fb76d3d75753d",
"as_of_date": "2026-01-30",
"row_count": 18,
"imported_at": 0
}
]
--- stderr

View file

@ -0,0 +1,12 @@
exit: 0
--- stdout
┌────────────┬──────┬──────────────────────────────────────────────────────────────────┬────────┬─────────────┐
│ AS_OF ┆ ROWS ┆ SHA256 ┆ SOURCE ┆ IMPORTED_AT │
╞════════════╪══════╪══════════════════════════════════════════════════════════════════╪════════╪═════════════╡
│ 2026-08-31 ┆ 17 ┆ 7c1a4c209b8ce5bde9112436a0c64d66b042915995b584d2bbf7ca7b15ce81e3 ┆ - ┆ 0 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-02-27 ┆ 18 ┆ 7b8da1d820f67062410c48988bb6c70fec2088c24961d481d1c0fce8b9372547 ┆ - ┆ 0 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2026-01-30 ┆ 18 ┆ efb3d8fb19c9e0d803dc1b1734c8389356605d0f78d73db6729fb76d3d75753d ┆ - ┆ 0 │
└────────────┴──────┴──────────────────────────────────────────────────────────────────┴────────┴─────────────┘
--- stderr

View file

@ -0,0 +1,8 @@
exit: 0
--- stdout
{
"action": "mapped",
"alias": "SALIM ANTHONI",
"entity": "ANTHONI SALIM"
}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
Mapped alias 'SALIM ANTHONI' -> 'ANTHONI SALIM'.
--- stderr

View file

@ -0,0 +1,11 @@
exit: 0
--- stdout
{
"action": "merged",
"alias_updates": 1,
"bing_holdings_updates": 0,
"keep": 1,
"ksei_holdings_updates": 1,
"merge": 2
}
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
Merged entity 2 into 1 (aliases: 1, ksei_holdings: 1, bing_holdings: 0).
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[]
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
No unresolved or low-confidence aliases found.
--- stderr

View file

@ -0,0 +1,11 @@
exit: 0
--- stdout
[
{
"id": 17,
"canonical_name": "ANTHONI SALIM",
"entity_type": null,
"country": null
}
]
--- stderr

View file

@ -0,0 +1,8 @@
exit: 0
--- stdout
┌───────────────┬──────┬─────────┐
│ ENTITY ┆ TYPE ┆ COUNTRY │
╞═══════════════╪══════╪═════════╡
│ ANTHONI SALIM ┆ - ┆ - │
└───────────────┴──────┴─────────┘
--- stderr

View file

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

View file

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

View file

@ -0,0 +1,67 @@
exit: 0
--- stdout
{
"ticker": {
"id": 4,
"code": "BBCA",
"name": "BANK CENTRAL ASIA Tbk",
"sector": null
},
"ksei_as_of": "2026-08-31",
"bing_as_of": null,
"holders": [
{
"rank": 1,
"source": "ksei",
"name": "DWIMURIA INVESTAMA ANDALAN",
"entity_id": 14,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 67729950000,
"percentage_bps": 5494,
"signal": null
},
{
"rank": 2,
"source": "ksei",
"name": "ANTHONI SALIM",
"entity_id": 17,
"investor_type": "INDIVIDUAL",
"locality": "Local",
"shares": 1416306835,
"percentage_bps": 115,
"signal": null
},
{
"rank": 3,
"source": "ksei",
"name": "TRICIPTA MANDHALA GUMILANG",
"entity_id": 16,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 1313250000,
"percentage_bps": 107,
"signal": null
},
{
"rank": 4,
"source": "ksei",
"name": "CATURGUWIRATNA SUMAPALA",
"entity_id": 15,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 1261750000,
"percentage_bps": 102,
"signal": null
}
],
"concentration": {
"top1_bps": 5494,
"top3_bps": 5716,
"hhi": 3021,
"free_float_bps": 4182,
"holder_count": 4
},
"flow": null
}
--- stderr

View file

@ -0,0 +1,16 @@
exit: 0
--- stdout
Ownership: BBCA KSEI=2026-08-31 Bing=-
┌──────┬────────┬────────────────────────────┬────────────┬─────┬────────────────┬────────┬────────┐
│ RANK ┆ SOURCE ┆ INVESTOR ┆ TYPE ┆ L/F ┆ SHARES ┆ % ┆ SIGNAL │
╞══════╪════════╪════════════════════════════╪════════════╪═════╪════════════════╪════════╪════════╡
│ 1 ┆ KSEI ┆ DWIMURIA INVESTAMA ANDALAN ┆ CORPORATE ┆ L ┆ 67,729,950,000 ┆ 54.94% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 2 ┆ KSEI ┆ ANTHONI SALIM ┆ INDIVIDUAL ┆ L ┆ 1,416,306,835 ┆ 1.15% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 3 ┆ KSEI ┆ TRICIPTA MANDHALA GUMILANG ┆ CORPORATE ┆ L ┆ 1,313,250,000 ┆ 1.07% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 4 ┆ KSEI ┆ CATURGUWIRATNA SUMAPALA ┆ CORPORATE ┆ L ┆ 1,261,750,000 ┆ 1.02% ┆ - │
└──────┴────────┴────────────────────────────┴────────────┴─────┴────────────────┴────────┴────────┘
Concentration: top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
--- stderr

View file

@ -0,0 +1,67 @@
exit: 0
--- stdout
{
"ticker": {
"id": 4,
"code": "BBCA",
"name": "BANK CENTRAL ASIA Tbk",
"sector": null
},
"ksei_as_of": "2026-08-31",
"bing_as_of": null,
"holders": [
{
"rank": 1,
"source": "ksei",
"name": "DWIMURIA INVESTAMA ANDALAN",
"entity_id": 14,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 67729950000,
"percentage_bps": 5494,
"signal": null
},
{
"rank": 2,
"source": "ksei",
"name": "ANTHONI SALIM",
"entity_id": 17,
"investor_type": "INDIVIDUAL",
"locality": "Local",
"shares": 1416306835,
"percentage_bps": 115,
"signal": null
},
{
"rank": 3,
"source": "ksei",
"name": "TRICIPTA MANDHALA GUMILANG",
"entity_id": 16,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 1313250000,
"percentage_bps": 107,
"signal": null
},
{
"rank": 4,
"source": "ksei",
"name": "CATURGUWIRATNA SUMAPALA",
"entity_id": 15,
"investor_type": "CORPORATE",
"locality": "Local",
"shares": 1261750000,
"percentage_bps": 102,
"signal": null
}
],
"concentration": {
"top1_bps": 5494,
"top3_bps": 5716,
"hhi": 3021,
"free_float_bps": 4182,
"holder_count": 4
},
"flow": null
}
--- stderr

View file

@ -0,0 +1,16 @@
exit: 0
--- stdout
Ownership: BBCA KSEI=2026-08-31 Bing=-
┌──────┬────────┬────────────────────────────┬────────────┬─────┬────────────────┬────────┬────────┐
│ RANK ┆ SOURCE ┆ INVESTOR ┆ TYPE ┆ L/F ┆ SHARES ┆ % ┆ SIGNAL │
╞══════╪════════╪════════════════════════════╪════════════╪═════╪════════════════╪════════╪════════╡
│ 1 ┆ KSEI ┆ DWIMURIA INVESTAMA ANDALAN ┆ CORPORATE ┆ L ┆ 67,729,950,000 ┆ 54.94% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 2 ┆ KSEI ┆ ANTHONI SALIM ┆ INDIVIDUAL ┆ L ┆ 1,416,306,835 ┆ 1.15% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 3 ┆ KSEI ┆ TRICIPTA MANDHALA GUMILANG ┆ CORPORATE ┆ L ┆ 1,313,250,000 ┆ 1.07% ┆ - │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 4 ┆ KSEI ┆ CATURGUWIRATNA SUMAPALA ┆ CORPORATE ┆ L ┆ 1,261,750,000 ┆ 1.02% ┆ - │
└──────┴────────┴────────────────────────────┴────────────┴─────┴────────────────┴────────┴────────┘
Concentration: top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
--- stderr

View file

@ -0,0 +1,81 @@
exit: 0
--- stdout
[
{
"symbol": "BBCA.JK",
"growth": {
"revenue_growth": 0.081,
"earnings_growth": 0.121,
"revenue_growth_pct": 8.1,
"earnings_growth_pct": 12.1,
"revenue_signal": "slow",
"earnings_signal": "moderate",
"overall_signal": "mixed"
},
"valuation": {
"pe_trailing": 25.4,
"pe_forward": null,
"pb": 4.6,
"roe": 0.198,
"roe_pct": 19.8,
"net_margin": 0.324,
"net_margin_pct": 32.4,
"ev_ebitda": null,
"pe_signal": "premium",
"pb_signal": "expensive",
"roe_signal": "strong",
"margin_signal": "excellent",
"ev_ebitda_signal": "no data",
"overall_signal": "expensive"
},
"risk": {
"debt_to_equity": 0.42,
"current_ratio": 1.18,
"roa": 0.031,
"roa_pct": 3.1,
"de_signal": "conservative",
"current_ratio_signal": "tight",
"overall_signal": "moderate risk"
},
"overall_signal": "mixed"
},
{
"symbol": "BBRI.JK",
"growth": {
"revenue_growth": 0.081,
"earnings_growth": 0.121,
"revenue_growth_pct": 8.1,
"earnings_growth_pct": 12.1,
"revenue_signal": "slow",
"earnings_signal": "moderate",
"overall_signal": "mixed"
},
"valuation": {
"pe_trailing": 25.4,
"pe_forward": null,
"pb": 4.6,
"roe": 0.198,
"roe_pct": 19.8,
"net_margin": 0.324,
"net_margin_pct": 32.4,
"ev_ebitda": null,
"pe_signal": "premium",
"pb_signal": "expensive",
"roe_signal": "strong",
"margin_signal": "excellent",
"ev_ebitda_signal": "no data",
"overall_signal": "expensive"
},
"risk": {
"debt_to_equity": 0.42,
"current_ratio": 1.18,
"roa": 0.031,
"roa_pct": 3.1,
"de_signal": "conservative",
"current_ratio_signal": "tight",
"overall_signal": "moderate risk"
},
"overall_signal": "mixed"
}
]
--- stderr

View file

@ -0,0 +1,23 @@
exit: 0
--- stdout
Fundamental Comparison
┌────────────────┬─────────────────────────┬─────────────────────────┐
│ METRIC ┆ BBCA.JK ┆ BBRI.JK │
╞════════════════╪═════════════════════════╪═════════════════════════╡
│ Symbol ┆ BBCA.JK ┆ BBRI.JK │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Overall ┆ mixed ┆ mixed │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Growth ┆ mixed ┆ mixed │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Valuation ┆ expensive ┆ expensive │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Risk ┆ moderate risk ┆ moderate risk │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ P/E ┆ 25.40 ┆ 25.40 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ ROE ┆ +19.80% ┆ +19.80% │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Revenue Growth ┆ +8.10% ┆ +8.10% │
└────────────────┴─────────────────────────┴─────────────────────────┘
--- stderr

View file

@ -0,0 +1,22 @@
exit: 0
--- stdout
{
"symbol": "BBCA.JK",
"eps_last_year": 1200.0,
"revenue_last_year": 100000000000.0,
"forecast": [
{
"eps_actual": null,
"eps_forecast": 1300.0,
"eps_surprise": null,
"eps_surprise_pct": null,
"revenue_actual": null,
"revenue_forecast": 110000000000.0,
"revenue_surprise": null,
"earning_release_date": "2026-03-15",
"period_type": "2026"
}
],
"history": []
}
--- stderr

View file

@ -0,0 +1,10 @@
exit: 0
--- stdout
── Earnings Forecast ──
┌────────┬─────────┬─────────────────┬────────────┐
│ PERIOD ┆ EPS FC ┆ REV FC ┆ DATE │
╞════════╪═════════╪═════════════════╪════════════╡
│ FY2026 ┆ 1300.00 ┆ 110,000,000,000 ┆ 2026-03-15 │
└────────┴─────────┴─────────────────┴────────────┘
--- stderr

View file

@ -0,0 +1,22 @@
exit: 0
--- stdout
{
"symbol": "BBCA.JK",
"eps_last_year": 1200.0,
"revenue_last_year": 100000000000.0,
"forecast": [],
"history": [
{
"eps_actual": 320.0,
"eps_forecast": null,
"eps_surprise": 5.0,
"eps_surprise_pct": 1.5,
"revenue_actual": 27000000000.0,
"revenue_forecast": null,
"revenue_surprise": 200000000.0,
"earning_release_date": "2025-11-20",
"period_type": "Q42025"
}
]
}
--- stderr

View file

@ -0,0 +1,9 @@
exit: 0
--- stdout
── Earnings History ──
┌─────────┬─────────┬────────┬──────────┬───────────┬────────────────┬────────────┐
│ PERIOD ┆ EPS ACT ┆ EPS FC ┆ SURPRISE ┆ SURPRISE% ┆ REV ACT ┆ DATE │
╞═════════╪═════════╪════════╪══════════╪═══════════╪════════════════╪════════════╡
│ Q4 2025 ┆ 320.00 ┆ - ┆ 5.00 ┆ 1.50 ┆ 27,000,000,000 ┆ 2025-11-20 │
└─────────┴─────────┴────────┴──────────┴───────────┴────────────────┴────────────┘
--- stderr

View file

@ -0,0 +1,56 @@
exit: 0
--- stdout
{
"symbol": "BBCA.JK",
"eps_last_year": 1200.0,
"revenue_last_year": 100000000000.0,
"forecast": [
{
"eps_actual": null,
"eps_forecast": 1300.0,
"eps_surprise": null,
"eps_surprise_pct": null,
"revenue_actual": null,
"revenue_forecast": 110000000000.0,
"revenue_surprise": null,
"earning_release_date": "2026-03-15",
"period_type": "2026"
},
{
"eps_actual": null,
"eps_forecast": 330.0,
"eps_surprise": null,
"eps_surprise_pct": null,
"revenue_actual": null,
"revenue_forecast": 28000000000.0,
"revenue_surprise": null,
"earning_release_date": "2026-04-20",
"period_type": "Q12026"
}
],
"history": [
{
"eps_actual": 1250.0,
"eps_forecast": null,
"eps_surprise": 20.0,
"eps_surprise_pct": 1.6,
"revenue_actual": 105000000000.0,
"revenue_forecast": null,
"revenue_surprise": 500000000.0,
"earning_release_date": "2025-03-15",
"period_type": "2025"
},
{
"eps_actual": 320.0,
"eps_forecast": null,
"eps_surprise": 5.0,
"eps_surprise_pct": 1.5,
"revenue_actual": 27000000000.0,
"revenue_forecast": null,
"revenue_surprise": 200000000.0,
"earning_release_date": "2025-11-20",
"period_type": "Q42025"
}
]
}
--- stderr

View file

@ -0,0 +1,20 @@
exit: 0
--- stdout
── Earnings History ──
┌─────────┬─────────┬────────┬──────────┬───────────┬─────────────────┬────────────┐
│ PERIOD ┆ EPS ACT ┆ EPS FC ┆ SURPRISE ┆ SURPRISE% ┆ REV ACT ┆ DATE │
╞═════════╪═════════╪════════╪══════════╪═══════════╪═════════════════╪════════════╡
│ FY2025 ┆ 1250.00 ┆ - ┆ 20.00 ┆ 1.60 ┆ 105,000,000,000 ┆ 2025-03-15 │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Q4 2025 ┆ 320.00 ┆ - ┆ 5.00 ┆ 1.50 ┆ 27,000,000,000 ┆ 2025-11-20 │
└─────────┴─────────┴────────┴──────────┴───────────┴─────────────────┴────────────┘
── Earnings Forecast ──
┌─────────┬─────────┬─────────────────┬────────────┐
│ PERIOD ┆ EPS FC ┆ REV FC ┆ DATE │
╞═════════╪═════════╪═════════════════╪════════════╡
│ FY2026 ┆ 1300.00 ┆ 110,000,000,000 ┆ 2026-03-15 │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Q1 2026 ┆ 330.00 ┆ 28,000,000,000 ┆ 2026-04-20 │
└─────────┴─────────┴─────────────────┴────────────┘
--- stderr

View file

@ -0,0 +1,21 @@
exit: 0
--- stdout
{
"instrument": {
"id": "bn91jc",
"symbol": "BBCA.JK",
"name": "Bank Central Asia Tbk PT"
},
"balance_sheet": null,
"cash_flow": null,
"income_statement": {
"values": {
"netIncome": 400000000.0,
"totalRevenue": 1000000000.0
},
"currency": "IDR",
"report_date": "2025-12-31",
"end_date": "2025-12-31"
}
}
--- stderr

View file

@ -0,0 +1,12 @@
exit: 0
--- stdout
── Income Statement (2025-12-31) ──
┌───────────────┬───────────────┐
│ LINE ITEM ┆ VALUE (IDR) │
╞═══════════════╪═══════════════╡
│ Net Income ┆ 400,000,000 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Total Revenue ┆ 1,000,000,000 │
└───────────────┴───────────────┘
--- stderr

View file

@ -0,0 +1,36 @@
exit: 0
--- stdout
{
"instrument": {
"id": "bn91jc",
"symbol": "BBCA.JK",
"name": "Bank Central Asia Tbk PT"
},
"balance_sheet": {
"values": {
"cashAndShortTermInvestments": 500000000.0,
"totalEquity": 900000000.0
},
"currency": "IDR",
"report_date": "2025-12-31",
"end_date": "2025-12-31"
},
"cash_flow": {
"values": {
"operatingCashFlow": 600000000.0
},
"currency": "IDR",
"report_date": "2025-12-31",
"end_date": "2025-12-31"
},
"income_statement": {
"values": {
"netIncome": 400000000.0,
"totalRevenue": 1000000000.0
},
"currency": "IDR",
"report_date": "2025-12-31",
"end_date": "2025-12-31"
}
}
--- stderr

View file

@ -0,0 +1,28 @@
exit: 0
--- stdout
── Income Statement (2025-12-31) ──
┌───────────────┬───────────────┐
│ LINE ITEM ┆ VALUE (IDR) │
╞═══════════════╪═══════════════╡
│ Net Income ┆ 400,000,000 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Total Revenue ┆ 1,000,000,000 │
└───────────────┴───────────────┘
── Balance Sheet (2025-12-31) ──
┌─────────────────────────────────┬─────────────┐
│ LINE ITEM ┆ VALUE (IDR) │
╞═════════════════════════════════╪═════════════╡
│ Cash And Short Term Investments ┆ 500,000,000 │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Total Equity ┆ 900,000,000 │
└─────────────────────────────────┴─────────────┘
── Cash Flow (2025-12-31) ──
┌─────────────────────┬─────────────┐
│ LINE ITEM ┆ VALUE (IDR) │
╞═════════════════════╪═════════════╡
│ Operating Cash Flow ┆ 600,000,000 │
└─────────────────────┴─────────────┘
--- stderr

View file

@ -0,0 +1,41 @@
exit: 0
--- stdout
{
"symbol": "BBCA.JK",
"growth": {
"revenue_growth": 0.081,
"earnings_growth": 0.121,
"revenue_growth_pct": 8.1,
"earnings_growth_pct": 12.1,
"revenue_signal": "slow",
"earnings_signal": "moderate",
"overall_signal": "mixed"
},
"valuation": {
"pe_trailing": 25.4,
"pe_forward": null,
"pb": 4.6,
"roe": 0.198,
"roe_pct": 19.8,
"net_margin": 0.324,
"net_margin_pct": 32.4,
"ev_ebitda": null,
"pe_signal": "premium",
"pb_signal": "expensive",
"roe_signal": "strong",
"margin_signal": "excellent",
"ev_ebitda_signal": "no data",
"overall_signal": "expensive"
},
"risk": {
"debt_to_equity": 0.42,
"current_ratio": 1.18,
"roa": 0.031,
"roa_pct": 3.1,
"de_signal": "conservative",
"current_ratio_signal": "tight",
"overall_signal": "moderate risk"
},
"overall_signal": "mixed"
}
--- stderr

View file

@ -0,0 +1,49 @@
exit: 0
--- stdout
Fundamental Analysis for BBCA.JK
Growth Analysis for BBCA.JK
┌─────────────────┬─────────┬────────────────────┐
│ METRIC ┆ VALUE ┆ SIGNAL │
╞═════════════════╪═════════╪════════════════════╡
│ Revenue Growth ┆ +8.10% ┆ slow │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Earnings Growth ┆ +12.10% ┆ moderate │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Overall ┆ - ┆ mixed │
└─────────────────┴─────────┴────────────────────┘
Valuation Analysis for BBCA.JK
┌────────────────┬─────────┬─────────────────────┐
│ METRIC ┆ VALUE ┆ SIGNAL │
╞════════════════╪═════════╪═════════════════════╡
│ P/E (Trailing) ┆ 25.40 ┆ premium │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ P/E (Forward) ┆ - ┆ - │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Price/Book ┆ 4.60 ┆ expensive │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ ROE ┆ +19.80% ┆ strong │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Net Margin ┆ +32.40% ┆ excellent │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ EV/EBITDA ┆ - ┆ no data │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Overall ┆ - ┆ expensive │
└────────────────┴─────────┴─────────────────────┘
Risk Analysis for BBCA.JK
┌───────────────┬────────┬─────────────────────────┐
│ METRIC ┆ VALUE ┆ SIGNAL │
╞═══════════════╪════════╪═════════════════════════╡
│ Debt/Equity ┆ 0.42 ┆ conservative │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Current Ratio ┆ 1.18 ┆ tight │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ ROA ┆ +3.10% ┆ - │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Overall ┆ - ┆ moderate risk │
└───────────────┴────────┴─────────────────────────┘
Overall Signal: mixed
--- stderr

View file

@ -0,0 +1,12 @@
exit: 0
--- stdout
{
"revenue_growth": 0.081,
"earnings_growth": 0.121,
"revenue_growth_pct": 8.1,
"earnings_growth_pct": 12.1,
"revenue_signal": "slow",
"earnings_signal": "moderate",
"overall_signal": "mixed"
}
--- stderr

View file

@ -0,0 +1,13 @@
exit: 0
--- stdout
Growth Analysis for BBCA.JK
┌─────────────────┬─────────┬────────────────────┐
│ METRIC ┆ VALUE ┆ SIGNAL │
╞═════════════════╪═════════╪════════════════════╡
│ Revenue Growth ┆ +8.10% ┆ slow │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Earnings Growth ┆ +12.10% ┆ moderate │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ Overall ┆ - ┆ mixed │
└─────────────────┴─────────┴────────────────────┘
--- stderr

View file

@ -0,0 +1,29 @@
exit: 0
--- stdout
[
{
"date": "2026-01-14",
"open": 8000,
"high": 8000,
"low": 8000,
"close": 8000,
"volume": 0
},
{
"date": "2026-01-15",
"open": 8075,
"high": 8075,
"low": 8075,
"close": 8075,
"volume": 0
},
{
"date": "2026-01-16",
"open": 8025,
"high": 8025,
"low": 8025,
"close": 8025,
"volume": 0
}
]
--- stderr

View file

@ -0,0 +1,13 @@
exit: 0
--- stdout
History for BBCA.JK
┌────────────┬───────┬───────┬───────┬───────┬────────┐
│ DATE ┆ OPEN ┆ HIGH ┆ LOW ┆ CLOSE ┆ VOLUME │
╞════════════╪═══════╪═══════╪═══════╪═══════╪════════╡
│ 2026-01-14 ┆ 8,000 ┆ 8,000 ┆ 8,000 ┆ 8,000 ┆ 0 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 2026-01-15 ┆ 8,075 ┆ 8,075 ┆ 8,075 ┆ 8,075 ┆ 0 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ 2026-01-16 ┆ 8,025 ┆ 8,025 ┆ 8,025 ┆ 8,025 ┆ 0 │
└────────────┴───────┴───────┴───────┴───────┴────────┘
--- stderr

View file

@ -0,0 +1,29 @@
exit: 0
--- stdout
[
{
"date": "2024-03-01",
"open": 9800,
"high": 9900,
"low": 9750,
"close": 9875,
"volume": 12300000
},
{
"date": "2024-03-02",
"open": 9850,
"high": 9920,
"low": 9800,
"close": 9880,
"volume": 11000000
},
{
"date": "2024-03-03",
"open": 9860,
"high": 9950,
"low": 9820,
"close": 9925,
"volume": 14000000
}
]
--- stderr

View file

@ -0,0 +1,14 @@
exit: 0
--- stdout
History for BBCA.JK
┌────────────┬───────┬───────┬───────┬───────┬────────────┐
│ DATE ┆ OPEN ┆ HIGH ┆ LOW ┆ CLOSE ┆ VOLUME │
╞════════════╪═══════╪═══════╪═══════╪═══════╪════════════╡
│ 2024-03-01 ┆ 9,800 ┆ 9,900 ┆ 9,750 ┆ 9,875 ┆ 12,300,000 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-03-02 ┆ 9,850 ┆ 9,920 ┆ 9,800 ┆ 9,880 ┆ 11,000,000 │
├╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2024-03-03 ┆ 9,860 ┆ 9,950 ┆ 9,820 ┆ 9,925 ┆ 14,000,000 │
└────────────┴───────┴───────┴───────┴───────┴────────────┘
--- stderr
info: history provider fallback active (msn -> yahoo)

View file

@ -0,0 +1,16 @@
exit: 0
--- stdout
{
"id": "bn91jc",
"symbol": "BBCA.JK",
"summary": "Mixed analyst signals for Bank Central Asia Tbk PT: 1 positive, 1 negative, 1 neutral.",
"highlights": [
"P/E ratio: Earnings data unavailable, can't compare P/E ratio with peers",
"Analyst price target: Analysts forecast more than 20% upside for 12-month price target"
],
"risks": [
"Quarterly Revenue YoY Growth: Revenue grew worse than peers"
],
"last_updated": "2026-03-26T04:14:57.9197955Z"
}
--- stderr

View file

@ -0,0 +1,10 @@
exit: 0
--- stdout
Mixed analyst signals for Bank Central Asia Tbk PT: 1 positive, 1 negative, 1 neutral.
Last updated: 2026-03-26T04:14:57.9197955Z
Highlights:
- P/E ratio: Earnings data unavailable, can't compare P/E ratio with peers
- Analyst price target: Analysts forecast more than 20% upside for 12-month price target
Risks:
- Quarterly Revenue YoY Growth: Revenue grew worse than peers
--- stderr

View file

@ -0,0 +1,15 @@
exit: 0
--- stdout
[
{
"id": "news-1",
"symbol": "BBCA.JK",
"title": "BCA reports steady growth",
"url": "https://example.com/news/bca-growth",
"description": "BCA delivered steady quarterly growth across lending and deposits.",
"provider": "Contoso News",
"published_at": "2026-03-20T10:00:00Z",
"read_time_min": 3
}
]
--- stderr

Some files were not shown because too many files have changed in this diff Show more