mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat(cli): agent-native output, stdin symbols, and exit-code classes
Phase 1 of docs/DESIGN.md: - Output format resolves -o > IDX_OUTPUT > general.output > auto; auto is a table on a terminal and JSON when stdout is piped or captured. `config init` now writes output = "auto". - JSON is pretty only on a terminal and compact otherwise. New `-o jsonl` prints one array item per line with the same key order. - No ANSI color when stdout is not a terminal (direct owo-colors calls now go through output::style). - `stocks quote -` and `stocks compare -` read symbols from stdin. - Exit codes by class: 2 invalid input/config, 3 not found or cache miss, 4 upstream unavailable, 5 unsupported, 1 other. Golden changes are limited to those documented kinds and were verified mechanically: JSON goldens are semantically identical, tables differ only by removed ANSI codes and the column padding comfy-table had reserved for them (cell contents identical), exit codes match each error's class, and help/completions only gain the new --output values. 17 new golden cases cover auto, jsonl, stdin, and the new errors. tests/cli.rs now pins IDX_OUTPUT=table in its harness (its tests assert table text and run piped) and parses discover JSON instead of matching pretty-printed whitespace. Live A/B against main: 16/16 stocks cases identical; ownership discover got IDX 403 on both binaries, now exiting 4 instead of 1 as intended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fe627496dd
commit
22b6dabaeb
119 changed files with 920 additions and 1896 deletions
|
|
@ -32,7 +32,7 @@ clap = { version = "4", features = ["derive"] }
|
|||
clap_complete = "4"
|
||||
ureq = { version = "3", features = ["json", "cookies"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_json = { version = "1", features = ["raw_value"] }
|
||||
comfy-table = "7"
|
||||
owo-colors = "4"
|
||||
toml = "0.8"
|
||||
|
|
|
|||
26
README.md
26
README.md
|
|
@ -112,7 +112,11 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31
|
|||
|
||||
### Output
|
||||
|
||||
- Human-readable tables on stdout, JSON with `-o json`, errors on stderr
|
||||
- Tables when you run idx in a terminal; compact JSON when its output is piped
|
||||
or captured, so scripts and AI agents get structured data with no flag
|
||||
- `-o table|json|jsonl` (or `IDX_OUTPUT`, or `general.output` in the config)
|
||||
overrides that; `jsonl` prints one array item per line
|
||||
- Errors go to stderr, as one JSON object in JSON modes
|
||||
|
||||
## Configuration
|
||||
|
||||
|
|
@ -133,12 +137,26 @@ Precedence order:
|
|||
3. Config file
|
||||
4. Built-in defaults
|
||||
|
||||
## Agent-friendly usage
|
||||
## Scripting and agents
|
||||
|
||||
- Use `idx --help` and subcommand help for discoverability
|
||||
- Use `-o json` / `--output json` for structured output
|
||||
idx composes with `jq`, Python, and other shell tools:
|
||||
|
||||
```bash
|
||||
idx stocks quote BBCA BBRI | jq -r '.[] | "\(.symbol) \(.price)"'
|
||||
idx -o jsonl stocks history BBCA --period 1y | tail -5
|
||||
idx stocks screen --filter low-pe | jq -r '.[].symbol' | idx stocks compare -
|
||||
```
|
||||
|
||||
- stdout carries only data; warnings and errors go to stderr
|
||||
- `-` reads symbols from stdin for `stocks quote` and `stocks compare`
|
||||
- No color codes when output is not a terminal
|
||||
- Exit codes: `0` success, `2` invalid input or config, `3` not found or no
|
||||
cached data, `4` upstream unavailable (retry later), `5` unsupported, `1` other
|
||||
- `idx completions <shell>` generates shell completions
|
||||
|
||||
If you ran `idx config init` before v0.3, your config pins `output = "table"`;
|
||||
change it to `auto` with `idx config set general.output auto`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/INSTALL.md](docs/INSTALL.md): install script options
|
||||
|
|
|
|||
|
|
@ -144,11 +144,21 @@ CLI flags > environment variables > config file > defaults
|
|||
## Error Strategy
|
||||
- `IdxError` enum (thiserror) with structured error codes
|
||||
- Table mode: human-readable error on stderr
|
||||
- JSON mode: `{"error": true, "code": "...", "message": "..."}`
|
||||
- Exit code 0 on success, non-zero on failure
|
||||
- JSON and JSONL modes: `{"error": true, "code": "...", "message": "..."}` on stderr, stdout empty
|
||||
- Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage
|
||||
errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/
|
||||
`PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`, `5` `UNSUPPORTED`, `1` everything else
|
||||
- A totally failed `stocks compare` emits one structured error in JSON mode; per-symbol warnings are retained for partial success, not printed ahead of a total-failure error.
|
||||
|
||||
## CLI Output and Offline Cache
|
||||
- Output format resolves as `-o` flag > `IDX_OUTPUT` > `general.output` > `auto`.
|
||||
`auto` is a table when stdout is a terminal and JSON otherwise
|
||||
(`std::io::IsTerminal`).
|
||||
- JSON is pretty-printed only on a terminal; piped output is compact. `jsonl`
|
||||
prints each array item on its own line with the same key order as `json`,
|
||||
and a single object as one line.
|
||||
- ANSI styling goes through `output::style` and is disabled when stdout is not a
|
||||
terminal or color is turned off.
|
||||
- `version`, `cache info`, `cache clear`, and `ownership resolve map|merge` emit
|
||||
JSON objects when JSON output is selected.
|
||||
- `stocks quote` and `stocks compare` require at least one symbol.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# System Design: idx-core and its frontends
|
||||
|
||||
**Status:** Accepted 2026-10-14. Implementation on `refactor/idx-core`; nothing implemented yet.
|
||||
**Status:** Accepted 2026-10-14. Implementation on `refactor/idx-core`; phases 0-1 done.
|
||||
|
||||
## Goal
|
||||
|
||||
|
|
@ -164,8 +164,8 @@ is another 20-30% smaller than pretty JSON.
|
|||
Already true.
|
||||
- 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
|
||||
whitespace-separated:
|
||||
- `stocks quote` and `stocks compare` accept `-` to read symbols from stdin,
|
||||
separated by whitespace or commas:
|
||||
`idx stocks screen --filter low-pe | jq -r '.[].symbol' | idx stocks compare -`
|
||||
- Exit codes by error class (today every error exits `1`):
|
||||
|
||||
|
|
@ -175,7 +175,7 @@ is another 20-30% smaller than pretty JSON.
|
|||
| 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... |
|
||||
| 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` |
|
||||
| 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` |
|
||||
| 4 | upstream unavailable, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE` |
|
||||
| 4 | upstream unavailable, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR` |
|
||||
| 5 | unsupported | `UNSUPPORTED` |
|
||||
|
||||
- JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ pub fn handle(cmd: &CacheCmd, format: &OutputFormat) -> Result<(), IdxError> {
|
|||
match &cmd.command {
|
||||
CacheSubcommand::Info => {
|
||||
let info = cache.info()?;
|
||||
if matches!(format, OutputFormat::Json) {
|
||||
if format.is_json() {
|
||||
return json::print_json(&serde_json::json!({
|
||||
"path": info.path,
|
||||
"files": info.files,
|
||||
|
|
@ -52,7 +52,7 @@ pub fn handle(cmd: &CacheCmd, format: &OutputFormat) -> Result<(), IdxError> {
|
|||
}
|
||||
CacheSubcommand::Clear => {
|
||||
let (removed, failed) = cache.clear()?;
|
||||
if matches!(format, OutputFormat::Json) {
|
||||
if format.is_json() {
|
||||
return json::print_json(&serde_json::json!({
|
||||
"removed": removed,
|
||||
"failed": failed,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub mod stocks;
|
|||
|
||||
use clap::{Parser, Subcommand, ValueEnum};
|
||||
|
||||
use crate::output::OutputFormat;
|
||||
use crate::output::OutputSetting;
|
||||
|
||||
#[derive(Debug, Clone, ValueEnum)]
|
||||
pub enum Shell {
|
||||
|
|
@ -23,8 +23,14 @@ pub enum Shell {
|
|||
long_about = "idx-cli is a Rust command-line tool for Indonesian stock market (IDX) analysis.\nIt supports quote lookup, historical data, local caching, and output formats for both humans and AI agents."
|
||||
)]
|
||||
pub struct Cli {
|
||||
#[arg(short, long, value_enum, global = true, help = "Output format")]
|
||||
pub output: Option<OutputFormat>,
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_enum,
|
||||
global = true,
|
||||
help = "Output format [default: auto = table on a terminal, json when piped]"
|
||||
)]
|
||||
pub output: Option<OutputSetting>,
|
||||
#[arg(long, global = true, help = "Disable colored output")]
|
||||
pub no_color: bool,
|
||||
#[arg(short, long, global = true, help = "Suppress non-essential output")]
|
||||
|
|
|
|||
|
|
@ -2,16 +2,15 @@ use std::collections::HashSet;
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::output::style::Paint;
|
||||
use chrono::Utc;
|
||||
use clap::{Args, Subcommand};
|
||||
use comfy_table::{Cell, ContentArrangement, Table, presets::UTF8_FULL};
|
||||
use owo_colors::OwoColorize;
|
||||
use serde::Serialize;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::config::IdxConfig;
|
||||
use crate::error::IdxError;
|
||||
use crate::output::OutputFormat;
|
||||
use crate::output::json;
|
||||
use crate::output::table::format_idr;
|
||||
use crate::ownership::types::{
|
||||
|
|
@ -219,7 +218,7 @@ fn handle_sync(args: &SyncArgs, config: &IdxConfig) -> Result<(), IdxError> {
|
|||
let db_path = db::db_path(config)?;
|
||||
let result = snapshot::sync_snapshot(&manifest_source, &db_path, args.force)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&result);
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +268,7 @@ fn handle_discover(args: &DiscoverArgs, config: &IdxConfig) -> Result<(), IdxErr
|
|||
let family = parse_discovery_family(&args.family)?;
|
||||
let reports = remote::discover_idx_ownership_reports(family, args.limit)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&reports);
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +336,7 @@ fn handle_ticker(args: &TickerArgs, config: &IdxConfig) -> Result<(), IdxError>
|
|||
)));
|
||||
}
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&data);
|
||||
}
|
||||
|
||||
|
|
@ -407,7 +406,7 @@ fn handle_entity(args: &EntityArgs, config: &IdxConfig) -> Result<(), IdxError>
|
|||
|
||||
let data = db::query_entity_holdings(&conn, entity.0)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&data);
|
||||
}
|
||||
|
||||
|
|
@ -440,7 +439,7 @@ fn handle_search(args: &SearchArgs, config: &IdxConfig) -> Result<(), IdxError>
|
|||
let conn = db::open_db(config)?;
|
||||
let entities = search::fts_search(&conn, &args.query, args.limit)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&entities);
|
||||
}
|
||||
|
||||
|
|
@ -475,7 +474,7 @@ fn handle_cross_holders(args: &CrossHolderArgs, config: &IdxConfig) -> Result<()
|
|||
let conn = db::open_db(config)?;
|
||||
let rows = db::query_cross_holders(&conn, args.min_tickers, args.top)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&rows);
|
||||
}
|
||||
|
||||
|
|
@ -509,7 +508,7 @@ fn handle_concentration(args: &ConcentrationArgs, config: &IdxConfig) -> Result<
|
|||
|
||||
let rows = db::query_concentration(&conn, &by, args.top)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&rows);
|
||||
}
|
||||
|
||||
|
|
@ -551,14 +550,14 @@ fn handle_flow(args: &FlowArgs, config: &IdxConfig) -> Result<(), IdxError> {
|
|||
|
||||
let flow = db::query_bing_flow(&conn, ticker_id)?;
|
||||
let Some(flow) = flow else {
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&Option::<crate::ownership::types::InstitutionalFlow>::None);
|
||||
}
|
||||
println!("No institutional flow data available for {symbol}.");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&flow);
|
||||
}
|
||||
|
||||
|
|
@ -599,7 +598,7 @@ fn handle_graph(args: &GraphArgs, config: &IdxConfig) -> Result<(), IdxError> {
|
|||
let conn = db::open_db(config)?;
|
||||
let (nodes, edges) = graph::query_ownership_graph(&conn, &args.root, args.depth)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
#[derive(Serialize)]
|
||||
struct GraphOutput<'a> {
|
||||
nodes: &'a [crate::ownership::types::GraphNode],
|
||||
|
|
@ -630,7 +629,7 @@ fn handle_changes(args: &ChangesArgs, config: &IdxConfig) -> Result<(), IdxError
|
|||
let conn = db::open_db(config)?;
|
||||
let changes = db::query_changes(&conn, &args.from, &args.to)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&changes);
|
||||
}
|
||||
|
||||
|
|
@ -679,7 +678,7 @@ fn handle_resolve(args: &ResolveArgs, config: &IdxConfig) -> Result<(), IdxError
|
|||
ResolveCommand::Unresolved { limit } => {
|
||||
let rows = db::list_unresolved(&conn, *limit)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&rows);
|
||||
}
|
||||
|
||||
|
|
@ -718,7 +717,7 @@ fn handle_resolve(args: &ResolveArgs, config: &IdxConfig) -> Result<(), IdxError
|
|||
}
|
||||
ResolveCommand::Map { alias, entity } => {
|
||||
db::manual_map(&conn, alias, entity)?;
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&serde_json::json!({
|
||||
"action": "mapped",
|
||||
"alias": alias,
|
||||
|
|
@ -753,7 +752,7 @@ fn handle_resolve(args: &ResolveArgs, config: &IdxConfig) -> Result<(), IdxError
|
|||
|
||||
db::merge_entities(&conn, *keep, *merge)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&serde_json::json!({
|
||||
"action": "merged",
|
||||
"keep": keep,
|
||||
|
|
@ -813,7 +812,7 @@ fn handle_import(args: &ImportArgs, config: &IdxConfig) -> Result<(), IdxError>
|
|||
source_url: import_input.source_url,
|
||||
};
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&result);
|
||||
}
|
||||
|
||||
|
|
@ -897,7 +896,7 @@ fn handle_import(args: &ImportArgs, config: &IdxConfig) -> Result<(), IdxError>
|
|||
source_url: release.source_url.clone(),
|
||||
};
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&result);
|
||||
}
|
||||
|
||||
|
|
@ -915,7 +914,7 @@ fn handle_releases(config: &IdxConfig) -> Result<(), IdxError> {
|
|||
let conn = db::open_db(config)?;
|
||||
let releases = db::query_releases(&conn)?;
|
||||
|
||||
if matches!(config.output, OutputFormat::Json) {
|
||||
if config.output.is_json() {
|
||||
return json::print_json(&releases);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::io::Read;
|
||||
|
||||
use clap::{Args, Subcommand, ValueEnum};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
|
|
@ -50,6 +51,34 @@ const SCREENER_FILTERS: &[&str] = &[
|
|||
|
||||
const SCREENER_REGIONS: &[&str] = &["id", "us", "sg", "hk", "jp"];
|
||||
|
||||
/// Splits comma-separated symbol arguments; `-` reads symbols from stdin,
|
||||
/// separated by whitespace or commas.
|
||||
fn expand_symbols(args: &[String]) -> Result<Vec<String>, IdxError> {
|
||||
let mut symbols = Vec::new();
|
||||
for arg in args {
|
||||
if arg == "-" {
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut input)
|
||||
.map_err(|e| IdxError::Io(e.to_string()))?;
|
||||
let from_stdin: Vec<String> = input
|
||||
.split(|c: char| c.is_whitespace() || c == ',')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(String::from)
|
||||
.collect();
|
||||
if from_stdin.is_empty() {
|
||||
return Err(IdxError::InvalidInput(
|
||||
"no symbols read from stdin".to_string(),
|
||||
));
|
||||
}
|
||||
symbols.extend(from_stdin);
|
||||
} else {
|
||||
symbols.extend(arg.split(',').map(String::from));
|
||||
}
|
||||
}
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn cache_bucket(provider: ProviderKind, key: &str) -> String {
|
||||
format!("{}-{key}", provider.as_str())
|
||||
}
|
||||
|
|
@ -124,10 +153,10 @@ pub struct StocksCmd {
|
|||
pub enum StocksSubcommand {
|
||||
#[command(
|
||||
about = "Get real-time stock quotes",
|
||||
after_help = "Examples:\n idx stocks quote BBCA\n idx stocks quote BBCA,BBRI,BMRI\n idx -o json stocks quote BBCA"
|
||||
after_help = "Examples:\n idx stocks quote BBCA\n idx stocks quote BBCA,BBRI,BMRI\n idx -o json stocks quote BBCA\n echo BBCA BBRI | idx stocks quote -"
|
||||
)]
|
||||
Quote {
|
||||
/// One or more symbols, comma-separated or space-separated.
|
||||
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
|
||||
#[arg(required = true)]
|
||||
symbols: Vec<String>,
|
||||
},
|
||||
|
|
@ -228,10 +257,10 @@ pub enum StocksSubcommand {
|
|||
},
|
||||
#[command(
|
||||
about = "Compare fundamentals across stocks",
|
||||
after_help = "Examples:\n idx stocks compare BBCA BBRI BMRI\n idx stocks compare BBCA,BBRI,BMRI\n idx -o json stocks compare BBCA,BBRI"
|
||||
after_help = "Examples:\n idx stocks compare BBCA BBRI BMRI\n idx stocks compare BBCA,BBRI,BMRI\n idx -o json stocks compare BBCA,BBRI\n idx stocks screen --filter low-pe | jq -r '.[].symbol' | idx stocks compare -"
|
||||
)]
|
||||
Compare {
|
||||
/// One or more symbols, comma-separated or space-separated.
|
||||
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
|
||||
#[arg(required = true)]
|
||||
symbols: Vec<String>,
|
||||
},
|
||||
|
|
@ -257,8 +286,8 @@ pub fn handle(
|
|||
StocksSubcommand::Quote { symbols } => {
|
||||
let quote_bucket = cache_bucket(provider.kind(), "quote");
|
||||
let mut quotes = Vec::new();
|
||||
for sym in symbols.iter().flat_map(|s| s.split(',')) {
|
||||
let resolved = crate::api::resolve_symbol(sym, &config.exchange)?;
|
||||
for sym in expand_symbols(symbols)? {
|
||||
let resolved = crate::api::resolve_symbol(&sym, &config.exchange)?;
|
||||
if !no_cache && let Some(q) = cache.get("e_bucket, &resolved)? {
|
||||
quotes.push(q);
|
||||
continue;
|
||||
|
|
@ -305,7 +334,7 @@ pub fn handle(
|
|||
history_provider(provider.kind(), history_mode, verbose)?;
|
||||
if matches!(history_mode, HistoryProviderKind::Auto)
|
||||
&& history_source != provider.kind()
|
||||
&& !matches!(config.output, crate::output::OutputFormat::Json)
|
||||
&& !config.output.is_json()
|
||||
{
|
||||
runtime::info(format!(
|
||||
"history provider fallback active ({} -> {})",
|
||||
|
|
@ -373,7 +402,7 @@ pub fn handle(
|
|||
history_provider(provider.kind(), history_mode, verbose)?;
|
||||
if matches!(history_mode, HistoryProviderKind::Auto)
|
||||
&& history_source != provider.kind()
|
||||
&& !matches!(config.output, crate::output::OutputFormat::Json)
|
||||
&& !config.output.is_json()
|
||||
{
|
||||
runtime::info(format!(
|
||||
"history provider fallback active ({} -> {})",
|
||||
|
|
@ -649,8 +678,8 @@ pub fn handle(
|
|||
let mut reports: Vec<FundamentalReport> = Vec::new();
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for sym in symbols.iter().flat_map(|s| s.split(',')) {
|
||||
let resolved = crate::api::resolve_symbol(sym, &config.exchange)?;
|
||||
for sym in expand_symbols(symbols)? {
|
||||
let resolved = crate::api::resolve_symbol(&sym, &config.exchange)?;
|
||||
match fetch_fundamental_analysis_report(
|
||||
&cache,
|
||||
provider.market(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use std::fs;
|
||||
use std::io::IsTerminal;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::ValueEnum;
|
||||
|
|
@ -7,7 +8,7 @@ use serde::Deserialize;
|
|||
|
||||
use crate::cli::Cli;
|
||||
use crate::error::IdxError;
|
||||
use crate::output::OutputFormat;
|
||||
use crate::output::{OutputFormat, OutputSetting};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
|
|
@ -92,7 +93,7 @@ struct FileGeneral {
|
|||
provider: Option<ProviderKind>,
|
||||
history_provider: Option<HistoryProviderKind>,
|
||||
exchange: Option<String>,
|
||||
output: Option<OutputFormat>,
|
||||
output: Option<OutputSetting>,
|
||||
color: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +109,7 @@ impl Default for IdxConfig {
|
|||
provider: ProviderKind::Msn,
|
||||
history_provider: HistoryProviderKind::Auto,
|
||||
exchange: "JK".to_string(),
|
||||
output: OutputFormat::Table,
|
||||
output: OutputSetting::Auto.resolve(stdout_is_terminal()),
|
||||
no_color: false,
|
||||
quote_ttl: 300,
|
||||
fundamental_ttl: 3600,
|
||||
|
|
@ -130,15 +131,12 @@ impl IdxConfig {
|
|||
cfg.exchange = exchange;
|
||||
}
|
||||
if let Ok(output) = std::env::var("IDX_OUTPUT") {
|
||||
cfg.output = if output.eq_ignore_ascii_case("json") {
|
||||
OutputFormat::Json
|
||||
} else if output.eq_ignore_ascii_case("table") {
|
||||
OutputFormat::Table
|
||||
} else {
|
||||
return Err(IdxError::ConfigError(format!(
|
||||
"invalid IDX_OUTPUT value: '{output}', expected 'json' or 'table'"
|
||||
)));
|
||||
};
|
||||
let setting = OutputSetting::parse(&output).ok_or_else(|| {
|
||||
IdxError::ConfigError(format!(
|
||||
"invalid IDX_OUTPUT value: '{output}', expected 'auto', 'table', 'json', or 'jsonl'"
|
||||
))
|
||||
})?;
|
||||
cfg.output = setting.resolve(stdout_is_terminal());
|
||||
}
|
||||
if let Ok(no_color) = std::env::var("IDX_NO_COLOR") {
|
||||
cfg.no_color = no_color == "1" || no_color.eq_ignore_ascii_case("true");
|
||||
|
|
@ -151,9 +149,9 @@ impl IdxConfig {
|
|||
}
|
||||
|
||||
if let Some(output) = cli.output {
|
||||
cfg.output = output;
|
||||
cfg.output = output.resolve(stdout_is_terminal());
|
||||
}
|
||||
cfg.no_color = cfg.no_color || cli.no_color;
|
||||
cfg.no_color = cfg.no_color || cli.no_color || !stdout_is_terminal();
|
||||
|
||||
Ok(cfg)
|
||||
}
|
||||
|
|
@ -176,7 +174,7 @@ impl IdxConfig {
|
|||
cfg.exchange = exchange;
|
||||
}
|
||||
if let Some(output) = general.output {
|
||||
cfg.output = output;
|
||||
cfg.output = output.resolve(stdout_is_terminal());
|
||||
}
|
||||
if let Some(color) = general.color {
|
||||
cfg.no_color = !color;
|
||||
|
|
@ -216,8 +214,12 @@ fn parse_env_ttl(name: &str) -> Result<Option<u64>, IdxError> {
|
|||
Ok(Some(parsed))
|
||||
}
|
||||
|
||||
pub fn stdout_is_terminal() -> bool {
|
||||
std::io::stdout().is_terminal()
|
||||
}
|
||||
|
||||
pub fn default_config_toml() -> String {
|
||||
"[general]\nprovider = \"msn\"\nhistory_provider = \"auto\"\nexchange = \"JK\"\noutput = \"table\"\ncolor = true\n\n[cache]\nquote_ttl = 300\nfundamental_ttl = 3600\n".to_string()
|
||||
"[general]\nprovider = \"msn\"\nhistory_provider = \"auto\"\nexchange = \"JK\"\noutput = \"auto\"\ncolor = true\n\n[cache]\nquote_ttl = 300\nfundamental_ttl = 3600\n".to_string()
|
||||
}
|
||||
|
||||
pub fn config_path() -> Result<PathBuf, IdxError> {
|
||||
|
|
@ -292,9 +294,9 @@ fn normalize_config_value(key: &str, value: &str) -> Result<toml::Value, IdxErro
|
|||
HistoryProviderKind::parse(value)?.as_str().to_string(),
|
||||
)),
|
||||
"general.output" => {
|
||||
if !value.eq_ignore_ascii_case("table") && !value.eq_ignore_ascii_case("json") {
|
||||
if OutputSetting::parse(value).is_none() {
|
||||
return Err(IdxError::InvalidInput(format!(
|
||||
"invalid output format '{value}': expected 'table' or 'json'"
|
||||
"invalid output format '{value}': expected 'auto', 'table', 'json', or 'jsonl'"
|
||||
)));
|
||||
}
|
||||
Ok(toml::Value::String(value.to_ascii_lowercase()))
|
||||
|
|
|
|||
17
src/error.rs
17
src/error.rs
|
|
@ -76,8 +76,23 @@ impl IdxError {
|
|||
}
|
||||
}
|
||||
|
||||
/// Exit status by error class, so scripts can branch without parsing
|
||||
/// stderr. Clap usage errors also exit 2. See docs/DESIGN.md.
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
1
|
||||
match self.code() {
|
||||
ErrorCode::InvalidInput | ErrorCode::ConfigError => 2,
|
||||
ErrorCode::SymbolNotFound | ErrorCode::NoMarketData | ErrorCode::CacheMiss => 3,
|
||||
ErrorCode::Http
|
||||
| ErrorCode::RateLimited
|
||||
| ErrorCode::ProviderUnavailable
|
||||
| ErrorCode::Offline
|
||||
| ErrorCode::AuthError => 4,
|
||||
ErrorCode::Unsupported => 5,
|
||||
ErrorCode::Io
|
||||
| ErrorCode::ParseError
|
||||
| ErrorCode::DatabaseError
|
||||
| ErrorCode::PdfParseError => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
31
src/main.rs
31
src/main.rs
|
|
@ -44,7 +44,7 @@ use crate::api::default_provider;
|
|||
use crate::cli::{Cli, Commands};
|
||||
use crate::config::IdxConfig;
|
||||
use crate::error::IdxError;
|
||||
use crate::output::{OutputFormat, emit_error};
|
||||
use crate::output::{OutputFormat, OutputSetting, emit_error};
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
|
|
@ -62,16 +62,19 @@ fn run() -> Result<(), IdxError> {
|
|||
return Err(err);
|
||||
}
|
||||
};
|
||||
output::json::configure(config.output, config::stdout_is_terminal());
|
||||
output::style::set_color(!config.no_color);
|
||||
|
||||
match &cli.command {
|
||||
Commands::Version => match config.output {
|
||||
OutputFormat::Json => {
|
||||
Commands::Version => {
|
||||
if config.output.is_json() {
|
||||
output::json::print_json(&serde_json::json!({
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
}))?;
|
||||
} else {
|
||||
println!("{}", env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
}
|
||||
OutputFormat::Table => println!("{}", env!("CARGO_PKG_VERSION")),
|
||||
},
|
||||
Commands::Completions { shell } => {
|
||||
let mut cmd = Cli::command();
|
||||
let name = cmd.get_name().to_owned();
|
||||
|
|
@ -119,13 +122,15 @@ fn run() -> Result<(), IdxError> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Output format for errors raised before the configuration has loaded.
|
||||
fn bootstrap_output_format(cli: &Cli) -> OutputFormat {
|
||||
if let Some(output) = cli.output {
|
||||
return output;
|
||||
}
|
||||
|
||||
match std::env::var("IDX_OUTPUT") {
|
||||
Ok(value) if value.eq_ignore_ascii_case("json") => OutputFormat::Json,
|
||||
_ => OutputFormat::Table,
|
||||
}
|
||||
let setting = cli
|
||||
.output
|
||||
.or_else(|| {
|
||||
std::env::var("IDX_OUTPUT")
|
||||
.ok()
|
||||
.and_then(|value| OutputSetting::parse(&value))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
setting.resolve(config::stdout_is_terminal())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,44 @@
|
|||
use std::sync::atomic::{AtomicU8, Ordering};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::error::IdxError;
|
||||
|
||||
const PRETTY: u8 = 0;
|
||||
const COMPACT: u8 = 1;
|
||||
const LINES: u8 = 2;
|
||||
|
||||
static STYLE: AtomicU8 = AtomicU8::new(PRETTY);
|
||||
|
||||
/// Pretty JSON only for a human at a terminal; compact for pipes and
|
||||
/// captured output; JSON Lines when requested.
|
||||
pub fn configure(format: super::OutputFormat, stdout_is_terminal: bool) {
|
||||
let style = match format {
|
||||
super::OutputFormat::Jsonl => LINES,
|
||||
_ if stdout_is_terminal => PRETTY,
|
||||
_ => COMPACT,
|
||||
};
|
||||
STYLE.store(style, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn print_json<T: Serialize + ?Sized>(value: &T) -> Result<(), IdxError> {
|
||||
let out =
|
||||
serde_json::to_string_pretty(value).map_err(|e| IdxError::ParseError(e.to_string()))?;
|
||||
println!("{out}");
|
||||
let to_err = |e: serde_json::Error| IdxError::ParseError(e.to_string());
|
||||
match STYLE.load(Ordering::Relaxed) {
|
||||
PRETTY => println!("{}", serde_json::to_string_pretty(value).map_err(to_err)?),
|
||||
COMPACT => println!("{}", serde_json::to_string(value).map_err(to_err)?),
|
||||
_ => {
|
||||
// Split an array into one line per item without re-serializing,
|
||||
// so each line keeps the same key order as `-o json`.
|
||||
let compact = serde_json::to_string(value).map_err(to_err)?;
|
||||
match serde_json::from_str::<Vec<&serde_json::value::RawValue>>(&compact) {
|
||||
Ok(items) => {
|
||||
for item in items {
|
||||
println!("{}", item.get());
|
||||
}
|
||||
}
|
||||
Err(_) => println!("{compact}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
pub mod json;
|
||||
pub mod style;
|
||||
pub mod table;
|
||||
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use clap::ValueEnum;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -13,12 +16,48 @@ use crate::api::types::{
|
|||
};
|
||||
use crate::error::IdxError;
|
||||
|
||||
/// Output format requested by the user (flag, `IDX_OUTPUT`, or config file).
|
||||
#[derive(Debug, Clone, Copy, ValueEnum, PartialEq, Eq, Serialize, serde::Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum OutputSetting {
|
||||
// Table on a terminal, JSON when stdout is piped or captured.
|
||||
#[default]
|
||||
Auto,
|
||||
Table,
|
||||
Json,
|
||||
// JSON Lines: one array item per line.
|
||||
Jsonl,
|
||||
}
|
||||
|
||||
impl OutputSetting {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
<Self as ValueEnum>::from_str(value, true).ok()
|
||||
}
|
||||
|
||||
pub fn resolve(self, stdout_is_terminal: bool) -> OutputFormat {
|
||||
match self {
|
||||
Self::Auto if stdout_is_terminal => OutputFormat::Table,
|
||||
Self::Auto | Self::Json => OutputFormat::Json,
|
||||
Self::Table => OutputFormat::Table,
|
||||
Self::Jsonl => OutputFormat::Jsonl,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolved output format used by renderers.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum OutputFormat {
|
||||
#[default]
|
||||
Table,
|
||||
Json,
|
||||
Jsonl,
|
||||
}
|
||||
|
||||
impl OutputFormat {
|
||||
/// True for every machine-readable format (JSON and JSON Lines).
|
||||
pub fn is_json(self) -> bool {
|
||||
matches!(self, Self::Json | Self::Jsonl)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -56,7 +95,7 @@ pub fn render_quotes(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_quotes(quotes, no_color),
|
||||
OutputFormat::Json => json::print_json(quotes),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,7 +106,7 @@ pub fn render_history(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_history(symbol, history),
|
||||
OutputFormat::Json => json::print_json(history),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(history),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -78,7 +117,7 @@ pub fn render_technical(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_technical(report, no_color),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,7 +129,7 @@ pub fn render_growth(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_growth(symbol, report, no_color),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -102,7 +141,7 @@ pub fn render_valuation(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_valuation(symbol, report, no_color),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +153,7 @@ pub fn render_risk(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_risk(symbol, report, no_color),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +164,7 @@ pub fn render_fundamental(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_fundamental(report, no_color),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -136,14 +175,14 @@ pub fn render_compare(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_compare(reports, no_color),
|
||||
OutputFormat::Json => json::print_json(reports),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(reports),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_profile(profile: &CompanyProfile, format: &OutputFormat) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_profile(profile),
|
||||
OutputFormat::Json => json::print_json(profile),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(profile),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,35 +192,35 @@ pub fn render_financials(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_financials(financials),
|
||||
OutputFormat::Json => json::print_json(financials),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(financials),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_earnings(report: &EarningsReport, format: &OutputFormat) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_earnings(report),
|
||||
OutputFormat::Json => json::print_json(report),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(report),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_sentiment(data: &SentimentData, format: &OutputFormat) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_sentiment(data),
|
||||
OutputFormat::Json => json::print_json(data),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_insights(data: &InsightData, format: &OutputFormat) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_insights(data),
|
||||
OutputFormat::Json => json::print_json(data),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_news(items: &[NewsItem], format: &OutputFormat) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_news(items),
|
||||
OutputFormat::Json => json::print_json(items),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(items),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -192,23 +231,25 @@ pub fn render_screener(
|
|||
) -> Result<(), IdxError> {
|
||||
match format {
|
||||
OutputFormat::Table => table::print_quotes(quotes, no_color),
|
||||
OutputFormat::Json => json::print_json(quotes),
|
||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn emit_error(err: &IdxError, format: &OutputFormat) {
|
||||
match format {
|
||||
OutputFormat::Table => eprintln!("Error: {err}"),
|
||||
OutputFormat::Json => {
|
||||
if !format.is_json() {
|
||||
eprintln!("Error: {err}");
|
||||
return;
|
||||
}
|
||||
let payload = serde_json::json!({
|
||||
"error": true,
|
||||
"code": format!("{:?}", err.code()).to_uppercase(),
|
||||
"message": err.to_string()
|
||||
});
|
||||
eprintln!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
let pretty = *format == OutputFormat::Json && std::io::stderr().is_terminal();
|
||||
let rendered = if pretty {
|
||||
serde_json::to_string_pretty(&payload)
|
||||
} else {
|
||||
serde_json::to_string(&payload)
|
||||
};
|
||||
eprintln!("{}", rendered.unwrap_or_else(|_| "{}".to_string()));
|
||||
}
|
||||
|
|
|
|||
41
src/output/style.rs
Normal file
41
src/output/style.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
//! Terminal styling that turns itself off when color is disabled or stdout is
|
||||
//! not a terminal, so piped output never carries ANSI escape codes.
|
||||
|
||||
use std::fmt::Display;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
static COLOR: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
pub fn set_color(enabled: bool) {
|
||||
COLOR.store(enabled, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn paint<T: Display>(value: T, styled: impl FnOnce(&T) -> String) -> String {
|
||||
if COLOR.load(Ordering::Relaxed) {
|
||||
styled(&value)
|
||||
} else {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Paint: Display + Sized {
|
||||
fn bold(self) -> String {
|
||||
paint(self, |v| OwoColorize::bold(v).to_string())
|
||||
}
|
||||
fn green(self) -> String {
|
||||
paint(self, |v| OwoColorize::green(v).to_string())
|
||||
}
|
||||
fn red(self) -> String {
|
||||
paint(self, |v| OwoColorize::red(v).to_string())
|
||||
}
|
||||
fn yellow(self) -> String {
|
||||
paint(self, |v| OwoColorize::yellow(v).to_string())
|
||||
}
|
||||
fn blue(self) -> String {
|
||||
paint(self, |v| OwoColorize::blue(v).to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Display> Paint for T {}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::output::style::Paint;
|
||||
use comfy_table::{Cell, Color, ContentArrangement, Table, presets::UTF8_FULL};
|
||||
use owo_colors::OwoColorize;
|
||||
|
||||
use crate::analysis::fundamental::{FundamentalReport, GrowthReport, RiskReport, ValuationReport};
|
||||
use crate::analysis::signals::Signal;
|
||||
|
|
|
|||
17
tests/cli.rs
17
tests/cli.rs
|
|
@ -73,7 +73,10 @@ fn candidate_is_idx_binary(path: &Path) -> bool {
|
|||
return false;
|
||||
}
|
||||
|
||||
let Ok(output) = std::process::Command::new(path).arg("version").output() else {
|
||||
let Ok(output) = std::process::Command::new(path)
|
||||
.args(["-o", "table", "version"])
|
||||
.output()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
@ -111,6 +114,10 @@ fn bin_with_root(root: &Path) -> Command {
|
|||
|
||||
let mut cmd = bin();
|
||||
cmd.current_dir(env!("CARGO_MANIFEST_DIR"));
|
||||
// Tests run with piped stdout, where the default (auto) format is JSON.
|
||||
// Pin table so tests that assert table text keep doing so; `-o` and
|
||||
// per-test IDX_OUTPUT still override this.
|
||||
cmd.env("IDX_OUTPUT", "table");
|
||||
cmd.env("XDG_CONFIG_HOME", &config_home);
|
||||
cmd.env("XDG_CACHE_HOME", &cache_home);
|
||||
cmd
|
||||
|
|
@ -1271,6 +1278,7 @@ fn ownership_discover_json_includes_status() {
|
|||
}"#;
|
||||
let json_url = spawn_single_response_server("application/json", body.to_string());
|
||||
|
||||
let discovered = run_success_json(
|
||||
test_bin("ownership-discover-json-status")
|
||||
.env("IDX_CURL_IMPERSONATE_BIN", "curl")
|
||||
.env("IDX_OWNERSHIP_ANNOUNCEMENT_API_URL", &json_url)
|
||||
|
|
@ -1279,10 +1287,9 @@ fn ownership_discover_json_includes_status() {
|
|||
"IDX_OWNERSHIP_ANNOUNCEMENT_PAGE_URL",
|
||||
"http://127.0.0.1/pengumuman",
|
||||
)
|
||||
.args(["-o", "json", "ownership", "discover", "--limit", "1"])
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("\"status\": \"supported\""));
|
||||
.args(["-o", "json", "ownership", "discover", "--limit", "1"]),
|
||||
);
|
||||
assert_eq!(discovered[0]["status"], "supported");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@
|
|||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
|
@ -40,6 +41,9 @@ struct Case {
|
|||
/// Commands run first in the same environment; their output is ignored.
|
||||
#[serde(default)]
|
||||
before: Vec<Vec<String>>,
|
||||
/// Text piped to the command's stdin.
|
||||
#[serde(default)]
|
||||
stdin: Option<String>,
|
||||
}
|
||||
|
||||
fn default_formats() -> Vec<String> {
|
||||
|
|
@ -56,6 +60,15 @@ fn work_dir() -> PathBuf {
|
|||
|
||||
/// 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 {
|
||||
idx_with_stdin(root, args, env, None)
|
||||
}
|
||||
|
||||
fn idx_with_stdin(
|
||||
root: &Path,
|
||||
args: &[String],
|
||||
env: &BTreeMap<String, String>,
|
||||
stdin: Option<&str>,
|
||||
) -> std::process::Output {
|
||||
let mut cmd = Command::new(IDX);
|
||||
cmd.current_dir(REPO).args(args);
|
||||
for (key, _) in std::env::vars_os() {
|
||||
|
|
@ -68,8 +81,17 @@ fn idx(root: &Path, args: &[String], env: &BTreeMap<String, String>) -> std::pro
|
|||
.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")
|
||||
.envs(env)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let mut child = cmd.spawn().expect("spawn idx");
|
||||
let mut pipe = child.stdin.take().expect("stdin pipe");
|
||||
if let Some(input) = stdin {
|
||||
pipe.write_all(input.as_bytes()).expect("write stdin");
|
||||
}
|
||||
drop(pipe);
|
||||
child.wait_with_output().expect("run idx")
|
||||
}
|
||||
|
||||
/// Fixture ownership DB: the above-1% XLSX (2026-08-31) plus two KSEI archive
|
||||
|
|
@ -161,7 +183,8 @@ fn cli_output_matches_golden_files() {
|
|||
idx(&root, before, &case.env);
|
||||
}
|
||||
|
||||
let actual = render(&root, &idx(&root, &args, &case.env));
|
||||
let output = idx_with_stdin(&root, &args, &case.env, case.stdin.as_deref());
|
||||
let actual = render(&root, &output);
|
||||
let path = golden_dir().join(format!("{name}.golden"));
|
||||
if update {
|
||||
fs::write(&path, &actual).expect("write golden");
|
||||
|
|
|
|||
8
tests/golden/auto-format-env-table.golden
Normal file
8
tests/golden/auto-format-env-table.golden
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
┌─────────┬───────┬──────┬────────┬────────────┬───────────────────────┬────────────┐
|
||||
│ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ MKT CAP ┆ 52W RANGE │
|
||||
╞═════════╪═══════╪══════╪════════╪════════════╪═══════════════════════╪════════════╡
|
||||
│ BBCA.JK ┆ 9,875 ┆ +117 ┆ +1.20% ┆ 12,300,000 ┆ 1,215,200,000,000,000 ┆ ████████░░ │
|
||||
└─────────┴───────┴──────┴────────┴────────────┴───────────────────────┴────────────┘
|
||||
--- stderr
|
||||
4
tests/golden/auto-format-piped.golden
Normal file
4
tests/golden/auto-format-piped.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 0
|
||||
--- 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}]
|
||||
--- stderr
|
||||
|
|
@ -1,7 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"failed": [],
|
||||
"removed": 0
|
||||
}
|
||||
{"failed":[],"removed":0}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"files": 0,
|
||||
"newest": null,
|
||||
"oldest": null,
|
||||
"path": "<ROOT>/cache/idx",
|
||||
"size_bytes": 0
|
||||
}
|
||||
{"files":0,"newest":null,"oldest":null,"path":"<ROOT>/cache/idx","size_bytes":0}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
# 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.
|
||||
# `-o <format>` prepended; `formats = []` runs the args as given, which means
|
||||
# the auto format: stdout is a pipe here, so auto resolves to compact JSON.
|
||||
# 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; `stdin` is piped to the command.
|
||||
#
|
||||
# `idx version` is excluded on purpose: its output changes with every release.
|
||||
|
||||
|
|
@ -62,6 +64,22 @@ name = "config-set"
|
|||
args = ["config", "set", "general.provider", "yahoo"]
|
||||
formats = []
|
||||
|
||||
[[case]]
|
||||
name = "config-set-output-jsonl"
|
||||
args = ["config", "set", "general.output", "jsonl"]
|
||||
formats = []
|
||||
|
||||
[[case]]
|
||||
name = "config-set-output-invalid"
|
||||
args = ["config", "set", "general.output", "yaml"]
|
||||
formats = []
|
||||
|
||||
[[case]]
|
||||
name = "config-output-table-pins-format"
|
||||
args = ["stocks", "quote", "BBCA"]
|
||||
formats = []
|
||||
before = [["config", "set", "general.output", "table"]]
|
||||
|
||||
[[case]]
|
||||
name = "cache-info-empty"
|
||||
args = ["cache", "info"]
|
||||
|
|
@ -78,10 +96,12 @@ args = ["stocks", "quote", "BBCA"]
|
|||
|
||||
[[case]]
|
||||
name = "stocks-quote-multi"
|
||||
formats = ["table", "json", "jsonl"]
|
||||
args = ["stocks", "quote", "BBCA,BBRI"]
|
||||
|
||||
[[case]]
|
||||
name = "stocks-history"
|
||||
formats = ["table", "json", "jsonl"]
|
||||
args = ["stocks", "history", "BBCA"]
|
||||
|
||||
[[case]]
|
||||
|
|
@ -111,6 +131,7 @@ args = ["stocks", "risk", "BBCA"]
|
|||
|
||||
[[case]]
|
||||
name = "stocks-fundamental"
|
||||
formats = ["table", "json", "jsonl"]
|
||||
args = ["stocks", "fundamental", "BBCA"]
|
||||
|
||||
[[case]]
|
||||
|
|
@ -169,6 +190,38 @@ args = ["stocks", "screen", "--filter", "high-volume"]
|
|||
name = "stocks-screen-large-cap"
|
||||
args = ["stocks", "screen", "--filter", "large-cap"]
|
||||
|
||||
[[case]]
|
||||
name = "auto-format-piped"
|
||||
args = ["stocks", "quote", "BBCA,BBRI"]
|
||||
formats = []
|
||||
|
||||
[[case]]
|
||||
name = "auto-format-env-table"
|
||||
args = ["stocks", "quote", "BBCA"]
|
||||
formats = []
|
||||
env = { IDX_OUTPUT = "table" }
|
||||
|
||||
[[case]]
|
||||
name = "stocks-quote-stdin"
|
||||
args = ["stocks", "quote", "-"]
|
||||
stdin = "BBCA\nBBRI\n"
|
||||
|
||||
[[case]]
|
||||
name = "stocks-compare-stdin"
|
||||
args = ["stocks", "compare", "-"]
|
||||
stdin = "BBCA, BBRI"
|
||||
|
||||
[[case]]
|
||||
name = "error-stdin-empty"
|
||||
args = ["stocks", "quote", "-"]
|
||||
stdin = ""
|
||||
|
||||
[[case]]
|
||||
name = "error-invalid-output-env"
|
||||
args = ["stocks", "quote", "BBCA"]
|
||||
formats = []
|
||||
env = { IDX_OUTPUT = "yaml" }
|
||||
|
||||
[[case]]
|
||||
name = "stocks-quote-offline-warm"
|
||||
args = ["--offline", "stocks", "quote", "BBCA"]
|
||||
|
|
@ -214,6 +267,7 @@ env = { IDX_MOCK_MSN_KEYRATIOS_FIXTURE = "tests/fixtures/msn_keyratios_industry_
|
|||
|
||||
[[case]]
|
||||
name = "error-screen-region"
|
||||
formats = ["table", "json", "jsonl"]
|
||||
args = ["stocks", "screen", "--region", "eu"]
|
||||
|
||||
[[case]]
|
||||
|
|
@ -298,6 +352,7 @@ ownership = true
|
|||
|
||||
[[case]]
|
||||
name = "ownership-changes"
|
||||
formats = ["table", "json", "jsonl"]
|
||||
args = ["ownership", "changes", "--from", "2026-01-30", "--to", "2026-02-27"]
|
||||
ownership = true
|
||||
|
||||
|
|
|
|||
|
|
@ -446,11 +446,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -468,11 +468,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -490,11 +490,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -568,11 +568,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -590,11 +590,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -612,11 +612,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -634,11 +634,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -740,11 +740,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -762,11 +762,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -784,11 +784,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1436,11 +1436,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1466,11 +1466,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1496,11 +1496,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1526,11 +1526,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1556,11 +1556,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1578,11 +1578,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1600,11 +1600,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1630,11 +1630,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1916,11 +1916,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1938,11 +1938,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -1960,11 +1960,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2052,11 +2052,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2074,11 +2074,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2100,11 +2100,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2126,11 +2126,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2152,11 +2152,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2178,11 +2178,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2200,11 +2200,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2222,11 +2222,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2244,11 +2244,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2270,11 +2270,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2292,11 +2292,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2314,11 +2314,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2586,11 +2586,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2608,11 +2608,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2634,11 +2634,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2656,11 +2656,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2678,11 +2678,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2700,11 +2700,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2734,11 +2734,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2756,11 +2756,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2782,11 +2782,11 @@ _idx() {
|
|||
return 0
|
||||
;;
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2804,11 +2804,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
@ -2826,11 +2826,11 @@ _idx() {
|
|||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "table json" -- "${cur}"))
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
|
|
|
|||
8
tests/golden/config-output-table-pins-format.golden
Normal file
8
tests/golden/config-output-table-pins-format.golden
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
┌─────────┬───────┬──────┬────────┬────────────┬───────────────────────┬────────────┐
|
||||
│ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ MKT CAP ┆ 52W RANGE │
|
||||
╞═════════╪═══════╪══════╪════════╪════════════╪═══════════════════════╪════════════╡
|
||||
│ BBCA.JK ┆ 9,875 ┆ +117 ┆ +1.20% ┆ 12,300,000 ┆ 1,215,200,000,000,000 ┆ ████████░░ │
|
||||
└─────────┴───────┴──────┴────────┴────────────┴───────────────────────┴────────────┘
|
||||
--- stderr
|
||||
4
tests/golden/config-set-output-invalid.golden
Normal file
4
tests/golden/config-set-output-invalid.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{"code":"INVALIDINPUT","error":true,"message":"invalid input: invalid output format 'yaml': expected 'auto', 'table', 'json', or 'jsonl'"}
|
||||
4
tests/golden/config-set-output-jsonl.golden
Normal file
4
tests/golden/config-set-output-jsonl.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
ok
|
||||
--- stderr
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 4
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "PROVIDERUNAVAILABLE",
|
||||
"error": true,
|
||||
"message": "provider unavailable"
|
||||
}
|
||||
{"code":"PROVIDERUNAVAILABLE","error":true,"message":"provider unavailable"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 4
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: provider unavailable
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 5
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "UNSUPPORTED",
|
||||
"error": true,
|
||||
"message": "unsupported: --fetch-bing import is not implemented yet"
|
||||
}
|
||||
{"code":"UNSUPPORTED","error":true,"message":"unsupported: --fetch-bing import is not implemented yet"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 5
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: unsupported: --fetch-bing import is not implemented yet
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "PARSEERROR",
|
||||
"error": true,
|
||||
"message": "parse error: unsupported: company fundamentals unavailable from MSN; industry fallback is disabled"
|
||||
}
|
||||
{"code":"PARSEERROR","error":true,"message":"parse error: unsupported: company fundamentals unavailable from MSN; industry fallback is disabled"}
|
||||
|
|
|
|||
4
tests/golden/error-invalid-output-env.golden
Normal file
4
tests/golden/error-invalid-output-env.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{"code":"CONFIGERROR","error":true,"message":"config error: invalid IDX_OUTPUT value: 'yaml', expected 'auto', 'table', 'json', or 'jsonl'"}
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "CONFIGERROR",
|
||||
"error": true,
|
||||
"message": "config error: invalid provider 'bogus' (expected yahoo or msn)"
|
||||
}
|
||||
{"code":"CONFIGERROR","error":true,"message":"config error: invalid provider 'bogus' (expected yahoo or msn)"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: config error: invalid provider 'bogus' (expected yahoo or msn)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "CACHEMISS",
|
||||
"error": true,
|
||||
"message": "cache miss: msn-quote/BBCA.JK"
|
||||
}
|
||||
{"code":"CACHEMISS","error":true,"message":"cache miss: msn-quote/BBCA.JK"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: cache miss: msn-quote/BBCA.JK
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "INVALIDINPUT",
|
||||
"error": true,
|
||||
"message": "invalid input: cannot combine --offline with --no-cache"
|
||||
}
|
||||
{"code":"INVALIDINPUT","error":true,"message":"invalid input: cannot combine --offline with --no-cache"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: invalid input: cannot combine --offline with --no-cache
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 4
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "PROVIDERUNAVAILABLE",
|
||||
"error": true,
|
||||
"message": "provider unavailable"
|
||||
}
|
||||
{"code":"PROVIDERUNAVAILABLE","error":true,"message":"provider unavailable"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 4
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: provider unavailable
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "INVALIDINPUT",
|
||||
"error": true,
|
||||
"message": "invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)"
|
||||
}
|
||||
{"code":"INVALIDINPUT","error":true,"message":"invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)"}
|
||||
|
|
|
|||
4
tests/golden/error-screen-region.jsonl.golden
Normal file
4
tests/golden/error-screen-region.jsonl.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{"code":"INVALIDINPUT","error":true,"message":"invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)"}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: invalid input: invalid screener region 'eu' (expected one of: id, us, sg, hk, jp)
|
||||
|
|
|
|||
4
tests/golden/error-stdin-empty.json.golden
Normal file
4
tests/golden/error-stdin-empty.json.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
{"code":"INVALIDINPUT","error":true,"message":"invalid input: no symbols read from stdin"}
|
||||
4
tests/golden/error-stdin-empty.table.golden
Normal file
4
tests/golden/error-stdin-empty.table.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 2
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: invalid input: no symbols read from stdin
|
||||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 5
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "UNSUPPORTED",
|
||||
"error": true,
|
||||
"message": "unsupported: BBCA.JK: command requires --provider msn"
|
||||
}
|
||||
{"code":"UNSUPPORTED","error":true,"message":"unsupported: BBCA.JK: command requires --provider msn"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 5
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: unsupported: BBCA.JK: command requires --provider msn
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ Commands:
|
|||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [possible values: table, json]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Commands:
|
|||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [possible values: table, json]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ Commands:
|
|||
|
||||
Options:
|
||||
-o, --output <OUTPUT>
|
||||
Output format
|
||||
Output format [default: auto = table on a terminal, json when piped]
|
||||
|
||||
[possible values: table, json]
|
||||
[possible values: auto, table, json, jsonl]
|
||||
|
||||
--no-color
|
||||
Disable colored output
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ Commands:
|
|||
|
||||
Options:
|
||||
-o, --output <OUTPUT>
|
||||
Output format
|
||||
Output format [default: auto = table on a terminal, json when piped]
|
||||
|
||||
[possible values: table, json]
|
||||
[possible values: auto, table, json, jsonl]
|
||||
|
||||
--no-color
|
||||
Disable colored output
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Commands:
|
|||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [possible values: table, json]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
|
|
|
|||
|
|
@ -1,109 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
16
tests/golden/ownership-changes.jsonl.golden
Normal file
16
tests/golden/ownership-changes.jsonl.golden
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
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
|
||||
|
|
@ -1,32 +1,32 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
┌────────┬───────────────────────────┬─────────────────────┬────────┬────────┬────────┐
|
||||
┌────────┬───────────────────────────┬───────────┬────────┬────────┬────────┐
|
||||
│ TICKER ┆ ENTITY ┆ CHANGE ┆ OLD% ┆ NEW% ┆ DELTA% │
|
||||
╞════════╪═══════════════════════════╪═════════════════════╪════════╪════════╪════════╡
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL ID ┆ [31mDECREASED[39m ┆ 24.00% ┆ 23.34% ┆ -0.66% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL IS ┆ [32mINCREASED[39m ┆ 0.75% ┆ 1.29% ┆ +0.54% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL MF ┆ [32mINCREASED[39m ┆ 1.17% ┆ 1.52% ┆ +0.35% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN MF ┆ [32mINCREASED[39m ┆ 3.07% ┆ 3.37% ┆ +0.30% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN OT ┆ [31mDECREASED[39m ┆ 0.91% ┆ 0.64% ┆ -0.27% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN PF ┆ [31mDECREASED[39m ┆ 0.76% ┆ 0.56% ┆ -0.20% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN CP ┆ [32mINCREASED[39m ┆ 0.72% ┆ 0.86% ┆ +0.14% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL CP ┆ [31mDECREASED[39m ┆ 64.81% ┆ 64.67% ┆ -0.14% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL SC ┆ [31mDECREASED[39m ┆ 0.36% ┆ 0.29% ┆ -0.07% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN IB ┆ [31mDECREASED[39m ┆ 2.67% ┆ 2.63% ┆ -0.04% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN SC ┆ [32mINCREASED[39m ┆ 0.51% ┆ 0.55% ┆ +0.04% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE LOCAL PF ┆ [32mINCREASED[39m ┆ 0.17% ┆ 0.20% ┆ +0.03% │
|
||||
├╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ AADI ┆ KSEI AGGREGATE FOREIGN ID ┆ [31mDECREASED[39m ┆ 0.03% ┆ 0.02% ┆ -0.01% │
|
||||
└────────┴───────────────────────────┴─────────────────────┴────────┴────────┴────────┘
|
||||
╞════════╪═══════════════════════════╪═══════════╪════════╪════════╪════════╡
|
||||
│ 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
|
||||
|
|
|
|||
|
|
@ -1,45 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
]
|
||||
[["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
|
||||
|
|
|
|||
|
|
@ -1,175 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
|
|
@ -1,26 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mEntity:[0m [1mANTHONI SALIM[0m
|
||||
Entity: ANTHONI SALIM
|
||||
┌────────┬────────┬───────────────┬───────┬─────────────┐
|
||||
│ TICKER ┆ SOURCE ┆ SHARES ┆ % ┆ REPORT_DATE │
|
||||
╞════════╪════════╪═══════════════╪═══════╪═════════════╡
|
||||
│ BBCA ┆ KSEI ┆ 1,416,306,835 ┆ 1.15% ┆ 2026-08-31 │
|
||||
└────────┴────────┴───────────────┴───────┴─────────────┘
|
||||
[1mTotal tickers:[0m 1
|
||||
Total tickers: 1
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,58 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,58 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"action": "imported",
|
||||
"inserted_rows": 18,
|
||||
"ticker_count": 1,
|
||||
"as_of_date": "2026-02-27",
|
||||
"sha256": "7b8da1d820f67062410c48988bb6c70fec2088c24961d481d1c0fce8b9372547",
|
||||
"source_url": null
|
||||
}
|
||||
{"action":"imported","inserted_rows":18,"ticker_count":1,"as_of_date":"2026-02-27","sha256":"7b8da1d820f67062410c48988bb6c70fec2088c24961d481d1c0fce8b9372547","source_url":null}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"action": "imported",
|
||||
"inserted_rows": 17,
|
||||
"ticker_count": 4,
|
||||
"as_of_date": "2026-08-31",
|
||||
"sha256": "7c1a4c209b8ce5bde9112436a0c64d66b042915995b584d2bbf7ca7b15ce81e3",
|
||||
"source_url": null
|
||||
}
|
||||
{"action":"imported","inserted_rows":17,"ticker_count":4,"as_of_date":"2026-08-31","sha256":"7c1a4c209b8ce5bde9112436a0c64d66b042915995b584d2bbf7ca7b15ce81e3","source_url":null}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"action": "mapped",
|
||||
"alias": "SALIM ANTHONI",
|
||||
"entity": "ANTHONI SALIM"
|
||||
}
|
||||
{"action":"mapped","alias":"SALIM ANTHONI","entity":"ANTHONI SALIM"}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"action": "merged",
|
||||
"alias_updates": 1,
|
||||
"bing_holdings_updates": 0,
|
||||
"keep": 1,
|
||||
"ksei_holdings_updates": 1,
|
||||
"merge": 2
|
||||
}
|
||||
{"action":"merged","alias_updates":1,"bing_holdings_updates":0,"keep":1,"ksei_holdings_updates":1,"merge":2}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,11 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[
|
||||
{
|
||||
"id": 17,
|
||||
"canonical_name": "ANTHONI SALIM",
|
||||
"entity_type": null,
|
||||
"country": null
|
||||
}
|
||||
]
|
||||
[{"id":17,"canonical_name":"ANTHONI SALIM","entity_type":null,"country":null}]
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
exit: 1
|
||||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
{
|
||||
"code": "SYMBOLNOTFOUND",
|
||||
"error": true,
|
||||
"message": "symbol not found: BBCA"
|
||||
}
|
||||
{"code":"SYMBOLNOTFOUND","error":true,"message":"symbol not found: BBCA"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
exit: 1
|
||||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: symbol not found: BBCA
|
||||
|
|
|
|||
|
|
@ -1,67 +1,4 @@
|
|||
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
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mOwnership:[0m [1mBBCA[0m KSEI=2026-08-31 Bing=-
|
||||
Ownership: BBCA KSEI=2026-08-31 Bing=-
|
||||
┌──────┬────────┬────────────────────────────┬────────────┬─────┬────────────────┬────────┬────────┐
|
||||
│ RANK ┆ SOURCE ┆ INVESTOR ┆ TYPE ┆ L/F ┆ SHARES ┆ % ┆ SIGNAL │
|
||||
╞══════╪════════╪════════════════════════════╪════════════╪═════╪════════════════╪════════╪════════╡
|
||||
|
|
@ -12,5 +12,5 @@ exit: 0
|
|||
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ 4 ┆ KSEI ┆ CATURGUWIRATNA SUMAPALA ┆ CORPORATE ┆ L ┆ 1,261,750,000 ┆ 1.02% ┆ - │
|
||||
└──────┴────────┴────────────────────────────┴────────────┴─────┴────────────────┴────────┴────────┘
|
||||
[1mConcentration:[0m top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
|
||||
Concentration: top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,67 +1,4 @@
|
|||
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
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mOwnership:[0m [1mBBCA[0m KSEI=2026-08-31 Bing=-
|
||||
Ownership: BBCA KSEI=2026-08-31 Bing=-
|
||||
┌──────┬────────┬────────────────────────────┬────────────┬─────┬────────────────┬────────┬────────┐
|
||||
│ RANK ┆ SOURCE ┆ INVESTOR ┆ TYPE ┆ L/F ┆ SHARES ┆ % ┆ SIGNAL │
|
||||
╞══════╪════════╪════════════════════════════╪════════════╪═════╪════════════════╪════════╪════════╡
|
||||
|
|
@ -12,5 +12,5 @@ exit: 0
|
|||
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
|
||||
│ 4 ┆ KSEI ┆ CATURGUWIRATNA SUMAPALA ┆ CORPORATE ┆ L ┆ 1,261,750,000 ┆ 1.02% ┆ - │
|
||||
└──────┴────────┴────────────────────────────┴────────────┴─────┴────────────────┴────────┴────────┘
|
||||
[1mConcentration:[0m top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
|
||||
Concentration: top1=54.94% top3=57.16% hhi=3021 free_float=41.82% holders=4
|
||||
--- stderr
|
||||
|
|
|
|||
4
tests/golden/stocks-compare-stdin.json.golden
Normal file
4
tests/golden/stocks-compare-stdin.json.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
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
|
||||
23
tests/golden/stocks-compare-stdin.table.golden
Normal file
23
tests/golden/stocks-compare-stdin.table.golden
Normal 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
|
||||
|
|
@ -1,81 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
|
|
@ -1,23 +1,23 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mFundamental Comparison[0m
|
||||
┌────────────────┬─────────────────────────┬─────────────────────────┐
|
||||
Fundamental Comparison
|
||||
┌────────────────┬───────────────┬───────────────┐
|
||||
│ METRIC ┆ BBCA.JK ┆ BBRI.JK │
|
||||
╞════════════════╪═════════════════════════╪═════════════════════════╡
|
||||
╞════════════════╪═══════════════╪═══════════════╡
|
||||
│ Symbol ┆ BBCA.JK ┆ BBRI.JK │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ [33mmixed[39m ┆ [33mmixed[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Growth ┆ [33mmixed[39m ┆ [33mmixed[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Valuation ┆ [31mexpensive[39m ┆ [31mexpensive[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Risk ┆ [33mmoderate risk[39m ┆ [33mmoderate risk[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ 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
|
||||
|
|
|
|||
|
|
@ -1,22 +1,4 @@
|
|||
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": []
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,22 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,56 +1,4 @@
|
|||
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,21 +1,4 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,36 +1,4 @@
|
|||
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"
|
||||
}
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,41 +1,4 @@
|
|||
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":"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
|
||||
|
|
|
|||
4
tests/golden/stocks-fundamental.jsonl.golden
Normal file
4
tests/golden/stocks-fundamental.jsonl.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
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
|
||||
|
|
@ -1,49 +1,49 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mFundamental Analysis for BBCA.JK[0m
|
||||
Fundamental Analysis for BBCA.JK
|
||||
|
||||
[1mGrowth Analysis for BBCA.JK[0m
|
||||
┌─────────────────┬─────────┬────────────────────┐
|
||||
Growth Analysis for BBCA.JK
|
||||
┌─────────────────┬─────────┬──────────┐
|
||||
│ METRIC ┆ VALUE ┆ SIGNAL │
|
||||
╞═════════════════╪═════════╪════════════════════╡
|
||||
│ Revenue Growth ┆ +8.10% ┆ [33mslow[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Earnings Growth ┆ +12.10% ┆ [32mmoderate[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ [33mmixed[39m │
|
||||
└─────────────────┴─────────┴────────────────────┘
|
||||
╞═════════════════╪═════════╪══════════╡
|
||||
│ Revenue Growth ┆ +8.10% ┆ slow │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Earnings Growth ┆ +12.10% ┆ moderate │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ mixed │
|
||||
└─────────────────┴─────────┴──────────┘
|
||||
|
||||
[1mValuation Analysis for BBCA.JK[0m
|
||||
┌────────────────┬─────────┬─────────────────────┐
|
||||
Valuation Analysis for BBCA.JK
|
||||
┌────────────────┬─────────┬───────────┐
|
||||
│ METRIC ┆ VALUE ┆ SIGNAL │
|
||||
╞════════════════╪═════════╪═════════════════════╡
|
||||
│ P/E (Trailing) ┆ 25.40 ┆ [33mpremium[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
╞════════════════╪═════════╪═══════════╡
|
||||
│ P/E (Trailing) ┆ 25.40 ┆ premium │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ P/E (Forward) ┆ - ┆ - │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Price/Book ┆ 4.60 ┆ [31mexpensive[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ ROE ┆ +19.80% ┆ [32mstrong[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Net Margin ┆ +32.40% ┆ [32mexcellent[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ EV/EBITDA ┆ - ┆ [33mno data[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ [31mexpensive[39m │
|
||||
└────────────────┴─────────┴─────────────────────┘
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Price/Book ┆ 4.60 ┆ expensive │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ ROE ┆ +19.80% ┆ strong │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Net Margin ┆ +32.40% ┆ excellent │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ EV/EBITDA ┆ - ┆ no data │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ expensive │
|
||||
└────────────────┴─────────┴───────────┘
|
||||
|
||||
[1mRisk Analysis for BBCA.JK[0m
|
||||
┌───────────────┬────────┬─────────────────────────┐
|
||||
Risk Analysis for BBCA.JK
|
||||
┌───────────────┬────────┬───────────────┐
|
||||
│ METRIC ┆ VALUE ┆ SIGNAL │
|
||||
╞═══════════════╪════════╪═════════════════════════╡
|
||||
│ Debt/Equity ┆ 0.42 ┆ [32mconservative[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Current Ratio ┆ 1.18 ┆ [33mtight[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
╞═══════════════╪════════╪═══════════════╡
|
||||
│ Debt/Equity ┆ 0.42 ┆ conservative │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Current Ratio ┆ 1.18 ┆ tight │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ ROA ┆ +3.10% ┆ - │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ [33mmoderate risk[39m │
|
||||
└───────────────┴────────┴─────────────────────────┘
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ moderate risk │
|
||||
└───────────────┴────────┴───────────────┘
|
||||
|
||||
[1mOverall Signal:[0m [33mmixed[39m
|
||||
Overall Signal: mixed
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,12 +1,4 @@
|
|||
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"
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mGrowth Analysis for BBCA.JK[0m
|
||||
┌─────────────────┬─────────┬────────────────────┐
|
||||
Growth Analysis for BBCA.JK
|
||||
┌─────────────────┬─────────┬──────────┐
|
||||
│ METRIC ┆ VALUE ┆ SIGNAL │
|
||||
╞═════════════════╪═════════╪════════════════════╡
|
||||
│ Revenue Growth ┆ +8.10% ┆ [33mslow[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Earnings Growth ┆ +12.10% ┆ [32mmoderate[39m │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ [33mmixed[39m │
|
||||
└─────────────────┴─────────┴────────────────────┘
|
||||
╞═════════════════╪═════════╪══════════╡
|
||||
│ Revenue Growth ┆ +8.10% ┆ slow │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Earnings Growth ┆ +12.10% ┆ moderate │
|
||||
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ Overall ┆ - ┆ mixed │
|
||||
└─────────────────┴─────────┴──────────┘
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mHistory for BBCA.JK[0m
|
||||
History for BBCA.JK
|
||||
┌────────────┬───────┬───────┬───────┬───────┬────────┐
|
||||
│ DATE ┆ OPEN ┆ HIGH ┆ LOW ┆ CLOSE ┆ VOLUME │
|
||||
╞════════════╪═══════╪═══════╪═══════╪═══════╪════════╡
|
||||
|
|
|
|||
|
|
@ -1,29 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
6
tests/golden/stocks-history.jsonl.golden
Normal file
6
tests/golden/stocks-history.jsonl.golden
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[1mHistory for BBCA.JK[0m
|
||||
History for BBCA.JK
|
||||
┌────────────┬───────┬───────┬───────┬───────┬────────────┐
|
||||
│ DATE ┆ OPEN ┆ HIGH ┆ LOW ┆ CLOSE ┆ VOLUME │
|
||||
╞════════════╪═══════╪═══════╪═══════╪═══════╪════════════╡
|
||||
|
|
|
|||
|
|
@ -1,16 +1,4 @@
|
|||
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"
|
||||
}
|
||||
{"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
|
||||
|
|
|
|||
|
|
@ -1,15 +1,4 @@
|
|||
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
|
||||
}
|
||||
]
|
||||
[{"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
|
||||
|
|
|
|||
|
|
@ -1,27 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
{
|
||||
"id": "feee16af-c94c-465d-a59d-a9727c22bd0e",
|
||||
"symbol": "BBCA",
|
||||
"short_name": "Bank Cntrl Asia",
|
||||
"long_name": "PT Bank Central Asia Tbk",
|
||||
"description": "PT Bank Central Asia Tbk (the Bank) is an Indonesia-based commercial bank.",
|
||||
"sector": "Financials",
|
||||
"industry": "Banking Services",
|
||||
"website": "https://www.bca.co.id/",
|
||||
"employees": 27937,
|
||||
"address": "Menara BCA, Grand, Jl. MH Thamrin No. 1",
|
||||
"city": "JAKARTA PUSAT",
|
||||
"country": "Indonesia",
|
||||
"phone": "+62 21 23588000",
|
||||
"officers": [
|
||||
{
|
||||
"name": "Jahja Setiaatmadja",
|
||||
"title": "President Director",
|
||||
"age": 69,
|
||||
"year_born": 1955,
|
||||
"total_pay": 12000000000
|
||||
}
|
||||
]
|
||||
}
|
||||
{"id":"feee16af-c94c-465d-a59d-a9727c22bd0e","symbol":"BBCA","short_name":"Bank Cntrl Asia","long_name":"PT Bank Central Asia Tbk","description":"PT Bank Central Asia Tbk (the Bank) is an Indonesia-based commercial bank.","sector":"Financials","industry":"Banking Services","website":"https://www.bca.co.id/","employees":27937,"address":"Menara BCA, Grand, Jl. MH Thamrin No. 1","city":"JAKARTA PUSAT","country":"Indonesia","phone":"+62 21 23588000","officers":[{"name":"Jahja Setiaatmadja","title":"President Director","age":69,"year_born":1955,"total_pay":12000000000}]}
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -1,35 +1,4 @@
|
|||
exit: 0
|
||||
--- 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},{"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}]
|
||||
--- stderr
|
||||
|
|
|
|||
5
tests/golden/stocks-quote-multi.jsonl.golden
Normal file
5
tests/golden/stocks-quote-multi.jsonl.golden
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
exit: 0
|
||||
--- 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}
|
||||
--- stderr
|
||||
|
|
@ -1,20 +1,4 @@
|
|||
exit: 0
|
||||
--- 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}]
|
||||
--- stderr
|
||||
|
|
|
|||
4
tests/golden/stocks-quote-stdin.json.golden
Normal file
4
tests/golden/stocks-quote-stdin.json.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 0
|
||||
--- 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}]
|
||||
--- stderr
|
||||
10
tests/golden/stocks-quote-stdin.table.golden
Normal file
10
tests/golden/stocks-quote-stdin.table.golden
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
┌─────────┬───────┬──────┬────────┬────────────┬───────────────────────┬────────────┐
|
||||
│ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ MKT CAP ┆ 52W RANGE │
|
||||
╞═════════╪═══════╪══════╪════════╪════════════╪═══════════════════════╪════════════╡
|
||||
│ BBCA.JK ┆ 9,875 ┆ +117 ┆ +1.20% ┆ 12,300,000 ┆ 1,215,200,000,000,000 ┆ ████████░░ │
|
||||
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||
│ BBRI.JK ┆ 9,875 ┆ +117 ┆ +1.20% ┆ 12,300,000 ┆ 1,215,200,000,000,000 ┆ ████████░░ │
|
||||
└─────────┴───────┴──────┴────────┴────────────┴───────────────────────┴────────────┘
|
||||
--- stderr
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue