feat: login-free Stockbit intraday and quote provider

Read Stockbit's public symbol page (__NEXT_DATA__) with a plain ureq
request and an honest user agent: `stocks intraday` (today by the
minute, any provider), `--provider stockbit` quotes with the best
bid/offer only while the market is open, and MCP `get_intraday`.

- UPSTREAMBLOCKED error (exit 4) for a CDN 403; never retried
- requests paced 1 s apart across idx processes, 5 s snapshot cache
- global --provider flag (yahoo, msn, stockbit)
- roadmap: L1 ships from Stockbit; depth stays a login follow-on
This commit is contained in:
hermes 2026-10-23 04:58:32 +00:00
commit 74e5bb0536
65 changed files with 2320 additions and 381 deletions

View file

@ -71,6 +71,7 @@ idx stocks history BBCA --period 3mo
idx stocks technical BBCA idx stocks technical BBCA
idx stocks fundamental BBCA idx stocks fundamental BBCA
idx stocks compare BBCA BBRI BMRI idx stocks compare BBCA BBRI BMRI
idx stocks intraday BBCA # today by the minute (Stockbit, delayed)
idx -o json stocks quote BBCA idx -o json stocks quote BBCA
# Ownership: download the maintained snapshot once, then query locally # Ownership: download the maintained snapshot once, then query locally
@ -90,8 +91,12 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31
- Analysis: `technical`, `growth`, `valuation`, `risk`, and a combined - Analysis: `technical`, `growth`, `valuation`, `risk`, and a combined
`fundamental` report; multi-symbol `compare`; MSN stock `screen` `fundamental` report; multi-symbol `compare`; MSN stock `screen`
- Providers: MSN is the default for quotes, fundamentals, and screening; - Providers: MSN is the default for quotes, fundamentals, and screening;
Yahoo serves history. Switch with `IDX_PROVIDER=msn|yahoo` and Yahoo serves history. Switch with `--provider`/`IDX_PROVIDER=msn|yahoo` and
`IDX_HISTORY_PROVIDER=auto|yahoo|msn` `IDX_HISTORY_PROVIDER=auto|yahoo|msn`
- Stockbit, opt-in and without a login: `stocks intraday` (today's price by
the minute) and `--provider stockbit` quotes with the best bid/offer while
the market is open. Delayed at least 10 minutes, for personal use under
Stockbit's terms; see the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/)
- Local file cache with TTL, and `--offline` mode with stale-cache fallback - Local file cache with TTL, and `--offline` mode with stale-cache fallback
### Ownership (KSEI shareholders above 1%) ### Ownership (KSEI shareholders above 1%)
@ -158,8 +163,8 @@ change it to `auto` with `idx config set general.output auto`.
## MCP server ## MCP server
`idx-mcp` serves the same data to MCP clients over stdio, as 14 tools: `idx-mcp` serves the same data to MCP clients over stdio, as 15 tools:
`get_quotes`, `get_price_history`, `screen_stocks`, `get_company`, `get_quotes`, `get_intraday`, `get_price_history`, `screen_stocks`, `get_company`,
`analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`, `analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`,
`get_foreign_flow`, `get_shareholders`, `get_investor_holdings`, `get_foreign_flow`, `get_shareholders`, `get_investor_holdings`,
`search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as `search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as

View file

@ -4,16 +4,23 @@ Open work only. Shipped behavior is described in `README.md` and `docs/`;
history lives in git. history lives in git.
## Stocks ## Stocks
- `market summary`: IHSG index and market breadth - `market summary`: market breadth and an index overview (index levels alone
are already `stocks intraday ^JKSE` and `--provider stockbit stocks quote IHSG`)
- `market movers`: top gainers, losers, and volume - `market movers`: top gainers, losers, and volume
- `market sectors`: sector performance overview - `market sectors`: sector performance overview
- `screen query "<EXPR>"`: filter stocks by expression - `screen query "<EXPR>"`: filter stocks by expression
- `screen presets` / `screen run <PRESET>`: built-in screener presets - `screen presets` / `screen run <PRESET>`: built-in screener presets
(graduate `screen` out of `stocks` only if these grow into a richer workflow) (graduate `screen` out of `stocks` only if these grow into a richer workflow)
- Multi-period financial statements (today `financials` is single-period) - Multi-period financial statements (today `financials` is single-period)
- Order book (bid/offer depth): no free, login-free source exists. IDX sells - Order book depth: no free, login-free source exists (IDX sells delayed depth
delayed depth to vendors and Stockbit's needs a user login. Revisit if a to vendors). Planned as the Stockbit login follow-on: opt-in with the
permitted source appears; do not ship the closing best bid/offer as one. user's own account, the depth ladder, intraday OHLCV and token refresh from
`exodus.stockbit.com`; fzn0x/idx (MIT) maps the endpoints. The public page
already gives the best bid/offer (L1) while the market is open.
- Observe Stockbit's `market_hour.status` in the pre-open (08:45-09:00) and
the closing auction (15:50-16:00) before `--watch` or alerts build on it;
`open`, `break` and `close` are known, and the bid/offer gate hides the book
for anything but `open`.
- Bandarmology follow-ups: a daily series (per-session readings and the - Bandarmology follow-ups: a daily series (per-session readings and the
cumulative top-N net) and a price-move phase (quiet accumulation, markup), cumulative top-N net) and a price-move phase (quiet accumulation, markup),
calibration of the `top_concentration_v1` thresholds across more sessions, calibration of the `top_concentration_v1` thresholds across more sessions,
@ -26,7 +33,8 @@ history lives in git.
- `watchlist`: named symbol sets usable by other commands - `watchlist`: named symbol sets usable by other commands
- `alerts`: price alerts with a daemon - `alerts`: price alerts with a daemon
- CSV/TSV output - CSV/TSV output
- Additional providers (Alpha Vantage, Twelve Data, IDX official) - Additional providers (Alpha Vantage, Twelve Data); IDX's own site serves
only `stocks flow --source idx` today
## Agent integration ## Agent integration
- JSON Schema export for command output - JSON Schema export for command output
@ -39,11 +47,16 @@ history lives in git.
- `stocks flow --source idx` depends on IDX's Cloudflare passing a - `stocks flow --source idx` depends on IDX's Cloudflare passing a
curl-impersonate profile (only `curl_safari184` in October 2026) and on the curl-impersonate profile (only `curl_safari184` in October 2026) and on the
network; the default IPOT source rounds to one decimal. network; the default IPOT source rounds to one decimal.
- Stockbit's terms call its data delayed at least 10 minutes and for
personal, non-commercial use (on 2026-10-23 the last trade was within a
minute of the clock); its CDN refuses some user agents with a 403
(`UPSTREAMBLOCKED`). Its order-book sizes are not shown: the page does not
say whether they are lots or shares.
- Yahoo can return `429` from datacenter IPs; requests retry with backoff but - Yahoo can return `429` from datacenter IPs; requests retry with backoff but
can still fail. can still fail.
- `stocks technical` reports the SMA200 trend as unavailable when fewer than - `stocks technical` reports the SMA200 trend as unavailable when fewer than
200 daily candles are returned. 200 daily candles are returned.
- MSN's `low-pe` screen returned no priced stocks on 2026-10-14 (upstream; - MSN's `low-pe` screen returns no priced stocks (`PARSEERROR`; still so on
other filters work). 2026-10-23, upstream; other filters work).
- MSN history for IDX is price-only (`1mo`, `3mo`, `1y`); Yahoo stays the - MSN history for IDX is price-only (`1mo`, `3mo`, `1y`); Yahoo stays the
default OHLCV source. default OHLCV source.

View file

@ -5,6 +5,7 @@ pub mod ownership;
pub mod stocks; pub mod stocks;
use clap::{Parser, Subcommand, ValueEnum}; use clap::{Parser, Subcommand, ValueEnum};
use idx_core::config::ProviderKind;
use crate::output::OutputSetting; use crate::output::OutputSetting;
@ -39,6 +40,13 @@ pub struct Cli {
pub quiet: bool, pub quiet: bool,
#[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")] #[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")]
pub verbose: u8, pub verbose: u8,
#[arg(
long,
value_enum,
global = true,
help = "Market data provider [default: general.provider, else msn]"
)]
pub provider: Option<ProviderKind>,
#[arg(long, global = true, help = "Run without network requests")] #[arg(long, global = true, help = "Run without network requests")]
pub offline: bool, pub offline: bool,
#[arg(long, global = true, help = "Bypass local cache")] #[arg(long, global = true, help = "Bypass local cache")]

View file

@ -6,8 +6,8 @@ use crate::config::{HistoryProviderKind, IdxConfig};
use crate::output::{ use crate::output::{
render_bandar, render_broker_summary, render_compare, render_earnings, render_financials, render_bandar, render_broker_summary, render_compare, render_earnings, render_financials,
render_foreign_flow, render_fundamental, render_growth, render_history, render_insights, render_foreign_flow, render_fundamental, render_growth, render_history, render_insights,
render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment, render_intraday, render_news, render_profile, render_quotes, render_risk, render_screener,
render_technical, render_valuation, render_sentiment, render_technical, render_valuation,
}; };
use idx_core::analysis::BandarQuery; use idx_core::analysis::BandarQuery;
use idx_core::analysis::bandar::DEFAULT_TOP; use idx_core::analysis::bandar::DEFAULT_TOP;
@ -126,13 +126,26 @@ pub struct StocksCmd {
pub enum StocksSubcommand { pub enum StocksSubcommand {
#[command( #[command(
about = "Get real-time stock quotes", 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\n echo BBCA BBRI | idx stocks quote -" 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 -\n idx --provider stockbit stocks quote BBCA # with bid/offer while the market is open"
)] )]
Quote { Quote {
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin. /// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
#[arg(required = true)] #[arg(required = true)]
symbols: Vec<String>, symbols: Vec<String>,
}, },
#[command(
about = "Today's session price by the minute (Stockbit)",
long_about = "Today's session of one IDX stock or index, by the minute, from Stockbit's \
public page (no login): the quote, the best bid/offer while the market is open, a sparkline, \
and the last minutes. Delayed at least 10 minutes per Stockbit's terms; prices only, no \
per-minute volume. A stock that did not trade has no minutes, which is not an error. \
Indices use Yahoo's tickers (^JKSE, ^JKLQ45) or Stockbit's (IHSG, LQ45).",
after_help = "Examples:\n idx stocks intraday BBCA\n idx stocks intraday ^JKSE\n idx -o json stocks intraday BBCA | jq '.points[-1]'"
)]
Intraday {
/// Single ticker symbol (e.g. BBCA).
symbol: String,
},
#[command( #[command(
about = "Get historical OHLC data", about = "Get historical OHLC data",
after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk" after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk"
@ -313,7 +326,11 @@ pub fn handle(
StocksSubcommand::Quote { symbols } => { StocksSubcommand::Quote { symbols } => {
let quotes = idx.market().quotes(&expand_symbols(symbols)?, policy)?; let quotes = idx.market().quotes(&expand_symbols(symbols)?, policy)?;
let quotes: Vec<_> = quotes.into_iter().map(|q| q.data).collect(); let quotes: Vec<_> = quotes.into_iter().map(|q| q.data).collect();
render_quotes(&quotes, format, no_color) render_quotes(&quotes, config.core.provider, format, no_color)
}
StocksSubcommand::Intraday { symbol } => {
let day = idx.market().intraday(symbol, policy)?;
render_intraday(&day.data, format, no_color)
} }
StocksSubcommand::History { StocksSubcommand::History {
symbol, symbol,

View file

@ -34,6 +34,9 @@ impl IdxConfig {
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> { pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
let mut core = CoreConfig::load()?; let mut core = CoreConfig::load()?;
core.verbose = cli.verbose > 0; core.verbose = cli.verbose > 0;
if let Some(provider) = cli.provider {
core.provider = provider;
}
let mut setting = OutputSetting::Auto; let mut setting = OutputSetting::Auto;
let mut no_color = false; let mut no_color = false;

View file

@ -14,10 +14,11 @@ use idx_core::analysis::fundamental::{
}; };
use idx_core::analysis::report::TechnicalReport; use idx_core::analysis::report::TechnicalReport;
use idx_core::api::types::{ use idx_core::api::types::{
CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote, CompanyProfile, EarningsReport, FinancialStatements, InsightData, Intraday, NewsItem, Ohlc,
SentimentData, Quote, SentimentData,
}; };
use idx_core::broker::types::BrokerSummary; use idx_core::broker::types::BrokerSummary;
use idx_core::config::ProviderKind;
use idx_core::error::IdxError; use idx_core::error::IdxError;
use idx_core::flow::types::ForeignFlow; use idx_core::flow::types::ForeignFlow;
@ -67,15 +68,30 @@ impl OutputFormat {
pub fn render_quotes( pub fn render_quotes(
quotes: &[Quote], quotes: &[Quote],
provider: ProviderKind,
format: &OutputFormat, format: &OutputFormat,
no_color: bool, no_color: bool,
) -> Result<(), IdxError> { ) -> Result<(), IdxError> {
match format { match format {
OutputFormat::Table if provider == ProviderKind::Stockbit => {
table::print_stockbit_quotes(quotes, no_color)
}
OutputFormat::Table => table::print_quotes(quotes, no_color), OutputFormat::Table => table::print_quotes(quotes, no_color),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes), OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes),
} }
} }
pub fn render_intraday(
day: &Intraday,
format: &OutputFormat,
no_color: bool,
) -> Result<(), IdxError> {
match format {
OutputFormat::Table => table::print_intraday(day, no_color),
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(day),
}
}
pub fn render_broker_summary( pub fn render_broker_summary(
summary: &BrokerSummary, summary: &BrokerSummary,
format: &OutputFormat, format: &OutputFormat,

View file

@ -11,8 +11,8 @@ use idx_core::analysis::fundamental::{
use idx_core::analysis::report::TechnicalReport; use idx_core::analysis::report::TechnicalReport;
use idx_core::analysis::signals::Signal; use idx_core::analysis::signals::Signal;
use idx_core::api::types::{ use idx_core::api::types::{
CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, Intraday,
Quote, SentimentData, NewsItem, Ohlc, Quote, SentimentData,
}; };
use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary}; use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary};
use idx_core::error::IdxError; use idx_core::error::IdxError;
@ -61,12 +61,7 @@ pub fn print_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
]); ]);
for q in quotes { for q in quotes {
let pct_cell = match q.change_pct { let pct_cell = pct_cell(q.change_pct, no_color);
None => cell("-"),
Some(pct) if no_color => cell(format!("{pct:+.2}%")),
Some(pct) if pct >= 0.0 => cell(format!("{pct:+.2}%")).fg(Color::Green),
Some(pct) => cell(format!("{pct:+.2}%")).fg(Color::Red),
};
table.add_row(vec![ table.add_row(vec![
cell(&q.symbol), cell(&q.symbol),
cell(format_idr(q.price)), cell(format_idr(q.price)),
@ -121,6 +116,182 @@ fn stale_quote_notes(quotes: &[Quote], now: chrono::DateTime<chrono::Utc>) -> Ve
.collect() .collect()
} }
fn pct_cell(change_pct: Option<f64>, no_color: bool) -> Cell {
match change_pct {
None => cell("-"),
Some(pct) if no_color => cell(format!("{pct:+.2}%")),
Some(pct) if pct >= 0.0 => cell(format!("{pct:+.2}%")).fg(Color::Green),
Some(pct) => cell(format!("{pct:+.2}%")).fg(Color::Red),
}
}
/// Printed under every Stockbit table: what the data is and is not.
const STOCKBIT_SOURCE_NOTE: &str = "Source: Stockbit's public page, delayed at least 10 \
minutes (Stockbit's terms); bid/offer only while the market is open.";
fn opt_idr(value: Option<i64>) -> String {
value.map(format_idr).unwrap_or_else(|| "-".to_string())
}
fn wib_minute(time: chrono::DateTime<chrono::FixedOffset>) -> String {
time.format("%Y-%m-%d %H:%M").to_string()
}
/// Quotes from `--provider stockbit`: Stockbit has no market cap or 52-week
/// range, but a best bid/offer and the market's state.
pub fn print_stockbit_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec![
"SYMBOL", "PRICE", "CHG", "CHG%", "VOLUME", "BID", "OFFER", "MARKET", "AS OF",
]);
for q in quotes {
table.add_row(vec![
cell(&q.symbol),
cell(format_idr(q.price)),
cell(
q.change
.map(|c| format!("{c:+}"))
.unwrap_or_else(|| "-".to_string()),
),
pct_cell(q.change_pct, no_color),
cell(q.volume.map(format_u64).unwrap_or_else(|| "-".to_string())),
cell(opt_idr(q.bid)),
cell(opt_idr(q.offer)),
cell(q.market_status.as_deref().unwrap_or("-")),
cell(q.as_of.map(wib_minute).unwrap_or_else(|| "-".to_string())),
]);
}
println!("{table}");
for q in quotes {
if let Some(note) = &q.session_note {
println!(
"Note: {}: {}",
terminal_safe(&q.symbol),
terminal_safe(note)
);
}
}
for note in stale_quote_notes(quotes, chrono::Utc::now()) {
println!("{}", terminal_safe(&note));
}
println!("{STOCKBIT_SOURCE_NOTE}");
Ok(())
}
/// Minutes listed under the intraday chart.
const INTRADAY_TAIL: usize = 5;
/// Widest the intraday sparkline gets, in characters.
const SPARKLINE_WIDTH: usize = 60;
pub fn print_intraday(day: &Intraday, no_color: bool) -> Result<(), IdxError> {
let q = &day.quote;
let change = match (q.change, q.change_pct) {
(Some(c), Some(p)) => format!(" {c:+} ({p:+.2}%)"),
(Some(c), None) => format!(" {c:+}"),
_ => String::new(),
};
let change = match q.change {
_ if no_color || change.is_empty() => change,
Some(c) if c >= 0 => change.green(),
_ => change.red(),
};
println!(
"{}{change}",
format!("{} {}", terminal_safe(&q.symbol), format_idr(q.price)).bold()
);
let mut facts = vec![
format!("prev close {}", opt_idr(q.prev_close)),
format!(
"volume {}",
q.volume.map(format_u64).unwrap_or_else(|| "-".to_string())
),
format!(
"market {}",
terminal_safe(q.market_status.as_deref().unwrap_or("unknown"))
),
];
if let Some(as_of) = q.as_of {
facts.push(format!("last trade {} WIB", wib_minute(as_of)));
}
println!("{}", facts.join(" · "));
if q.bid.is_some() || q.offer.is_some() {
println!("bid {} · offer {}", opt_idr(q.bid), opt_idr(q.offer));
}
if let Some(note) = &q.session_note {
println!("Note: {}", terminal_safe(note));
}
let prices: Vec<f64> = day.points.iter().map(|p| p.price).collect();
if let (Some(first), Some(last)) = (day.points.first(), day.points.last()) {
let (low, high) = prices
.iter()
.fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(*p), hi.max(*p)));
println!();
println!("{}", sparkline(&prices, SPARKLINE_WIDTH));
println!(
"{} to {} WIB · low {} · high {} · {} minutes",
first.time.format("%H:%M"),
last.time.format("%H:%M"),
format_price(low),
format_price(high),
day.points.len()
);
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_header(vec!["TIME", "PRICE"]);
let start = day.points.len().saturating_sub(INTRADAY_TAIL);
for point in &day.points[start..] {
table.add_row(vec![
cell(point.time.format("%H:%M")),
cell(format_price(point.price)),
]);
}
println!("{table}");
} else if q.session_note.is_none() {
println!("No trades this session.");
}
println!("{STOCKBIT_SOURCE_NOTE}");
Ok(())
}
/// Whole Rupiah for stocks, two decimals for an index level.
fn format_price(price: f64) -> String {
let cents = (price * 100.0).round() as i64;
if cents % 100 == 0 {
format_idr(cents / 100)
} else {
format!("{}.{:02}", format_idr(cents / 100), (cents % 100).abs())
}
}
/// One block per bucket of points (the bucket's last price), scaled from the
/// lowest to the highest price.
fn sparkline(prices: &[f64], width: usize) -> String {
const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
let width = width.min(prices.len());
if width == 0 {
return String::new();
}
let (low, high) = prices
.iter()
.fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(*p), hi.max(*p)));
(1..=width)
.map(|i| {
let price = prices[i * prices.len() / width - 1];
if high > low {
BARS[((price - low) / (high - low) * 7.0).round() as usize]
} else {
BARS[3]
}
})
.collect()
}
pub fn print_history(symbol: &str, history: &[Ohlc]) -> Result<(), IdxError> { pub fn print_history(symbol: &str, history: &[Ohlc]) -> Result<(), IdxError> {
println!( println!(
"{}", "{}",
@ -1410,6 +1581,10 @@ mod tests {
avg_volume: None, avg_volume: None,
as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()), as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()),
behind_session: false, behind_session: false,
bid: None,
offer: None,
market_status: None,
session_note: None,
} }
} }

View file

@ -2116,6 +2116,27 @@ fn invalid_provider_env_honors_json_output() {
); );
} }
#[test]
fn provider_flag_overrides_env() {
let quotes = run_success_json(
test_bin("provider-flag")
.env("IDX_PROVIDER", "msn")
.env("IDX_USE_MOCK_PROVIDER", "1")
.args([
"-o",
"json",
"--provider",
"stockbit",
"stocks",
"quote",
"BBCA",
]),
);
// Only Stockbit reports the market's state; MSN has a market cap.
assert_eq!(quotes[0]["market_status"], "close");
assert_eq!(quotes[0]["market_cap"], Value::Null);
}
#[test] #[test]
fn invalid_quote_ttl_env_returns_non_zero() { fn invalid_quote_ttl_env_returns_non_zero() {
test_bin("invalid-quote-ttl") test_bin("invalid-quote-ttl")

View file

@ -244,6 +244,25 @@ name = "yahoo-fundamental"
args = ["stocks", "fundamental", "BBCA"] args = ["stocks", "fundamental", "BBCA"]
env = { IDX_PROVIDER = "yahoo" } env = { IDX_PROVIDER = "yahoo" }
# --- stocks: Stockbit (intraday, and the opt-in quote provider) -----------------
[[case]]
name = "stocks-intraday"
args = ["stocks", "intraday", "BBCA"]
[[case]]
name = "stocks-intraday-index"
args = ["stocks", "intraday", "^JKSE"]
formats = ["table"]
[[case]]
name = "stocks-intraday-suspended"
args = ["stocks", "intraday", "MABA"]
[[case]]
name = "stockbit-quote"
args = ["--provider", "stockbit", "stocks", "quote", "BBCA,MABA"]
# --- stocks: broker summaries (IPOT) and foreign flow (IDX) -------------------- # --- stocks: broker summaries (IPOT) and foreign flow (IDX) --------------------
[[case]] [[case]]
@ -334,6 +353,19 @@ env = { IDX_MOCK_ERROR = "1" }
name = "error-fetch-bing-unsupported" name = "error-fetch-bing-unsupported"
args = ["ownership", "import", "--fetch-bing", "BBCA"] args = ["ownership", "import", "--fetch-bing", "BBCA"]
[[case]]
name = "error-intraday-blocked"
args = ["stocks", "intraday", "BBCA"]
env = { IDX_MOCK_STOCKBIT_STATUS = "403" }
[[case]]
name = "error-intraday-unknown-symbol"
args = ["stocks", "intraday", "ZZZZ"]
[[case]]
name = "error-stockbit-fundamental-unsupported"
args = ["--provider", "stockbit", "stocks", "fundamental", "BBCA"]
# --- ownership -------------------------------------------------------------------- # --- ownership --------------------------------------------------------------------
[[case]] [[case]]

View file

@ -204,6 +204,9 @@ _idx() {
idx__help__stocks,insights) idx__help__stocks,insights)
cmd="idx__help__stocks__insights" cmd="idx__help__stocks__insights"
;; ;;
idx__help__stocks,intraday)
cmd="idx__help__stocks__intraday"
;;
idx__help__stocks,news) idx__help__stocks,news)
cmd="idx__help__stocks__news" cmd="idx__help__stocks__news"
;; ;;
@ -378,6 +381,9 @@ _idx() {
idx__stocks,insights) idx__stocks,insights)
cmd="idx__stocks__insights" cmd="idx__stocks__insights"
;; ;;
idx__stocks,intraday)
cmd="idx__stocks__intraday"
;;
idx__stocks,news) idx__stocks,news)
cmd="idx__stocks__news" cmd="idx__stocks__news"
;; ;;
@ -435,6 +441,9 @@ _idx() {
idx__stocks__help,insights) idx__stocks__help,insights)
cmd="idx__stocks__help__insights" cmd="idx__stocks__help__insights"
;; ;;
idx__stocks__help,intraday)
cmd="idx__stocks__help__intraday"
;;
idx__stocks__help,news) idx__stocks__help,news)
cmd="idx__stocks__help__news" cmd="idx__stocks__help__news"
;; ;;
@ -466,7 +475,7 @@ _idx() {
case "${cmd}" in case "${cmd}" in
idx) idx)
opts="-o -q -v -V -h --output --no-color --quiet --verbose --offline --no-cache --version --help stocks config cache ownership completions version help" opts="-o -q -v -V -h --output --no-color --quiet --verbose --provider --offline --no-cache --version --help stocks config cache ownership completions version help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -480,6 +489,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -488,7 +501,7 @@ _idx() {
return 0 return 0
;; ;;
idx__cache) idx__cache)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help info clear help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help info clear help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -502,6 +515,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -510,7 +527,7 @@ _idx() {
return 0 return 0
;; ;;
idx__cache__clear) idx__cache__clear)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -524,6 +541,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -588,7 +609,7 @@ _idx() {
return 0 return 0
;; ;;
idx__cache__info) idx__cache__info)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -602,6 +623,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -610,7 +635,7 @@ _idx() {
return 0 return 0
;; ;;
idx__completions) idx__completions)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help bash zsh fish" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help bash zsh fish"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -624,6 +649,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -632,7 +661,7 @@ _idx() {
return 0 return 0
;; ;;
idx__config) idx__config)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help init get set path help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help init get set path help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -646,6 +675,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -654,7 +687,7 @@ _idx() {
return 0 return 0
;; ;;
idx__config__get) idx__config__get)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEY>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -668,6 +701,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -760,7 +797,7 @@ _idx() {
return 0 return 0
;; ;;
idx__config__init) idx__config__init)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -774,6 +811,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -782,7 +823,7 @@ _idx() {
return 0 return 0
;; ;;
idx__config__path) idx__config__path)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -796,6 +837,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -804,7 +849,7 @@ _idx() {
return 0 return 0
;; ;;
idx__config__set) idx__config__set)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEY> <VALUE>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY> <VALUE>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -818,6 +863,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1218,7 +1267,7 @@ _idx() {
return 0 return 0
;; ;;
idx__help__stocks) idx__help__stocks)
opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare" opts="quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1371,6 +1420,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__help__stocks__intraday)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__help__stocks__news) idx__help__stocks__news)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -1498,7 +1561,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership) idx__ownership)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help discover import sync ticker entity search cross-holders concentration flow changes graph resolve releases help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help discover import sync ticker entity search cross-holders concentration flow changes graph resolve releases help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1512,6 +1575,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1520,7 +1587,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__changes) idx__ownership__changes)
opts="-o -q -v -h --from --to --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --from --to --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1542,6 +1609,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1550,7 +1621,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__concentration) idx__ownership__concentration)
opts="-o -q -v -h --by --top --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --by --top --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1572,6 +1643,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1580,7 +1655,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__cross__holders) idx__ownership__cross__holders)
opts="-o -q -v -h --top --min-tickers --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --top --min-tickers --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1602,6 +1677,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1610,7 +1689,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__discover) idx__ownership__discover)
opts="-o -q -v -h --family --limit --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --family --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1632,6 +1711,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1640,7 +1723,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__entity) idx__ownership__entity)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <NAME>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <NAME>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1654,6 +1737,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1662,7 +1749,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__flow) idx__ownership__flow)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1676,6 +1763,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1684,7 +1775,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__graph) idx__ownership__graph)
opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --offline --no-cache --help <ROOT>" opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --provider --offline --no-cache --help <ROOT>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1706,6 +1797,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -1966,7 +2061,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__import) idx__ownership__import)
opts="-o -q -v -h --url --file --fetch-bing --force --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --url --file --fetch-bing --force --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -1992,6 +2087,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2000,7 +2099,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__releases) idx__ownership__releases)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2014,6 +2113,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2022,7 +2125,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__resolve) idx__ownership__resolve)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help unresolved map merge help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help unresolved map merge help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2036,6 +2139,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2114,7 +2221,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__resolve__map) idx__ownership__resolve__map)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <ALIAS> <ENTITY>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <ALIAS> <ENTITY>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2128,6 +2235,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2136,7 +2247,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__resolve__merge) idx__ownership__resolve__merge)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEEP> <MERGE>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEEP> <MERGE>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2150,6 +2261,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2158,7 +2273,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__resolve__unresolved) idx__ownership__resolve__unresolved)
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2176,6 +2291,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2184,7 +2303,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__search) idx__ownership__search)
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help <QUERY>" opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <QUERY>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2202,6 +2321,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2210,7 +2333,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__sync) idx__ownership__sync)
opts="-o -q -v -h --manifest --force --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --manifest --force --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2228,6 +2351,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2236,7 +2363,7 @@ _idx() {
return 0 return 0
;; ;;
idx__ownership__ticker) idx__ownership__ticker)
opts="-o -q -v -h --source --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2254,6 +2381,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2262,7 +2393,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks) idx__stocks)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2276,6 +2407,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2284,7 +2419,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__bandar) idx__stocks__bandar)
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2322,6 +2457,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2330,7 +2469,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__broker) idx__stocks__broker)
opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2364,6 +2503,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2372,7 +2515,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__compare) idx__stocks__compare)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2386,6 +2529,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2394,7 +2541,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__earnings) idx__stocks__earnings)
opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2408,6 +2555,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2416,7 +2567,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__financials) idx__stocks__financials)
opts="-o -q -v -h --statement --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --statement --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2434,6 +2585,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2442,7 +2597,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__flow) idx__stocks__flow)
opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2472,6 +2627,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2480,7 +2639,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__fundamental) idx__stocks__fundamental)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2494,6 +2653,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2502,7 +2665,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__growth) idx__stocks__growth)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2516,6 +2679,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2524,7 +2691,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__help) idx__stocks__help)
opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help" opts="quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2691,6 +2858,20 @@ _idx() {
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
;; ;;
idx__stocks__help__intraday)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__help__news) idx__stocks__help__news)
opts="" opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
@ -2804,7 +2985,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__history) idx__stocks__history)
opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2830,6 +3011,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2838,7 +3023,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__insights) idx__stocks__insights)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2852,6 +3037,36 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
idx__stocks__intraday)
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--output)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
-o)
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0
;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2860,7 +3075,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__news) idx__stocks__news)
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2878,6 +3093,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2886,7 +3105,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__profile) idx__stocks__profile)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2900,6 +3119,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2908,7 +3131,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__quote) idx__stocks__quote)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..." opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2922,6 +3145,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2930,7 +3157,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__risk) idx__stocks__risk)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2944,6 +3171,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2952,7 +3183,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__screen) idx__stocks__screen)
opts="-o -q -v -h --filter --region --limit --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --filter --region --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -2978,6 +3209,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -2986,7 +3221,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__sentiment) idx__stocks__sentiment)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -3000,6 +3235,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -3008,7 +3247,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__technical) idx__stocks__technical)
opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -3026,6 +3265,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -3034,7 +3277,7 @@ _idx() {
return 0 return 0
;; ;;
idx__stocks__valuation) idx__stocks__valuation)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -3048,6 +3291,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;
@ -3056,7 +3303,7 @@ _idx() {
return 0 return 0
;; ;;
idx__version) idx__version)
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help" opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0
@ -3070,6 +3317,10 @@ _idx() {
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
return 0 return 0
;; ;;
--provider)
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
return 0
;;
*) *)
COMPREPLY=() COMPREPLY=()
;; ;;

View file

@ -0,0 +1,4 @@
exit: 4
--- stdout
--- stderr
{"code":"UPSTREAMBLOCKED","error":true,"message":"upstream blocked: stockbit.com refused this client (HTTP 403 from its CDN); idx does not retry a block. Try again later or from another network; `--provider msn` serves quotes without Stockbit"}

View file

@ -0,0 +1,4 @@
exit: 4
--- stdout
--- stderr
Error: upstream blocked: stockbit.com refused this client (HTTP 403 from its CDN); idx does not retry a block. Try again later or from another network; `--provider msn` serves quotes without Stockbit

View file

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

View file

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

View file

@ -1,4 +1,4 @@
exit: 2 exit: 2
--- stdout --- stdout
--- stderr --- 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, msn, or stockbit)"}

View file

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

View file

@ -0,0 +1,4 @@
exit: 5
--- stdout
--- stderr
{"code":"UNSUPPORTED","error":true,"message":"unsupported: BBCA.JK: Stockbit has no fundamentals; command requires --provider msn or yahoo"}

View file

@ -0,0 +1,4 @@
exit: 5
--- stdout
--- stderr
Error: unsupported: BBCA.JK: Stockbit has no fundamentals; command requires --provider msn or yahoo

View file

@ -10,11 +10,12 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
--- stderr --- stderr

View file

@ -28,6 +28,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests

View file

@ -40,6 +40,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests

View file

@ -29,6 +29,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests

View file

@ -6,6 +6,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
Commands: Commands:
quote Get real-time stock quotes quote Get real-time stock quotes
intraday Today's session price by the minute (Stockbit)
history Get historical OHLC data history Get historical OHLC data
technical Run technical analysis on a stock technical Run technical analysis on a stock
growth Run growth analysis on a stock growth Run growth analysis on a stock
@ -26,11 +27,12 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
--- stderr --- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
[{"symbol":"BBCA.JK","price":5175,"change":-100,"change_pct":-1.9,"volume":286928500,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":5275,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close"},{"symbol":"MABA.JK","price":50,"change":0,"change_pct":0.0,"volume":0,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":50,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close","session_note":"Stockbit lists the stock as suspended"}]
--- stderr

View file

@ -0,0 +1,12 @@
exit: 0
--- stdout
┌─────────┬───────┬──────┬────────┬─────────────┬─────┬───────┬────────┬───────┐
│ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ BID ┆ OFFER ┆ MARKET ┆ AS OF │
╞═════════╪═══════╪══════╪════════╪═════════════╪═════╪═══════╪════════╪═══════╡
│ BBCA.JK ┆ 5,175 ┆ -100 ┆ -1.90% ┆ 286,928,500 ┆ - ┆ - ┆ close ┆ - │
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ MABA.JK ┆ 50 ┆ +0 ┆ +0.00% ┆ 0 ┆ - ┆ - ┆ close ┆ - │
└─────────┴───────┴──────┴────────┴─────────────┴─────┴───────┴────────┴───────┘
Note: MABA.JK: Stockbit lists the stock as suspended
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
--- stderr

View file

@ -0,0 +1,22 @@
exit: 0
--- stdout
^JKSE.JK 5,388 +60 (+1.12%)
prev close 5,328 · volume 36,446,806,700 · market close · last trade 2026-10-22 16:14 WIB
▃▄▄▆▆▆▆▆▆▇▇▇▇▇████▇▇▇▇███▇▇▇▇███▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇
08:58 to 16:14 WIB · low 5,192.41 · high 5,419.34 · 335 minutes
┌───────┬──────────┐
│ TIME ┆ PRICE │
╞═══════╪══════════╡
│ 16:10 ┆ 5,387.90 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 16:11 ┆ 5,387.90 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 16:12 ┆ 5,387.90 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 16:13 ┆ 5,387.90 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌┤
│ 16:14 ┆ 5,387.90 │
└───────┴──────────┘
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
--- stderr

View file

@ -0,0 +1,4 @@
exit: 0
--- stdout
{"quote":{"symbol":"MABA.JK","price":50,"change":0,"change_pct":0.0,"volume":0,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":50,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close","session_note":"Stockbit lists the stock as suspended"},"points":[]}
--- stderr

View file

@ -0,0 +1,7 @@
exit: 0
--- stdout
MABA.JK 50 +0 (+0.00%)
prev close 50 · volume 0 · market close
Note: Stockbit lists the stock as suspended
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
--- stderr

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,22 @@
exit: 0
--- stdout
BBCA.JK 5,175 -100 (-1.90%)
prev close 5,275 · volume 286,928,500 · market close · last trade 2026-10-22 16:14 WIB
▄▅▇█▅▇▇▇▅▇▇▇▇▇▇██▇▅▇▅▅███▇▅▇▅▇▇▇▅▇▅▅▄▅▄▅▄▄▅▅▅▅▅▇▅▄▄▄▄▄▄▂▄▄▄▄
08:58 to 16:14 WIB · low 5,125 · high 5,250 · 335 minutes
┌───────┬───────┐
│ TIME ┆ PRICE │
╞═══════╪═══════╡
│ 16:10 ┆ 5,175 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 16:11 ┆ 5,175 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 16:12 ┆ 5,175 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 16:13 ┆ 5,175 │
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
│ 16:14 ┆ 5,175 │
└───────┴───────┘
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
--- stderr

View file

@ -1,10 +1,10 @@
//! Fixture-backed provider for tests, enabled by the `testing` feature and //! Fixture-backed provider for tests, enabled by the `testing` feature and
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it. //! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
use super::types::{Bar, Fundamentals, Interval, Period, Quote}; use super::types::{Bar, Fundamentals, Interval, Intraday, Period, Quote};
use super::{ use super::{
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, QuoteProvider, BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, IntradayProvider, QuoteProvider,
TradingSummaryProvider, idx, ipot, msn, yahoo, TradingSummaryProvider, idx, ipot, msn, stockbit, yahoo,
}; };
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery}; use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
use crate::config::ProviderKind; use crate::config::ProviderKind;
@ -45,6 +45,10 @@ impl MockProvider {
match provider { match provider {
ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose), ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose),
ProviderKind::Msn => Self::from_msn_fixtures(), ProviderKind::Msn => Self::from_msn_fixtures(),
// `MockStockbitProvider` serves Stockbit; it has no history.
ProviderKind::Stockbit => Self::with_error(IdxError::Unsupported(
"Stockbit serves no price history".to_string(),
)),
} }
} }
@ -131,6 +135,57 @@ impl HistoryProvider for MockProvider {
} }
} }
/// Stockbit symbol pages from `tests/fixtures/stockbit_symbol_<code>.html`;
/// a code without a fixture is Stockbit's 404. `IDX_MOCK_STOCKBIT_STATUS`
/// (e.g. `403`) answers that HTTP status instead, and `IDX_MOCK_ERROR` fails
/// as for the other providers.
pub struct MockStockbitProvider;
impl MockStockbitProvider {
fn snapshot(&self, symbol: &str) -> Result<Intraday, IdxError> {
if std::env::var("IDX_MOCK_ERROR").is_ok() {
return Err(IdxError::ProviderUnavailable);
}
if let Ok(status) = std::env::var("IDX_MOCK_STOCKBIT_STATUS") {
return Err(stockbit::status_error(
status.parse().unwrap_or(500),
symbol,
));
}
let code = stockbit::stockbit_code(symbol)?;
let path = format!(
"tests/fixtures/stockbit_symbol_{}.html",
code.to_ascii_lowercase()
);
match std::fs::read_to_string(path) {
Ok(html) => stockbit::parse_symbol_page(symbol, &html),
Err(_) => Err(stockbit::status_error(404, symbol)),
}
}
}
impl QuoteProvider for MockStockbitProvider {
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
let mut quote = self.snapshot(symbol)?.quote;
// Quotes are judged against the clock (behind the session, a week
// old); a fixed trade time would make their output age.
quote.as_of = None;
Ok(quote)
}
}
impl FundamentalsProvider for MockStockbitProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
Err(stockbit::fundamentals_unsupported(symbol))
}
}
impl IntradayProvider for MockStockbitProvider {
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError> {
self.snapshot(symbol)
}
}
/// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from> /// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from>
/// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an /// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an
/// unknown code. /// unknown code.

View file

@ -3,6 +3,7 @@ pub mod ipot;
#[cfg(feature = "testing")] #[cfg(feature = "testing")]
pub mod mock; pub mod mock;
pub mod msn; pub mod msn;
pub mod stockbit;
pub mod types; pub mod types;
pub mod yahoo; pub mod yahoo;
@ -13,7 +14,7 @@ use crate::config::{HistoryProviderKind, ProviderKind};
use crate::error::IdxError; use crate::error::IdxError;
use types::{ use types::{
Bar, CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval, Bar, CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval,
NewsItem, Period, Quote, SentimentData, Intraday, NewsItem, Period, Quote, SentimentData,
}; };
/// IDX trades in Western Indonesia Time (WIB, UTC+7, no DST). Providers stamp /// IDX trades in Western Indonesia Time (WIB, UTC+7, no DST). Providers stamp
@ -64,6 +65,13 @@ pub trait TradingSummaryProvider {
) -> Result<Vec<crate::flow::types::StockDay>, IdxError>; ) -> Result<Vec<crate::flow::types::StockDay>, IdxError>;
} }
/// Today's session of one IDX symbol: quote and price per minute. Served by
/// Stockbit's public page whichever market provider is selected.
pub trait IntradayProvider {
/// `symbol` is resolved, e.g. `BBCA.JK`.
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError>;
}
pub trait FundamentalsProvider { pub trait FundamentalsProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>; fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
} }
@ -221,7 +229,10 @@ fn mock_enabled() -> bool {
#[cfg(feature = "testing")] #[cfg(feature = "testing")]
fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> { fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
Some(Box::new(mock::MockProvider::from_fixtures(provider))) match provider {
ProviderKind::Stockbit => Some(Box::new(mock::MockStockbitProvider)),
_ => Some(Box::new(mock::MockProvider::from_fixtures(provider))),
}
} }
#[cfg(not(feature = "testing"))] #[cfg(not(feature = "testing"))]
@ -274,6 +285,18 @@ fn build_selected_provider(
news: Some(Box::new(msn::MsnProvider::new(verbose))), news: Some(Box::new(msn::MsnProvider::new(verbose))),
screener: Some(Box::new(msn::MsnProvider::new(verbose))), screener: Some(Box::new(msn::MsnProvider::new(verbose))),
}, },
// Quotes only; history falls back to Yahoo (see `history_source`).
ProviderKind::Stockbit => SelectedProvider {
kind: ProviderKind::Stockbit,
market: mock.unwrap_or_else(|| Box::new(stockbit::StockbitProvider::new())),
profile: None,
financials: None,
earnings: None,
sentiment: None,
insights: None,
news: None,
screener: None,
},
} }
} }
@ -297,6 +320,14 @@ pub fn trading_summary_provider() -> Box<dyn TradingSummaryProvider> {
Box::new(idx::IdxSiteProvider) Box::new(idx::IdxSiteProvider)
} }
pub fn intraday_provider() -> Box<dyn IntradayProvider> {
#[cfg(feature = "testing")]
if mock::enabled() {
return Box::new(mock::MockStockbitProvider);
}
Box::new(stockbit::StockbitProvider::new())
}
/// Today in WIB, the date IDX trades on. /// Today in WIB, the date IDX trades on.
pub(crate) fn today_wib() -> chrono::NaiveDate { pub(crate) fn today_wib() -> chrono::NaiveDate {
#[cfg(feature = "testing")] #[cfg(feature = "testing")]
@ -312,8 +343,7 @@ pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind)
HistoryProviderKind::Yahoo => ProviderKind::Yahoo, HistoryProviderKind::Yahoo => ProviderKind::Yahoo,
HistoryProviderKind::Msn => ProviderKind::Msn, HistoryProviderKind::Msn => ProviderKind::Msn,
HistoryProviderKind::Auto => match provider { HistoryProviderKind::Auto => match provider {
ProviderKind::Yahoo => ProviderKind::Yahoo, ProviderKind::Yahoo | ProviderKind::Msn | ProviderKind::Stockbit => ProviderKind::Yahoo,
ProviderKind::Msn => ProviderKind::Yahoo,
}, },
} }
} }
@ -339,6 +369,9 @@ pub fn history_provider(
match resolved { match resolved {
ProviderKind::Yahoo => Ok((resolved, Box::new(yahoo::YahooProvider::new(verbose)))), ProviderKind::Yahoo => Ok((resolved, Box::new(yahoo::YahooProvider::new(verbose)))),
ProviderKind::Msn => Ok((resolved, Box::new(msn::MsnProvider::new(verbose)))), ProviderKind::Msn => Ok((resolved, Box::new(msn::MsnProvider::new(verbose)))),
ProviderKind::Stockbit => Err(IdxError::Unsupported(
"Stockbit serves no price history; use --history-provider yahoo or msn".to_string(),
)),
} }
} }

View file

@ -71,6 +71,10 @@ pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result<Quote, Id
avg_volume: round_u64(quote.average_volume), avg_volume: round_u64(quote.average_volume),
as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()), as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()),
behind_session: false, behind_session: false,
bid: None,
offer: None,
market_status: None,
session_note: None,
}) })
} }
@ -720,6 +724,10 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
avg_volume: round_u64(q.average_volume), avg_volume: round_u64(q.average_volume),
as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()), as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()),
behind_session: false, behind_session: false,
bid: None,
offer: None,
market_status: None,
session_note: None,
}) })
}) })
.collect(); .collect();

View file

@ -0,0 +1,193 @@
//! Stockbit's public symbol page: a quote, the best bid/offer and today's
//! price per minute for one IDX symbol, without a login or key.
//!
//! `GET https://stockbit.com/symbol/{CODE}` server-renders everything into
//! the page's `__NEXT_DATA__` JSON (see `parse`). Stockbit's own JSON API
//! (`exodus.stockbit.com`) needs a logged-in bearer token and is not used.
//!
//! Access rules this module keeps:
//! - An honest user agent. CloudFront in front of the page answers a static
//! HTTP 403 to `curl/`, `axios/`, `Go-http-client/` and empty agents; it
//! does not fingerprint TLS, so no browser impersonation is involved.
//! - One request per symbol, at least `MIN_INTERVAL` apart across every idx
//! process of the user (see `pace`), no retries, and a 403 surfaces as
//! `UPSTREAMBLOCKED` at once.
//! - Stockbit's terms describe its market data as delayed at least 10
//! minutes and for personal use; frontends say so.
mod parse;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
pub use parse::{parse_symbol_page, status_error, stockbit_code};
use super::types::{Fundamentals, Intraday, Quote};
use super::{FundamentalsProvider, IntradayProvider, QuoteProvider};
use crate::error::IdxError;
pub const SOURCE: &str = "stockbit";
/// Identifies idx-cli and where to reach its maintainers.
pub const USER_AGENT: &str = concat!(
"idx-cli/",
env!("CARGO_PKG_VERSION"),
" (+",
env!("CARGO_PKG_REPOSITORY"),
")"
);
/// Longest a snapshot is served from cache: enough to coalesce a burst (an
/// agent asking for a quote and the intraday series), short enough that a
/// session view stays current.
pub(crate) const CACHE_TTL_SECS: u64 = 5;
/// Least time between two requests to stockbit.com from one process.
const MIN_INTERVAL: Duration = Duration::from_secs(1);
const BASE_URL: &str = "https://stockbit.com/symbol/";
pub struct StockbitProvider {
agent: ureq::Agent,
}
impl StockbitProvider {
pub fn new() -> Self {
let agent = ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(5)))
.timeout_recv_body(Some(Duration::from_secs(10)))
// A server that stalls before sending headers must not hold a
// call (and an MCP call slot) forever.
.timeout_global(Some(Duration::from_secs(20)))
.build()
.into();
Self { agent }
}
/// The quote and today's series for a resolved symbol (`BBCA.JK`).
pub fn snapshot(&self, resolved: &str) -> Result<Intraday, IdxError> {
let code = stockbit_code(resolved)?;
let html = self.page(&code, resolved)?;
parse_symbol_page(resolved, &html)
}
fn page(&self, code: &str, resolved: &str) -> Result<String, IdxError> {
pace();
let response = self
.agent
.get(format!("{BASE_URL}{code}"))
.header("User-Agent", USER_AGENT)
.header("Accept", "text/html")
.call();
match response {
Ok(ok) => ok
.into_body()
.read_to_string()
.map_err(|e| IdxError::Http(format!("stockbit: {e}"))),
Err(ureq::Error::StatusCode(status)) => Err(status_error(status, resolved)),
Err(err) => Err(IdxError::Http(format!("stockbit: {err}"))),
}
}
}
impl Default for StockbitProvider {
fn default() -> Self {
Self::new()
}
}
/// Waits until `MIN_INTERVAL` has passed since the previous request. Threads
/// of one process queue on a mutex; separate processes (a shell loop of
/// `idx` calls, a CLI beside idx-mcp) share the time of the last request in
/// `<cache>/stockbit.pace` under an exclusive file lock. When that file is
/// unusable, pacing is per process.
fn pace() {
static LAST: Mutex<Option<Instant>> = Mutex::new(None);
let mut last = LAST.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
if let Some(prev) = *last {
let wait = MIN_INTERVAL.saturating_sub(prev.elapsed());
if !wait.is_zero() {
std::thread::sleep(wait);
}
}
// Best effort: the in-process pacing above already holds.
let _ = pace_across_processes();
*last = Some(Instant::now());
}
fn pace_across_processes() -> std::io::Result<()> {
let dir = crate::cache::cache_dir().map_err(|e| std::io::Error::other(e.to_string()))?;
std::fs::create_dir_all(&dir)?;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(dir.join("stockbit.pace"))?;
// Released when `file` is dropped, after the new time is written.
file.lock()?;
let mut raw = String::new();
file.read_to_string(&mut raw)?;
if let Ok(prev_ms) = raw.trim().parse::<u128>() {
// A clock that went backwards waits at most one interval.
let elapsed = now_ms().saturating_sub(prev_ms);
let wait = MIN_INTERVAL.as_millis().saturating_sub(elapsed);
if wait > 0 {
std::thread::sleep(Duration::from_millis(wait as u64));
}
}
stamp(&mut file)
}
fn stamp(file: &mut File) -> std::io::Result<()> {
file.set_len(0)?;
file.seek(SeekFrom::Start(0))?;
write!(file, "{}", now_ms())
}
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis())
}
impl QuoteProvider for StockbitProvider {
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
self.snapshot(symbol).map(|day| day.quote)
}
}
impl FundamentalsProvider for StockbitProvider {
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
Err(fundamentals_unsupported(symbol))
}
}
impl IntradayProvider for StockbitProvider {
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError> {
self.snapshot(symbol)
}
}
pub(crate) fn fundamentals_unsupported(symbol: &str) -> IdxError {
IdxError::Unsupported(format!(
"{symbol}: Stockbit has no fundamentals; command requires --provider msn or yahoo"
))
}
#[cfg(test)]
mod tests {
use super::USER_AGENT;
/// CloudFront answers HTTP 403 to agents containing these (probed
/// 2026-10-22); the honest agent must never match one.
#[test]
fn user_agent_is_honest_and_not_on_the_cdn_blocklist() {
assert!(USER_AGENT.starts_with("idx-cli/"));
assert!(USER_AGENT.contains("github.com/0xrsydn/idx-cli"));
for blocked in ["curl/", "axios/", "Go-http-client/"] {
assert!(
!USER_AGENT.contains(blocked),
"{USER_AGENT} contains {blocked}"
);
}
}
}

View file

@ -0,0 +1,440 @@
//! Pure parsing of a Stockbit symbol page: the `__NEXT_DATA__` JSON that
//! Next.js renders into `https://stockbit.com/symbol/{CODE}`. No I/O.
//!
//! Shapes (recorded 2026-10-22 after the close, see
//! `tests/fixtures/stockbit_symbol_*.html`):
//! - `props.pageProps.company`: the quote. Numbers are mostly strings
//! (`price: "5175"`, an index `"5387.90478515625"`, `change: "+59.54"`),
//! `percentage` is a JSON number, `orderbook.{bid,offer}.price` is
//! `"5175.000000"` or `"N/A"`, `market_hour.status` is e.g. `close`, and
//! `status` is `STATUS_ACTIVE`, `STATUS_SUSPENDED` or `STATUS_DELISTED`.
//! `date`/`time` (`"22 Oct 2026"`, `"Thu 16:14"`) are the last trade only
//! when the stock traded: a suspended one carries today's date.
//! - `props.pageProps.chart.prices[]`: one row per minute,
//! `formatted_date: "2026-10-22 16:14:00"` (WIB, no offset) and
//! `value: "5175"`. The first row is a synthetic anchor at the previous
//! close with an empty `formatted_date`. Per-minute OHLC and volume are
//! empty strings, so only the price is kept.
//! - An unknown code renders the 404 page: `page: "/404"`, empty pageProps.
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
use serde::Deserialize;
use serde_json::Value;
use crate::api::idx_offset;
use crate::api::types::{Intraday, IntradayPoint, Quote};
use crate::error::IdxError;
/// Stockbit's market status while continuous trading runs. Bid and offer are
/// reported only then: outside it they are the closing book.
const MARKET_OPEN: &str = "open";
#[derive(Debug, Deserialize)]
struct NextData {
page: String,
props: Props,
}
#[derive(Debug, Deserialize)]
struct Props {
#[serde(rename = "pageProps", default)]
page_props: PageProps,
}
#[derive(Debug, Default, Deserialize)]
struct PageProps {
company: Option<RawCompany>,
chart: Option<RawChart>,
}
#[derive(Debug, Deserialize)]
struct RawCompany {
price: Value,
#[serde(default)]
previous: Value,
#[serde(default)]
change: Value,
#[serde(default)]
percentage: Value,
#[serde(default)]
volume: Value,
date: Option<String>,
time: Option<String>,
status: Option<String>,
orderbook: Option<RawOrderbook>,
market_hour: Option<RawMarketHour>,
}
#[derive(Debug, Deserialize)]
struct RawOrderbook {
bid: Option<RawLevel>,
offer: Option<RawLevel>,
}
#[derive(Debug, Deserialize)]
struct RawLevel {
#[serde(default)]
price: Value,
}
#[derive(Debug, Deserialize)]
struct RawMarketHour {
status: Option<String>,
}
#[derive(Debug, Default, Deserialize)]
struct RawChart {
#[serde(default)]
prices: Vec<RawPoint>,
}
#[derive(Debug, Deserialize)]
struct RawPoint {
#[serde(default)]
formatted_date: String,
#[serde(default)]
value: Value,
}
/// Stockbit's code for a resolved symbol: `BBCA.JK` -> `BBCA`, and Yahoo's
/// index tickers to Stockbit's (`^JKSE` -> `IHSG`, `^JKLQ45` -> `LQ45`).
pub fn stockbit_code(resolved: &str) -> Result<String, IdxError> {
let unsupported = || {
IdxError::Unsupported(format!(
"{resolved}: Stockbit covers IDX (.JK) symbols only"
))
};
let code = resolved.strip_suffix(".JK").ok_or_else(unsupported)?;
let code = match code.strip_prefix('^') {
Some("JKSE") => "IHSG",
Some("JKLQ45") => "LQ45",
Some(_) => return Err(unsupported()),
None => code,
};
if code.is_empty() || !code.chars().all(|c| c.is_ascii_alphanumeric()) {
return Err(unsupported());
}
Ok(code.to_string())
}
/// The error for a non-success HTTP status from stockbit.com. A 403 is
/// CloudFront's static block page (it refuses some user agents outright),
/// not a challenge, so nothing is retried.
pub fn status_error(status: u16, resolved: &str) -> IdxError {
match status {
403 => IdxError::UpstreamBlocked(
"stockbit.com refused this client (HTTP 403 from its CDN); idx does not retry a \
block. Try again later or from another network; `--provider msn` serves quotes \
without Stockbit"
.to_string(),
),
404 => IdxError::SymbolNotFound(resolved.to_string()),
429 => IdxError::RateLimited,
_ => IdxError::Http(format!("stockbit: HTTP {status}")),
}
}
/// The quote and today's per-minute prices from a symbol page, stamped with
/// `resolved` (e.g. `BBCA.JK`).
pub fn parse_symbol_page(resolved: &str, html: &str) -> Result<Intraday, IdxError> {
let data: NextData = serde_json::from_str(next_data(html)?)
.map_err(|e| layout_error(format!("__NEXT_DATA__ is not the expected JSON: {e}")))?;
if data.page == "/404" {
return Err(IdxError::SymbolNotFound(resolved.to_string()));
}
let company = data
.props
.page_props
.company
.ok_or_else(|| layout_error("no company in the page".to_string()))?;
let points = parse_points(&data.props.page_props.chart.unwrap_or_default())?;
let quote = parse_quote(resolved, &company)?;
Ok(Intraday { quote, points })
}
fn layout_error(detail: String) -> IdxError {
IdxError::ParseError(format!(
"stockbit symbol page: {detail} (the page layout changed?)"
))
}
/// The JSON inside `<script id="__NEXT_DATA__" ...>...</script>`.
fn next_data(html: &str) -> Result<&str, IdxError> {
let missing = || layout_error("no __NEXT_DATA__ script".to_string());
let tag = html.find("id=\"__NEXT_DATA__\"").ok_or_else(missing)?;
let start = tag + html[tag..].find('>').ok_or_else(missing)? + 1;
let len = html[start..].find("</script>").ok_or_else(missing)?;
Ok(&html[start..start + len])
}
fn parse_quote(resolved: &str, company: &RawCompany) -> Result<Quote, IdxError> {
let price = number(&company.price)
.ok_or_else(|| layout_error(format!("price {} is not a number", company.price)))?;
let price = price.round() as i64;
let prev_close = number(&company.previous).map(|p| p.round() as i64);
let change = number(&company.change)
.map(|c| c.round() as i64)
.or_else(|| prev_close.map(|prev| price - prev));
let volume = count(&company.volume);
let listing = company.status.as_deref().unwrap_or_default();
let market_status = company
.market_hour
.as_ref()
.and_then(|m| m.status.clone())
.filter(|s| !s.is_empty());
let open = market_status.as_deref() == Some(MARKET_OPEN);
let level = |side: Option<&RawLevel>| {
side.and_then(|l| number(&l.price))
.filter(|p| *p > 0.0)
.map(|p| p.round() as i64)
};
let book = company.orderbook.as_ref();
let (bid, offer) = if open {
(
level(book.and_then(|b| b.bid.as_ref())),
level(book.and_then(|b| b.offer.as_ref())),
)
} else {
(None, None)
};
let traded = volume.is_some_and(|v| v > 0);
let as_of = if listing == "STATUS_ACTIVE" && traded {
last_trade(company.date.as_deref(), company.time.as_deref())
} else {
None
};
// Facts about this symbol only; the market's state is `market_status`.
// An untraded stock can still have a flat chart at the previous close.
let session_note = match listing {
"STATUS_SUSPENDED" => Some("Stockbit lists the stock as suspended".to_string()),
"STATUS_DELISTED" => Some("Stockbit lists the stock as delisted".to_string()),
_ if volume == Some(0) => Some("no trades this session".to_string()),
_ => None,
};
Ok(Quote {
symbol: resolved.to_string(),
price,
change,
change_pct: number(&company.percentage),
volume,
market_cap: None,
week52_high: None,
week52_low: None,
week52_position: None,
range_signal: None,
prev_close,
avg_volume: None,
as_of,
behind_session: false,
bid,
offer,
market_status,
session_note,
})
}
/// Today's prices, oldest first, one per minute. The anchor row (no
/// timestamp) is dropped; any other row that does not parse means the
/// layout changed.
fn parse_points(chart: &RawChart) -> Result<Vec<IntradayPoint>, IdxError> {
let mut points = Vec::with_capacity(chart.prices.len());
for row in &chart.prices {
if row.formatted_date.is_empty() {
continue;
}
let bad = || layout_error(format!("chart row {:?} {}", row.formatted_date, row.value));
let local = NaiveDateTime::parse_from_str(&row.formatted_date, "%Y-%m-%d %H:%M:%S")
.map_err(|_| bad())?;
let time = wib(local).ok_or_else(bad)?;
let price = number(&row.value).ok_or_else(bad)?;
points.push(IntradayPoint { time, price });
}
points.sort_by_key(|p| p.time);
// A repeated minute keeps its last row.
points.reverse();
points.dedup_by_key(|p| p.time);
points.reverse();
Ok(points)
}
/// `"22 Oct 2026"` and `"Thu 16:14"` as a WIB time.
fn last_trade(date: Option<&str>, time: Option<&str>) -> Option<DateTime<FixedOffset>> {
let date = NaiveDate::parse_from_str(date?.trim(), "%d %b %Y").ok()?;
let clock = time?.trim().rsplit(' ').next()?;
let clock = NaiveTime::parse_from_str(clock, "%H:%M").ok()?;
wib(date.and_time(clock))
}
fn wib(local: NaiveDateTime) -> Option<DateTime<FixedOffset>> {
idx_offset().from_local_datetime(&local).single()
}
/// A JSON number or numeric string (`"+59.54"`, `"5175.000000"`); `"N/A"`,
/// `"NA"` and empty strings are absent.
fn number(value: &Value) -> Option<f64> {
let n = match value {
Value::Number(n) => n.as_f64()?,
Value::String(s) => s.trim().parse::<f64>().ok()?,
_ => return None,
};
n.is_finite().then_some(n)
}
fn count(value: &Value) -> Option<u64> {
match value {
Value::Number(n) => n.as_u64(),
Value::String(s) => s.trim().parse::<u64>().ok(),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fixture(name: &str) -> String {
std::fs::read_to_string(format!(
"{}/tests/fixtures/{name}",
env!("CARGO_MANIFEST_DIR")
))
.unwrap()
}
/// The recorded BBCA page with Stockbit's market status set to `status`.
fn bbca_with_status(status: &str) -> String {
let page = fixture("stockbit_symbol_bbca.html");
assert!(page.contains(r#""status":"close""#));
page.replace(r#""status":"close""#, &format!(r#""status":"{status}""#))
}
#[test]
fn closed_session_keeps_the_series_and_hides_the_closing_book() {
let day = parse_symbol_page("BBCA.JK", &fixture("stockbit_symbol_bbca.html")).unwrap();
let q = &day.quote;
assert_eq!(
(q.price, q.prev_close, q.change),
(5175, Some(5275), Some(-100))
);
assert_eq!(q.change_pct, Some(-1.9));
assert_eq!(q.volume, Some(286_928_500));
assert_eq!(q.market_status.as_deref(), Some("close"));
assert_eq!((q.bid, q.offer), (None, None));
assert_eq!(q.session_note, None);
assert_eq!(q.as_of.unwrap().to_rfc3339(), "2026-10-22T16:14:00+07:00");
// 336 rows less the anchor at the previous close.
assert_eq!(day.points.len(), 335);
let first = &day.points[0];
assert_eq!(first.time.to_rfc3339(), "2026-10-22T08:58:00+07:00");
assert_eq!(first.price, 5125.0);
let last = day.points.last().unwrap();
assert_eq!(last.time.to_rfc3339(), "2026-10-22T16:14:00+07:00");
assert_eq!(last.price, 5175.0);
assert!(day.points.windows(2).all(|w| w[0].time < w[1].time));
}
#[test]
fn open_market_reports_best_bid_and_offer() {
let day = parse_symbol_page("BBCA.JK", &bbca_with_status(MARKET_OPEN)).unwrap();
assert_eq!((day.quote.bid, day.quote.offer), (Some(5175), Some(5200)));
assert_eq!(day.quote.market_status.as_deref(), Some(MARKET_OPEN));
}
#[test]
fn any_other_market_status_hides_bid_and_offer() {
for status in ["pre_open", "break", "OPEN", ""] {
let day = parse_symbol_page("BBCA.JK", &bbca_with_status(status)).unwrap();
assert_eq!((day.quote.bid, day.quote.offer), (None, None), "{status:?}");
}
}
#[test]
fn index_keeps_decimals_in_the_series_and_rounds_the_quote() {
let day = parse_symbol_page("^JKSE.JK", &fixture("stockbit_symbol_ihsg.html")).unwrap();
let q = &day.quote;
assert_eq!(
(q.price, q.prev_close, q.change),
(5388, Some(5328), Some(60))
);
// An index has no book even while open.
assert_eq!((q.bid, q.offer), (None, None));
assert_eq!(day.points.last().unwrap().price, 5387.90);
}
#[test]
fn suspended_and_delisted_stocks_are_empty_sessions_not_errors() {
for (file, note) in [
(
"stockbit_symbol_maba.html",
"Stockbit lists the stock as suspended",
),
(
"stockbit_symbol_myrx.html",
"Stockbit lists the stock as delisted",
),
] {
let day = parse_symbol_page("X.JK", &fixture(file)).unwrap();
assert!(day.points.is_empty(), "{file}");
assert_eq!(day.quote.session_note.as_deref(), Some(note), "{file}");
// The page's date/time is not a trade time when nothing traded.
assert_eq!(day.quote.as_of, None, "{file}");
}
}
/// HOME and BIMA on 2026-10-22: active, a flat chart at the previous
/// close, volume 0. Without a note they read as a traded flat session.
#[test]
fn an_untraded_active_stock_says_so_even_with_a_flat_chart() {
let page = fixture("stockbit_symbol_bbca.html");
assert!(page.contains(r#""volume":"286928500""#));
let page = page.replace(r#""volume":"286928500""#, r#""volume":"0""#);
let day = parse_symbol_page("BBCA.JK", &page).unwrap();
assert!(!day.points.is_empty());
assert_eq!(
day.quote.session_note.as_deref(),
Some("no trades this session")
);
assert_eq!(day.quote.as_of, None);
}
#[test]
fn unknown_code_is_symbol_not_found() {
let err = parse_symbol_page("ZZZZ.JK", &fixture("stockbit_symbol_zzzz_404.html"));
assert!(matches!(err, Err(IdxError::SymbolNotFound(s)) if s == "ZZZZ.JK"));
}
#[test]
fn a_page_without_next_data_is_a_layout_change() {
let err = parse_symbol_page("BBCA.JK", &fixture("stockbit_blocked_403.html"));
assert!(matches!(err, Err(IdxError::ParseError(_))), "{err:?}");
}
#[test]
fn http_403_is_upstream_blocked_and_404_is_not_found() {
assert!(matches!(
status_error(403, "BBCA.JK"),
IdxError::UpstreamBlocked(_)
));
assert!(
matches!(status_error(404, "BBCA.JK"), IdxError::SymbolNotFound(s) if s == "BBCA.JK")
);
assert!(matches!(
status_error(429, "BBCA.JK"),
IdxError::RateLimited
));
assert!(matches!(status_error(502, "BBCA.JK"), IdxError::Http(_)));
}
#[test]
fn codes_map_idx_symbols_and_index_tickers() {
assert_eq!(stockbit_code("BBCA.JK").unwrap(), "BBCA");
assert_eq!(stockbit_code("^JKSE.JK").unwrap(), "IHSG");
assert_eq!(stockbit_code("^JKLQ45.JK").unwrap(), "LQ45");
for other in ["AAPL.US", "^GSPC.JK", "BRK-B.JK", ".JK"] {
assert!(
matches!(stockbit_code(other), Err(IdxError::Unsupported(_))),
"{other}"
);
}
}
}

View file

@ -57,6 +57,41 @@ pub struct Quote {
/// quote is returned, not when it is cached. /// quote is returned, not when it is cached.
#[serde(default)] #[serde(default)]
pub behind_session: bool, pub behind_session: bool,
/// Best bid in IDR (whole Rupiah). Stockbit only, and only while its
/// market status is `open`: outside trading it is the closing book, not
/// a live one. Absent from JSON when unknown.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bid: Option<i64>,
/// Best offer in IDR (whole Rupiah), gated like `bid`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offer: Option<i64>,
/// The provider's own session state, verbatim (Stockbit
/// `market_hour.status`, e.g. `close`). Absent for providers without one.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub market_status: Option<String>,
/// What the provider says about this symbol's session: suspended,
/// delisted, or no trades. Never inferred.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_note: Option<String>,
}
/// One price of today's session, minute by minute (Stockbit's chart).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct IntradayPoint {
/// The minute, in WIB.
pub time: DateTime<FixedOffset>,
/// Last price at that minute in IDR; indices carry two decimals.
pub price: f64,
}
/// The latest session of one IDX symbol: its quote and the price per minute.
/// Stockbit's public page carries no per-minute OHLC or volume.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Intraday {
pub quote: Quote,
/// Oldest first; empty when the symbol did not trade (suspended,
/// delisted, or no session yet), never an error.
pub points: Vec<IntradayPoint>,
} }
/// OHLC candle data normalized from Yahoo Finance chart indicators. /// OHLC candle data normalized from Yahoo Finance chart indicators.

View file

@ -65,6 +65,10 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0)) .and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
.map(|t| t.with_timezone(&crate::api::idx_offset())), .map(|t| t.with_timezone(&crate::api::idx_offset())),
behind_session: false, behind_session: false,
bid: None,
offer: None,
market_status: None,
session_note: None,
}) })
} }

View file

@ -41,7 +41,7 @@ impl CachePolicy {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct Fetched<T> { pub struct Fetched<T> {
pub data: T, pub data: T,
/// Provider that produced the data (`msn` or `yahoo`). /// Provider that produced the data (e.g. `msn`, `yahoo`, `ipot`, `stockbit`).
pub source: &'static str, pub source: &'static str,
pub fetched_at: DateTime<Utc>, pub fetched_at: DateTime<Utc>,
/// True when an expired cache entry was served (offline, or after a /// True when an expired cache entry was served (offline, or after a

View file

@ -13,10 +13,14 @@ use crate::error::IdxError;
pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"]; pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"];
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[cfg_attr(feature = "clap", value(rename_all = "lower"))]
pub enum ProviderKind { pub enum ProviderKind {
Yahoo, Yahoo,
Msn, Msn,
// Opt-in: quotes from Stockbit's public page (delayed, no login).
Stockbit,
} }
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
@ -34,6 +38,7 @@ impl ProviderKind {
match self { match self {
Self::Yahoo => "yahoo", Self::Yahoo => "yahoo",
Self::Msn => "msn", Self::Msn => "msn",
Self::Stockbit => "stockbit",
} }
} }
@ -42,9 +47,11 @@ impl ProviderKind {
Ok(Self::Yahoo) Ok(Self::Yahoo)
} else if value.eq_ignore_ascii_case("msn") { } else if value.eq_ignore_ascii_case("msn") {
Ok(Self::Msn) Ok(Self::Msn)
} else if value.eq_ignore_ascii_case("stockbit") {
Ok(Self::Stockbit)
} else { } else {
Err(IdxError::ConfigError(format!( Err(IdxError::ConfigError(format!(
"invalid provider '{value}' (expected yahoo or msn)" "invalid provider '{value}' (expected yahoo, msn, or stockbit)"
))) )))
} }
} }

View file

@ -34,6 +34,10 @@ pub enum IdxError {
InvalidInput(String), InvalidInput(String),
#[error("no market data: {0}")] #[error("no market data: {0}")]
NoMarketData(String), NoMarketData(String),
/// The upstream refused this client outright (e.g. a CDN's HTTP 403
/// block page). Retrying does not help; the message says what might.
#[error("upstream blocked: {0}")]
UpstreamBlocked(String),
} }
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
@ -53,6 +57,7 @@ pub enum ErrorCode {
PdfParseError, PdfParseError,
InvalidInput, InvalidInput,
NoMarketData, NoMarketData,
UpstreamBlocked,
} }
/// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`. /// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`.
@ -80,6 +85,7 @@ impl IdxError {
Self::PdfParseError(_) => ErrorCode::PdfParseError, Self::PdfParseError(_) => ErrorCode::PdfParseError,
Self::InvalidInput(_) => ErrorCode::InvalidInput, Self::InvalidInput(_) => ErrorCode::InvalidInput,
Self::NoMarketData(_) => ErrorCode::NoMarketData, Self::NoMarketData(_) => ErrorCode::NoMarketData,
Self::UpstreamBlocked(_) => ErrorCode::UpstreamBlocked,
} }
} }
@ -93,7 +99,8 @@ impl IdxError {
| ErrorCode::RateLimited | ErrorCode::RateLimited
| ErrorCode::ProviderUnavailable | ErrorCode::ProviderUnavailable
| ErrorCode::Offline | ErrorCode::Offline
| ErrorCode::AuthError => 4, | ErrorCode::AuthError
| ErrorCode::UpstreamBlocked => 4,
ErrorCode::Unsupported => 5, ErrorCode::Unsupported => 5,
ErrorCode::Io ErrorCode::Io
| ErrorCode::ParseError | ErrorCode::ParseError
@ -119,4 +126,11 @@ mod tests {
let unsupported = IdxError::Unsupported("history unavailable".to_string()); let unsupported = IdxError::Unsupported("history unavailable".to_string());
assert_eq!(unsupported.code(), ErrorCode::Unsupported); assert_eq!(unsupported.code(), ErrorCode::Unsupported);
} }
#[test]
fn upstream_blocked_has_its_own_wire_code_and_network_exit() {
let blocked = IdxError::UpstreamBlocked("stockbit.com: HTTP 403".to_string());
assert_eq!(blocked.code().to_string(), "UPSTREAMBLOCKED");
assert_eq!(blocked.exit_code(), 4);
}
} }

View file

@ -6,8 +6,8 @@ use std::str::FromStr;
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday}; use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday};
use crate::api::types::{Interval, Ohlc, Period, Quote}; use crate::api::types::{Interval, Intraday, Ohlc, Period, Quote};
use crate::api::{history_provider, history_source, idx_offset}; use crate::api::{history_provider, history_source, idx_offset, intraday_provider, stockbit};
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch}; use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
use crate::config::{HistoryProviderKind, ProviderKind}; use crate::config::{HistoryProviderKind, ProviderKind};
use crate::error::IdxError; use crate::error::IdxError;
@ -34,6 +34,10 @@ impl Market<'_> {
) -> Result<Vec<Fetched<Quote>>, IdxError> { ) -> Result<Vec<Fetched<Quote>>, IdxError> {
let idx = self.idx; let idx = self.idx;
let bucket = idx.bucket("quote"); let bucket = idx.bucket("quote");
let ttl_secs = match idx.provider.kind() {
ProviderKind::Stockbit => idx.config.quote_ttl.min(stockbit::CACHE_TTL_SECS),
_ => idx.config.quote_ttl,
};
let session = latest_session(Utc::now()); let session = latest_session(Utc::now());
let mut quotes = Vec::with_capacity(symbols.len()); let mut quotes = Vec::with_capacity(symbols.len());
for symbol in symbols { for symbol in symbols {
@ -43,7 +47,7 @@ impl Market<'_> {
CacheSpec { CacheSpec {
bucket: &bucket, bucket: &bucket,
key: &resolved, key: &resolved,
ttl_secs: idx.config.quote_ttl, ttl_secs,
subject: &resolved, subject: &resolved,
source: idx.provider.kind().as_str(), source: idx.provider.kind().as_str(),
}, },
@ -60,6 +64,32 @@ impl Market<'_> {
Ok(quotes) Ok(quotes)
} }
/// Today's session of one IDX symbol from Stockbit's public page,
/// whichever market provider is selected: the quote (bid/offer only while
/// the market is open) and the price per minute.
pub fn intraday(
&self,
symbol: &str,
policy: CachePolicy,
) -> Result<Fetched<Intraday>, IdxError> {
let idx = self.idx;
let resolved = idx.resolve(symbol)?;
let provider = intraday_provider();
cached_fetch(
&idx.cache,
CacheSpec {
bucket: "stockbit-intraday",
key: &resolved,
ttl_secs: idx.config.quote_ttl.min(stockbit::CACHE_TTL_SECS),
subject: &resolved,
source: stockbit::SOURCE,
},
policy,
|| provider.intraday(&resolved),
Ok,
)
}
/// Resolves the history source for an optional `--history-provider` /// Resolves the history source for an optional `--history-provider`
/// override, without fetching. /// override, without fetching.
pub fn history_source(&self, mode: Option<HistoryProviderKind>) -> HistorySource { pub fn history_source(&self, mode: Option<HistoryProviderKind>) -> HistorySource {
@ -380,6 +410,10 @@ mod tests {
avg_volume: None, avg_volume: None,
as_of: None, as_of: None,
behind_session: false, behind_session: false,
bid: None,
offer: None,
market_status: None,
session_note: None,
} }
} }

View file

@ -0,0 +1,20 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<BR clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: VajZoZu7lzhasdjnxU37dHaztROLXHQ6DMpJzTPZoDhkd4I2dVc2mw==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Stockbit</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"symbol":"MABA","company":{"symbol":"MABA","name":"Marga Abhinaya Abadi Tbk","price":"50","previous":"50","change":"0.00","percentage":0,"volume":"0","date":"22 Oct 2026","time":"Thu 04:07","updated":"2022-10-17T01:22:50+07:00","status":"STATUS_SUSPENDED","tradeable":0,"type_company":"Saham","exchange":"IDX","orderbook":{"bid":{"price":"N/A","volume":"N/A"},"offer":{"price":"N/A","volume":"N/A"}},"market_hour":{"status":"close","time_left":52792,"formatted_time_left":"14 jam 39 menit 52 detik","suspend_info":""}},"chart":{"cagr":"","change":0,"drawdown":"","markingpoint":"","percentage":"","prices":[],"timeframe":"today","xaxisopt":"","previous":50,"line_weight":0,"previous_timeframe_price":null,"chart_type":"PRICE_CHART_TYPE_UNSPECIFIED","interval_in_minutes":0,"allowed_chart_type":[],"max_candles":0}},"__N_SSP":true},"page":"/symbol/[symbol]","query":{"symbol":"MABA"},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>

View file

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Stockbit</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"symbol":"MYRX","company":{"symbol":"MYRX","name":"Hanson International Tbk.","price":"50","previous":"50","change":"0.00","percentage":0,"volume":"0","date":"21 Jul 2025","time":"Mon 07:56","updated":"2025-07-21T08:53:24+07:00","status":"STATUS_DELISTED","tradeable":0,"type_company":"Saham","exchange":"IDX","orderbook":{"bid":{"price":"N/A","volume":"N/A"},"offer":{"price":"N/A","volume":"N/A"}},"market_hour":{"status":"close","time_left":52561,"formatted_time_left":"14 jam 36 menit 1 detik","suspend_info":""}},"chart":{"cagr":"","change":0,"drawdown":"","markingpoint":"","percentage":"","prices":[],"timeframe":"today","xaxisopt":"","previous":50,"line_weight":0,"previous_timeframe_price":null,"chart_type":"PRICE_CHART_TYPE_UNSPECIFIED","interval_in_minutes":0,"allowed_chart_type":[],"max_candles":0}},"__N_SSP":true},"page":"/symbol/[symbol]","query":{"symbol":"MYRX"},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>

View file

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Error | Stockbit - Investasi Saham Bersama Komunitas Saham Terbesar di Indonesia</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{},"__N_SSP":true},"page":"/404","query":{},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>

View file

@ -29,6 +29,15 @@ pub struct HistoryParams {
pub interval: Option<String>, pub interval: Option<String>,
} }
#[derive(Debug, Deserialize, JsonSchema)]
pub struct IntradayParams {
/// IDX ticker, e.g. "BBCA"; indices as "^JKSE" or "IHSG".
pub symbol: String,
/// Only the latest N minutes (default: the whole session, at most 400).
#[serde(default)]
pub tail: Option<usize>,
}
#[derive(Debug, Deserialize, JsonSchema)] #[derive(Debug, Deserialize, JsonSchema)]
pub struct ScreenParams { pub struct ScreenParams {
/// Screen to run (default "top-performers"). /// Screen to run (default "top-performers").

View file

@ -26,8 +26,8 @@ use tokio::sync::Semaphore;
use crate::params::{ use crate::params::{
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams, AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
CompanySection, EntityParams, FlowParams, HistoryParams, ScreenParams, SearchParams, CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, ScreenParams,
ShareholdersParams, SymbolsParams, SyncParams, SearchParams, ShareholdersParams, SymbolsParams, SyncParams,
}; };
const SYNC_TOOL: &str = "ownership_sync"; const SYNC_TOOL: &str = "ownership_sync";
@ -43,6 +43,8 @@ const MAX_SEARCH_LIMIT: usize = 100;
// Pages above a few hundred rows (~40 KB) overflow some agents' tool output. // Pages above a few hundred rows (~40 KB) overflow some agents' tool output.
const MAX_CHANGES_LIMIT: usize = 300; const MAX_CHANGES_LIMIT: usize = 300;
const CHANGES_TOP_TICKERS: usize = 10; const CHANGES_TOP_TICKERS: usize = 10;
// A session has about 340 minutes, so this is the whole day.
const MAX_INTRADAY_POINTS: usize = 400;
/// Tool calls running at once; the rest wait their turn. /// Tool calls running at once; the rest wait their turn.
static CALL_SLOTS: Semaphore = Semaphore::const_new(4); static CALL_SLOTS: Semaphore = Semaphore::const_new(4);
@ -117,7 +119,11 @@ impl IdxServer {
description = "Latest quotes (price, change, volume, 52-week range, market cap) \ description = "Latest quotes (price, change, volume, 52-week range, market cap) \
for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true \ for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true \
when that is before the latest IDX session, so the price is not that session's close: the \ when that is before the latest IDX session, so the price is not that session's close: the \
provider lags, the stock did not trade, or the day was an exchange holiday." provider lags, the stock did not trade, or the day was an exchange holiday. When the server \
runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. \
\"open\", \"close\"), `bid`/`offer` (best price, only while the market is open) and \
`session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 \
minutes."
)] )]
async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult { async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult {
let symbols = match checked_symbols(params.0.symbols) { let symbols = match checked_symbols(params.0.symbols) {
@ -131,6 +137,27 @@ provider lags, the stock did not trade, or the day was an exchange holiday."
.await .await
} }
#[tool(
description = "Today's session of one IDX stock or index from Stockbit's public page \
(no login), whichever provider the server uses: `quote` (price, change, volume, \
`market_status`, `bid`/`offer` only while the market is open, `session_note`) and \
`points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no \
per-minute volume. Empty `points` means the stock did not trade this session (see \
`session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine \
(HTTP 403): do not retry at once."
)]
async fn get_intraday(&self, params: Parameters<IntradayParams>) -> ToolResult {
let IntradayParams { symbol, tail } = params.0;
self.live(move |idx| {
let tail = bounded_limit("tail", tail, MAX_INTRADAY_POINTS, MAX_INTRADAY_POINTS)?;
let mut day = idx.market().intraday(&symbol, CachePolicy::Default)?;
let points = &mut day.data.points;
points.drain(..points.len().saturating_sub(tail));
Ok(day)
})
.await
}
#[tool(description = "OHLCV price history for one IDX ticker.")] #[tool(description = "OHLCV price history for one IDX ticker.")]
async fn get_price_history(&self, params: Parameters<HistoryParams>) -> ToolResult { async fn get_price_history(&self, params: Parameters<HistoryParams>) -> ToolResult {
let HistoryParams { let HistoryParams {

View file

@ -287,6 +287,33 @@
}, },
"name": "get_foreign_flow" "name": "get_foreign_flow"
}, },
{
"description": "Today's session of one IDX stock or index from Stockbit's public page (no login), whichever provider the server uses: `quote` (price, change, volume, `market_status`, `bid`/`offer` only while the market is open, `session_note`) and `points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no per-minute volume. Empty `points` means the stock did not trade this session (see `session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"symbol": {
"description": "IDX ticker, e.g. \"BBCA\"; indices as \"^JKSE\" or \"IHSG\".",
"type": "string"
},
"tail": {
"default": null,
"description": "Only the latest N minutes (default: the whole session, at most 400).",
"format": "uint",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"symbol"
],
"type": "object"
},
"name": "get_intraday"
},
{ {
"description": "Every IDX holding (above 1%) of one investor, matched by name.", "description": "Every IDX holding (above 1%) of one investor, matched by name.",
"inputSchema": { "inputSchema": {
@ -397,7 +424,7 @@
"name": "get_price_history" "name": "get_price_history"
}, },
{ {
"description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday.", "description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. When the server runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. \"open\", \"close\"), `bid`/`offer` (best price, only while the market is open) and `session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 minutes.",
"inputSchema": { "inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": { "properties": {

View file

@ -9,13 +9,14 @@ use idx_core::config::CoreConfig;
use idx_core::investor::{ImportSource, Investor}; use idx_core::investor::{ImportSource, Investor};
use serde_json::{Value, json}; use serde_json::{Value, json};
const TOOLS: [&str; 14] = [ const TOOLS: [&str; 15] = [
"analyze_bandar", "analyze_bandar",
"analyze_stock", "analyze_stock",
"compare_stocks", "compare_stocks",
"get_broker_summary", "get_broker_summary",
"get_company", "get_company",
"get_foreign_flow", "get_foreign_flow",
"get_intraday",
"get_investor_holdings", "get_investor_holdings",
"get_ownership_changes", "get_ownership_changes",
"get_price_history", "get_price_history",
@ -36,6 +37,10 @@ struct Client {
impl Client { impl Client {
fn start(args: &[&str], seed: bool) -> Self { fn start(args: &[&str], seed: bool) -> Self {
Self::start_with_env(args, seed, &[])
}
fn start_with_env(args: &[&str], seed: bool, env: &[(&str, &str)]) -> Self {
let home = tempdir::Dir::new(); let home = tempdir::Dir::new();
if seed { if seed {
seed_ownership_db(&home.path().join("data/idx/ownership.db")); seed_ownership_db(&home.path().join("data/idx/ownership.db"));
@ -50,6 +55,7 @@ impl Client {
.env("XDG_CACHE_HOME", home.path().join("cache")) .env("XDG_CACHE_HOME", home.path().join("cache"))
.env("XDG_DATA_HOME", home.path().join("data")) .env("XDG_DATA_HOME", home.path().join("data"))
.env("IDX_USE_MOCK_PROVIDER", "1") .env("IDX_USE_MOCK_PROVIDER", "1")
.envs(env.iter().copied())
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::null()) .stderr(Stdio::null())
@ -299,6 +305,63 @@ fn live_tools_wrap_data_with_freshness() {
assert_eq!(compared["failures"], json!([])); assert_eq!(compared["failures"], json!([]));
} }
#[test]
fn intraday_is_stockbit_whichever_provider_and_empty_sessions_are_data() {
let mut client = Client::start(&[], false);
let day = client.ok("get_intraday", json!({"symbol": "BBCA"}));
assert_fetched(&day);
assert_eq!(day["source"], "stockbit");
let quote = &day["data"]["quote"];
assert_eq!(quote["symbol"], "BBCA.JK");
assert_eq!(quote["market_status"], "close");
// The closing book is not reported as a live one.
assert!(
quote.get("bid").is_none() && quote.get("offer").is_none(),
"{quote}"
);
assert_eq!(day["data"]["points"].as_array().unwrap().len(), 335);
let tail = client.ok("get_intraday", json!({"symbol": "BBCA", "tail": 2}));
let points = tail["data"]["points"].as_array().unwrap();
assert_eq!(points.len(), 2);
assert_eq!(points[1]["time"], "2026-10-22T16:14:00+07:00");
let suspended = client.ok("get_intraday", json!({"symbol": "MABA"}));
assert_eq!(suspended["data"]["points"], json!([]));
assert_eq!(
suspended["data"]["quote"]["session_note"],
"Stockbit lists the stock as suspended"
);
for (arguments, code) in [
(json!({"symbol": "ZZZZ"}), "SYMBOLNOTFOUND"),
(json!({"symbol": "BBCA", "tail": 0}), "INVALIDINPUT"),
(json!({"symbol": "BBCA", "tail": 401}), "INVALIDINPUT"),
] {
let (is_error, body) = client.call("get_intraday", arguments.clone());
assert!(is_error, "{arguments}: {body}");
assert_eq!(body["code"], code, "{arguments}: {body}");
}
}
#[test]
fn stockbit_provider_quotes_carry_market_status_and_blocks_surface_once() {
let mut client = Client::start_with_env(&[], false, &[("IDX_PROVIDER", "stockbit")]);
let quotes = client.ok("get_quotes", json!({"symbols": ["BBCA"]}));
assert_eq!(quotes["quotes"][0]["source"], "stockbit");
assert_eq!(quotes["quotes"][0]["data"]["market_status"], "close");
let mut blocked = Client::start_with_env(&[], false, &[("IDX_MOCK_STOCKBIT_STATUS", "403")]);
let (is_error, body) = blocked.call("get_intraday", json!({"symbol": "BBCA"}));
assert!(is_error, "{body}");
assert_eq!(body["code"], "UPSTREAMBLOCKED");
assert!(
body["message"].as_str().unwrap().contains("does not retry"),
"{body}"
);
}
#[test] #[test]
fn errors_carry_cli_codes() { fn errors_carry_cli_codes() {
let mut client = Client::start(&[], false); let mut client = Client::start(&[], false);

View file

@ -55,6 +55,9 @@ command parses arguments, makes one idx-core call, and renders the result.
(`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries). (`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries).
- **IDX (idx.co.id)** — the daily trading summary behind `stocks flow - **IDX (idx.co.id)** — the daily trading summary behind `stocks flow
--source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow). --source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow).
- **Stockbit (public page)** — `stocks intraday` whichever provider is
selected (`IntradayProvider`), and quotes under the opt-in
`--provider stockbit`. See [Stockbit](#stockbit).
### MSN Endpoints ### MSN Endpoints
- `https://assets.msn.com/service/`: quotes, charts, equities, earnings, - `https://assets.msn.com/service/`: quotes, charts, equities, earnings,
@ -151,6 +154,41 @@ WIB as the fallback.
range fails on one. `foreign_net_value` is net shares at the session's range fails on one. `foreign_net_value` is net shares at the session's
average price (value over volume), an estimate. average price (value over volume), an estimate.
### Stockbit
- `GET https://stockbit.com/symbol/{CODE}` needs no login. Next.js
server-renders the quote and today's chart into the page's `__NEXT_DATA__`
JSON; `api/stockbit/parse.rs` reads it purely. `props.pageProps.company`
is the quote (numbers mostly as strings, an index price as a float string),
`orderbook.{bid,offer}.price` the best bid and offer, `market_hour.status`
the market's state, and `status` `STATUS_ACTIVE|SUSPENDED|DELISTED`.
`chart.prices[]` holds one price per minute in WIB with no offset, after a
synthetic first row at the previous close; per-minute OHLC and volume are
empty. An unknown code is HTTP 404 with the 404 page.
- Stockbit's JSON API (`exodus.stockbit.com`: chart, order book) answers 401
without a user's bearer token; it is not used.
- Bid and offer are kept only while `market_hour.status` is `open`, so a
closing book never reads as a live one. Observed 2026-10-23: `open` during
trading (bid/offer set), `break` from 11:30 on a Friday, `close` after
hours; any other value fails closed. In session the chart gained a point
every minute and the last trade was within a minute of the clock, although
Stockbit's terms call its data delayed at least 10 minutes. Order-book
sizes are dropped: the page does not say whether they are lots or shares.
`as_of` comes from `date`/`time`
only when the stock traded (a suspended stock carries today's date).
`session_note` repeats what the page says (suspended, delisted) or "no
trades this session"; it is never inferred.
- CloudFront in front of the page answers a static 403 to some user agents
(`curl/`, `axios/`, `Go-http-client/`, empty) and does not fingerprint TLS,
so a plain ureq request with `idx-cli/<version> (+repo)` passes. A 403 is
`UPSTREAMBLOCKED` and is not retried. Requests are at least 1 s apart
across the user's idx processes (the last request's time sits in
`<cache>/stockbit.pace` under a file lock, so a shell loop of `idx` calls is
paced too), and snapshots are cached for 5 s (at most `quote_ttl`), which
coalesces an agent's quote-then-intraday burst across MCP calls.
- Stockbit's terms call its data delayed at least 10 minutes and for
personal use; frontends say so. Fixtures (`tests/fixtures/stockbit_*`) are
pages recorded 2026-10-22 after the close, cut to the fields read.
### Capability Gating ### Capability Gating
``` ```
MarketDataProvider = QuoteProvider + FundamentalsProvider MarketDataProvider = QuoteProvider + FundamentalsProvider
@ -161,8 +199,13 @@ Factory functions:
history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)> history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)>
broker_provider() → Box<dyn BrokerSummaryProvider> broker_provider() → Box<dyn BrokerSummaryProvider>
trading_summary_provider() → Box<dyn TradingSummaryProvider> trading_summary_provider() → Box<dyn TradingSummaryProvider>
intraday_provider() → Box<dyn IntradayProvider>
``` ```
`ProviderKind::Stockbit` implements quotes only: fundamentals are
`UNSUPPORTED`, the MSN-only capabilities say "requires --provider msn", and
`auto` history falls back to Yahoo.
## Ownership Module (SQLite-backed) ## Ownership Module (SQLite-backed)
Unlike the `stocks` module (live-fetch), ownership is **import-then-query**: Unlike the `stocks` module (live-fetch), ownership is **import-then-query**:
@ -239,7 +282,9 @@ CLI flags > environment variables > config file > defaults
bidirectional overrides replaced before it reaches a terminal table. bidirectional overrides replaced before it reaches a terminal table.
- Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage - Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage
errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/ errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/
`PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`, `5` `UNSUPPORTED`, `1` everything else `PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`/`UPSTREAMBLOCKED`, `5` `UNSUPPORTED`,
`1` everything else. `UPSTREAMBLOCKED` is an upstream refusing this client
outright (a CDN 403 page); retrying at once does not help.
- 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. - 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 ## CLI Output and Offline Cache

View file

@ -184,7 +184,7 @@ is another 20-30% smaller than pretty JSON.
| 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... | | 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... |
| 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` | | 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` |
| 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` | | 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` |
| 4 | upstream unavailable, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR` | | 4 | upstream unavailable or blocked, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR`, `UPSTREAMBLOCKED` |
| 5 | unsupported | `UNSUPPORTED` | | 5 | unsupported | `UNSUPPORTED` |
- JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths - JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths

View file

@ -42,12 +42,12 @@ contents. The native macOS installer job runs separately to exercise BSD tools.
## Live drift check ## Live drift check
Fixtures cannot tell when MSN, Yahoo, or IDX change their responses. Fixtures cannot tell when MSN, Yahoo, IPOT, IDX, or Stockbit change their responses.
`scripts/live-smoke.sh` runs shipped commands against the real upstreams and `scripts/live-smoke.sh` runs shipped commands against the real upstreams and
validates each JSON output shape. It is opt-in and never part of the gate. validates each JSON output shape. It is opt-in and never part of the gate.
```bash ```bash
nix develop --command scripts/live-smoke.sh # stocks + ownership nix develop --command scripts/live-smoke.sh # stocks + stockbit + ownership
nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI
nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx
nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B against an older build nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B against an older build
@ -58,10 +58,16 @@ scripts/live-smoke.sh --dry-run
`flow` and `flow --source idx` on the new binary only, so an older `--compare` build that predates `flow` and `flow --source idx` on the new binary only, so an older `--compare` build that predates
them is fine), plus `compare BUMI,ADRO,AIMS`, them is fine), plus `compare BUMI,ADRO,AIMS`,
whose MSN key ratios contain non-finite values. whose MSN key ratios contain non-finite values.
- `stockbit`: `intraday` for the ticker and `^JKSE`, and
`--provider stockbit stocks quote`, on the new binary only. A run outside the
session sees `market_status: close` and no bid/offer.
- `ownership`: `discover`, then imports the newest supported report into a temp - `ownership`: `discover`, then imports the newest supported report into a temp
DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many
datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts. datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts.
A case whose upstream refuses us (`UPSTREAMBLOCKED`, an HTTP 403) prints
`BLOCKED (HTTP 403)` and counts as a failure, never as a pass.
`--compare <old-idx>` runs every case with both binaries back to back, each `--compare <old-idx>` runs every case with both binaries back to back, each
with its own config and cache, and fails on a different exit code, any added, with its own config and cache, and fails on a different exit code, any added,
missing, or retyped JSON key, or changed values for `profile`, `financials`, missing, or retyped JSON key, or changed values for `profile`, `financials`,

View file

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

View file

@ -1,8 +1,10 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Live provider drift check: runs shipped commands against real MSN, Yahoo, # Live provider drift check: runs shipped commands against real MSN, Yahoo,
# and IDX endpoints and validates the JSON output shape. Deterministic CLI # IPOT, IDX, and Stockbit endpoints and validates the JSON output shape.
# behavior is covered by tests/cli.rs and tests/golden.rs; this answers "do the # Deterministic CLI behavior is covered by tests/cli.rs and tests/golden.rs;
# upstreams still look the way our parsers expect?" # this answers "do the upstreams still look the way our parsers expect?"
# An upstream that refuses us (UPSTREAMBLOCKED, HTTP 403) is reported as
# BLOCKED, never as a pass.
# #
# With --compare <old-idx>, every case also runs the old binary back to back # With --compare <old-idx>, every case also runs the old binary back to back
# and fails on any difference in exit code or JSON structure, and on any value # and fails on any difference in exit code or JSON structure, and on any value
@ -26,7 +28,8 @@ usage() {
Usage: scripts/live-smoke.sh [options] Usage: scripts/live-smoke.sh [options]
Options: Options:
--group <stocks|ownership> Restrict to a group. Repeatable. Default: both --group <name> stocks, stockbit, or ownership. Repeatable.
Default: all three
--symbol <ticker> Primary ticker. Default: BBCA --symbol <ticker> Primary ticker. Default: BBCA
--bin <path> Use an existing idx binary (skips cargo build) --bin <path> Use an existing idx binary (skips cargo build)
--compare <old-idx> Also run an older idx binary and diff the results --compare <old-idx> Also run an older idx binary and diff the results
@ -46,12 +49,12 @@ while [[ $# -gt 0 ]]; do
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;; *) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac esac
done done
[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks ownership) [[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks stockbit ownership)
for group in "${GROUPS_SELECTED[@]}"; do for group in "${GROUPS_SELECTED[@]}"; do
case "$group" in case "$group" in
stocks|ownership) ;; stocks|stockbit|ownership) ;;
*) echo "invalid --group '$group' (expected stocks or ownership)" >&2; exit 2 ;; *) echo "invalid --group '$group' (expected stocks, stockbit, or ownership)" >&2; exit 2 ;;
esac esac
done done
@ -99,8 +102,10 @@ def types: [paths(scalars) as $p | select(getpath($p) != null)
# jq filter describing the JSON shape each command must keep. # jq filter describing the JSON shape each command must keep.
contract() { contract() {
case "$1" in case "$1" in
"stocks quote "*|"stocks screen"*) "stocks quote "*|"stocks screen"*|"--provider stockbit stocks quote "*)
echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.price | type == "number"))' ;; echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.price | type == "number"))' ;;
"stocks intraday "*)
echo '(.quote.symbol | type == "string") and (.quote.price | type == "number") and (.quote.market_status | type == "string") and (.points | type == "array") and all(.points[]; (.time | type == "string") and (.price | type == "number"))' ;;
"stocks history "*) "stocks history "*)
echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;; echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;;
"stocks technical "*) "stocks technical "*)
@ -172,6 +177,11 @@ run_case() {
fi fi
fi fi
if (( status != 0 )); then if (( status != 0 )); then
if grep -q 'UPSTREAMBLOCKED' "$log.stderr"; then
FAILURES+=("$name: BLOCKED, upstream refused us with HTTP 403 (see $log.stderr)")
echo " BLOCKED (HTTP 403)"
return
fi
FAILURES+=("$name: exit $status (see $log.stderr)") FAILURES+=("$name: exit $status (see $log.stderr)")
echo " FAIL exit" echo " FAIL exit"
return return
@ -238,6 +248,13 @@ if selected stocks; then
run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx" run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx"
fi fi
if selected stockbit; then
# Stockbit's public page (no login): one request per case, 1 s apart.
run_new_case intraday "stocks intraday $SYMBOL"
run_new_case intraday-index "stocks intraday ^JKSE"
run_new_case quote-stockbit "--provider stockbit stocks quote $SYMBOL"
fi
if selected ownership; then if selected ownership; then
run_case discover "ownership discover --limit 1" run_case discover "ownership discover --limit 1"
if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then

View file

@ -31,7 +31,7 @@ export default defineConfig({
}, },
{ {
label: 'CLI', label: 'CLI',
items: ['cli/stocks', 'cli/ownership', 'cli/output', 'cli/reference'], items: ['cli/stocks', 'cli/stockbit', 'cli/ownership', 'cli/output', 'cli/reference'],
}, },
{ {
label: 'MCP server', label: 'MCP server',

View file

@ -32,7 +32,7 @@ stdout is not a terminal, and with `--no-color`.
| `0` | success | | | `0` | success | |
| `2` | invalid input or config | fix the command | | `2` | invalid input or config | fix the command |
| `3` | not found, no data, or no cached data offline | check the ticker or name; go online | | `3` | not found, no data, or no cached data offline | check the ticker or name; go online |
| `4` | upstream unavailable | retry later | | `4` | upstream unavailable or blocked | retry later; for `UPSTREAMBLOCKED`, wait or change network |
| `5` | unsupported | use another command or provider | | `5` | unsupported | use another command or provider |
| `1` | anything else | read the message | | `1` | anything else | read the message |

View file

@ -41,6 +41,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -60,6 +65,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
Commands: Commands:
quote Get real-time stock quotes quote Get real-time stock quotes
intraday Today's session price by the minute (Stockbit)
history Get historical OHLC data history Get historical OHLC data
technical Run technical analysis on a stock technical Run technical analysis on a stock
growth Run growth analysis on a stock growth Run growth analysis on a stock
@ -80,13 +86,14 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks quote` ### `idx stocks quote`
@ -100,19 +107,67 @@ Arguments:
<SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin <SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks quote BBCA idx stocks quote BBCA
idx stocks quote BBCA,BBRI,BMRI idx stocks quote BBCA,BBRI,BMRI
idx -o json stocks quote BBCA idx -o json stocks quote BBCA
echo BBCA BBRI | idx stocks quote - echo BBCA BBRI | idx stocks quote -
idx --provider stockbit stocks quote BBCA # with bid/offer while the market is open
```
### `idx stocks intraday`
```text
Today's session of one IDX stock or index, by the minute, from Stockbit's public page (no login): the quote, the best bid/offer while the market is open, a sparkline, and the last minutes. Delayed at least 10 minutes per Stockbit's terms; prices only, no per-minute volume. A stock that did not trade has no minutes, which is not an error. Indices use Yahoo's tickers (^JKSE, ^JKLQ45) or Stockbit's (IHSG, LQ45).
Usage: idx stocks intraday [OPTIONS] <SYMBOL>
Arguments:
<SYMBOL>
Single ticker symbol (e.g. BBCA)
Options:
-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)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline
Run without network requests
--no-cache
Bypass local cache
-h, --help
Print help (see a summary with '-h')
Examples:
idx stocks intraday BBCA
idx stocks intraday ^JKSE
idx -o json stocks intraday BBCA | jq '.points[-1]'
``` ```
### `idx stocks history` ### `idx stocks history`
@ -140,6 +195,8 @@ Options:
Suppress non-essential output Suppress non-essential output
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
--no-cache --no-cache
@ -173,6 +230,8 @@ Options:
Suppress non-essential output Suppress non-essential output
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
--no-cache --no-cache
@ -196,13 +255,14 @@ Arguments:
<SYMBOL> Single ticker symbol (e.g. BBCA) <SYMBOL> Single ticker symbol (e.g. BBCA)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks growth BBCA idx stocks growth BBCA
@ -220,13 +280,14 @@ Arguments:
<SYMBOL> Single ticker symbol (e.g. BBCA) <SYMBOL> Single ticker symbol (e.g. BBCA)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks valuation BBCA idx stocks valuation BBCA
@ -244,13 +305,14 @@ Arguments:
<SYMBOL> Single ticker symbol (e.g. BBCA) <SYMBOL> Single ticker symbol (e.g. BBCA)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks risk BBCA idx stocks risk BBCA
@ -268,13 +330,14 @@ Arguments:
<SYMBOL> Single ticker symbol (e.g. BBCA) <SYMBOL> Single ticker symbol (e.g. BBCA)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks fundamental BBCA idx stocks fundamental BBCA
@ -337,6 +400,11 @@ Options:
[default: all] [default: all]
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -409,17 +477,22 @@ Options:
[default: all] [default: all]
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
--no-cache
Bypass local cache
--top <TOP> --top <TOP>
Tier behind the headline signal: 1, 3, 5 or 10 brokers per side Tier behind the headline signal: 1, 3, 5 or 10 brokers per side
[default: 5] [default: 5]
--no-cache
Bypass local cache
-h, --help -h, --help
Print help (see a summary with '-h') Print help (see a summary with '-h')
@ -474,6 +547,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -502,13 +580,14 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks financials` ### `idx stocks financials`
@ -527,6 +606,7 @@ Options:
--no-color Disable colored output --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--offline Run without network requests --offline Run without network requests
--no-cache Bypass local cache --no-cache Bypass local cache
-h, --help Print help -h, --help Print help
@ -548,17 +628,18 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
--forecast Only include forward earnings rows --forecast Only include forward earnings rows
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
--history Only include historical earnings rows --history Only include historical earnings rows
--no-color Disable colored output --no-color Disable colored output
--annual Only include annual periods --annual Only include annual periods
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
--quarterly Only include quarterly periods --quarterly Only include quarterly periods
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks earnings BBCA idx stocks earnings BBCA
@ -577,13 +658,14 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks insights` ### `idx stocks insights`
@ -597,13 +679,14 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks news` ### `idx stocks news`
@ -617,14 +700,15 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
--limit <LIMIT> [default: 10] --limit <LIMIT> [default: 10]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks screen` ### `idx stocks screen`
@ -635,16 +719,17 @@ MSN screener
Usage: idx stocks screen [OPTIONS] Usage: idx stocks screen [OPTIONS]
Options: Options:
--filter <FILTER> [default: top-performers] --filter <FILTER> [default: top-performers]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
--region <REGION> [default: id] --region <REGION> [default: id]
--limit <LIMIT> [default: 50] --limit <LIMIT> [default: 50]
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx stocks compare` ### `idx stocks compare`
@ -658,13 +743,14 @@ Arguments:
<SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin <SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx stocks compare BBCA BBRI BMRI idx stocks compare BBCA BBRI BMRI
@ -704,6 +790,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -733,13 +824,14 @@ Create default configuration file
Usage: idx config init [OPTIONS] Usage: idx config init [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx config get` ### `idx config get`
@ -753,13 +845,14 @@ Arguments:
<KEY> Key such as general.provider (see `idx config --help`) <KEY> Key such as general.provider (see `idx config --help`)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx config set` ### `idx config set`
@ -774,13 +867,14 @@ Arguments:
<VALUE> <VALUE>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx config path` ### `idx config path`
@ -791,13 +885,14 @@ Print configuration file path
Usage: idx config path [OPTIONS] Usage: idx config path [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
## `idx cache` ## `idx cache`
@ -813,13 +908,14 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx cache info` ### `idx cache info`
@ -830,13 +926,14 @@ Show cache usage information
Usage: idx cache info [OPTIONS] Usage: idx cache info [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx cache clear` ### `idx cache clear`
@ -847,13 +944,14 @@ Clear all cached files
Usage: idx cache clear [OPTIONS] Usage: idx cache clear [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
## `idx ownership` ## `idx ownership`
@ -899,6 +997,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -924,15 +1027,16 @@ Discover the latest IDX-hosted ownership report URLs
Usage: idx ownership discover [OPTIONS] Usage: idx ownership discover [OPTIONS]
Options: Options:
--family <FAMILY> Report family to discover: above1 (default), all, above5, or investor-type [default: above1] --family <FAMILY> Report family to discover: above1 (default), all, above5, or investor-type [default: above1]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
--limit <LIMIT> Maximum number of discovered report URLs to print [default: 6] --limit <LIMIT> Maximum number of discovered report URLs to print [default: 6]
--no-color Disable colored output --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx ownership discover --limit 1 idx ownership discover --limit 1
@ -977,6 +1081,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -1028,6 +1137,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -1056,14 +1170,15 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
--source <SOURCE> [default: all] --source <SOURCE> [default: all]
--no-color Disable colored output --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx ownership entity` ### `idx ownership entity`
@ -1077,13 +1192,14 @@ Arguments:
<NAME> Entity name or part of one, case-insensitive; the shortest match is shown <NAME> Entity name or part of one, case-insensitive; the shortest match is shown
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
Examples: Examples:
idx ownership entity "ASTRA INTERNATIONAL" idx ownership entity "ASTRA INTERNATIONAL"
@ -1121,6 +1237,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -1149,6 +1270,7 @@ Options:
--no-color Disable colored output --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--offline Run without network requests --offline Run without network requests
--no-cache Bypass local cache --no-cache Bypass local cache
-h, --help Print help -h, --help Print help
@ -1162,15 +1284,16 @@ Rank tickers by ownership concentration
Usage: idx ownership concentration [OPTIONS] Usage: idx ownership concentration [OPTIONS]
Options: Options:
--by <BY> [default: hhi] --by <BY> [default: hhi]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
--top <TOP> [default: 20] --top <TOP> [default: 20]
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx ownership flow` ### `idx ownership flow`
@ -1184,13 +1307,14 @@ Arguments:
<SYMBOL> <SYMBOL>
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx ownership changes` ### `idx ownership changes`
@ -1223,6 +1347,11 @@ Options:
-v, --verbose... -v, --verbose...
Increase verbosity (-v, -vv) Increase verbosity (-v, -vv)
--provider <PROVIDER>
Market data provider [default: general.provider, else msn]
[possible values: yahoo, msn, stockbit]
--offline --offline
Run without network requests Run without network requests
@ -1248,15 +1377,16 @@ Arguments:
<ROOT> Ticker code or entity name to start from <ROOT> Ticker code or entity name to start from
Options: Options:
--depth <DEPTH> Traversal depth (default 2) [default: 2] --depth <DEPTH> Traversal depth (default 2) [default: 2]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
--format <FORMAT> Output format: table or dot (Graphviz) [default: table] --format <FORMAT> Output format: table or dot (Graphviz) [default: table]
--no-color Disable colored output --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx ownership resolve` ### `idx ownership resolve`
@ -1273,13 +1403,14 @@ Commands:
help Print this message or the help of the given subcommand(s) help Print this message or the help of the given subcommand(s)
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
#### `idx ownership resolve unresolved` #### `idx ownership resolve unresolved`
@ -1290,14 +1421,15 @@ List unresolved or low-confidence entity aliases
Usage: idx ownership resolve unresolved [OPTIONS] Usage: idx ownership resolve unresolved [OPTIONS]
Options: Options:
--limit <LIMIT> [default: 50] --limit <LIMIT> [default: 50]
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
#### `idx ownership resolve map` #### `idx ownership resolve map`
@ -1312,13 +1444,14 @@ Arguments:
<ENTITY> The canonical entity name to map to <ENTITY> The canonical entity name to map to
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
#### `idx ownership resolve merge` #### `idx ownership resolve merge`
@ -1333,13 +1466,14 @@ Arguments:
<MERGE> Entity ID to merge and delete <MERGE> Entity ID to merge and delete
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
### `idx ownership releases` ### `idx ownership releases`
@ -1350,13 +1484,14 @@ List imported KSEI releases
Usage: idx ownership releases [OPTIONS] Usage: idx ownership releases [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
## `idx completions` ## `idx completions`
@ -1370,13 +1505,14 @@ Arguments:
<SHELL> [possible values: bash, zsh, fish] <SHELL> [possible values: bash, zsh, fish]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```
## `idx version` ## `idx version`
@ -1387,11 +1523,12 @@ Show idx-cli version
Usage: idx version [OPTIONS] Usage: idx version [OPTIONS]
Options: Options:
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] -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 --no-color Disable colored output
-q, --quiet Suppress non-essential output -q, --quiet Suppress non-essential output
-v, --verbose... Increase verbosity (-v, -vv) -v, --verbose... Increase verbosity (-v, -vv)
--offline Run without network requests --provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
--no-cache Bypass local cache --offline Run without network requests
-h, --help Print help --no-cache Bypass local cache
-h, --help Print help
``` ```

View file

@ -0,0 +1,75 @@
---
title: Stockbit
description: Today's session by the minute and quotes with best bid/offer, from Stockbit's public page. Opt-in, no account.
---
[Stockbit](https://stockbit.com)'s public symbol page carries a quote, the
best bid and offer, and today's price for every minute of the session. idx
reads that page directly. It needs no Stockbit account, no login and no
helper tools.
```bash
idx stocks intraday BBCA # today's session, by the minute
idx stocks intraday ^JKSE # an index (or IHSG, LQ45)
idx --provider stockbit stocks quote BBCA # quote with bid/offer
idx -o json stocks intraday BBCA | jq '.points | length'
```
## What you get
| | `stocks intraday` | `--provider stockbit stocks quote` |
| --- | --- | --- |
| Price, change, previous close, volume | yes | yes |
| Best bid and offer | while the market is open | while the market is open |
| Market status (`market_status`) | yes | yes |
| Price per minute (`points`) | yes | no |
| Market cap, 52-week range | no | no |
- **Delayed.** Stockbit's terms describe its market data as "a real-time
snapshot, delayed at least 10 minutes". It is not a live feed.
- **Bid and offer only while trading.** Outside the session, Stockbit still
shows the book as it closed. idx leaves `bid` and `offer` out then, so the
closing book never passes for a live one.
- **Top of the book only.** One bid and one offer price, no depth. The full
order book needs a Stockbit login, which idx does not do.
- **Prices only.** The page has no per-minute volume or OHLC.
- **Empty sessions are data.** A suspended or delisted stock answers with
`points: []` and a `session_note` saying so (`Stockbit lists the stock as
suspended`). An active stock with no volume says `no trades this session`,
even when its chart is a flat line at the previous close. Neither is an
error.
`--provider stockbit` (or `IDX_PROVIDER=stockbit`, or
`idx config set general.provider stockbit`) changes only where quotes come
from. History still comes from Yahoo. Company data, fundamentals and
screening answer `UNSUPPORTED` under it, so use MSN for those.
## How idx talks to Stockbit
- One request per symbol to `https://stockbit.com/symbol/<CODE>`, at least one
second apart across all your idx processes (a shell loop of `idx` calls
included), cached for 5 seconds.
- An honest user agent, `idx-cli/<version> (+https://github.com/0xrsydn/idx-cli)`,
not a browser disguise. Stockbit's CDN refuses some generic agents
(`curl/…`) outright but lets this one through. No curl-impersonate
needed.
- No retries. If Stockbit's CDN refuses your machine, the command fails at
once with `UPSTREAMBLOCKED` (exit `4`):
```json
{"error":true,"code":"UPSTREAMBLOCKED","message":"upstream blocked: stockbit.com refused this client (HTTP 403 from its CDN); ..."}
```
Wait, or try from another network. `--provider msn` serves quotes without
Stockbit.
## Terms
Stockbit's [terms](https://stockbit.com/terms) allow its content for personal,
non-commercial use. They prohibit data mining, robots and redistribution
without Stockbit's written consent. idx is not affiliated with or endorsed by
Stockbit. Use this for looking at a stock now and then, not for crawling the
market or republishing prices.
Over MCP, the same data comes from the `get_intraday` tool, and from
`get_quotes` when the server runs with `IDX_PROVIDER=stockbit`.

View file

@ -19,6 +19,20 @@ session, the table says so under it, and JSON sets `behind_session`: the
provider may still show the previous close. `history` takes `--period` (`1d` provider may still show the previous close. `history` takes `--period` (`1d`
to `5y`) and `--interval` (`1d`, `1wk`, `1mo`). to `5y`) and `--interval` (`1d`, `1wk`, `1mo`).
## Today by the minute
```bash
idx stocks intraday BBCA
idx stocks intraday ^JKSE
idx -o json stocks intraday BBCA | jq '.points[-1]'
```
`intraday` shows today's session from Stockbit's public page, without a
login: the quote, the best bid and offer while the market is open, a
sparkline and the last five minutes. JSON gives every minute in `points`. It
is delayed at least 10 minutes. See [Stockbit](/cli/stockbit/) for what it
covers and the terms it comes with.
## Company data ## Company data
| Command | What it shows | | Command | What it shows |
@ -142,6 +156,8 @@ idx stocks screen --filter high-dividend | jq -r '.[].symbol' | idx stocks compa
MSN is the default for quotes, company data and screening; Yahoo serves MSN is the default for quotes, company data and screening; Yahoo serves
price history. Indo Premier (IPOT) serves broker summaries and foreign flow price history. Indo Premier (IPOT) serves broker summaries and foreign flow
(IDX's own site with `flow --source idx`), whichever provider is selected. Switch with `IDX_PROVIDER=msn|yahoo` and (IDX's own site with `flow --source idx`), and Stockbit serves `intraday`,
`IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the matching whichever provider is selected. Switch with `--provider msn|yahoo|stockbit`
[config keys](/start/config/). (or `IDX_PROVIDER`) and `IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the
matching [config keys](/start/config/). `--provider stockbit` serves quotes
only, with the best bid/offer; see [Stockbit](/cli/stockbit/).

View file

@ -5,8 +5,9 @@ description: idx-mcp serves idx's stock and ownership data to AI agents over MCP
`idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) `idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
server over stdio. It gives an agent the same data as the `idx` CLI, as server over stdio. It gives an agent the same data as the `idx` CLI, as
[14 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, price history, screening, company data, [15 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's
analysis, comparison, broker summaries, bandarmology, foreign flow, and the KSEI session by the minute, price history, screening, company data, analysis,
comparison, broker summaries, bandarmology, foreign flow, and the KSEI
ownership queries. ownership queries.
It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps), It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps),
@ -27,7 +28,7 @@ Tools that fetch live data return:
} }
``` ```
`source` is the provider (`msn` or `yahoo`). Results are cached on disk; `source` is the provider (`msn`, `yahoo`, `ipot`, `idx` or `stockbit`). Results are cached on disk;
`stale` is true when an expired cache entry was served because a fresh fetch `stale` is true when an expired cache entry was served because a fresh fetch
failed. Ownership tools return their data directly, from the local failed. Ownership tools return their data directly, from the local
database. database.
@ -44,6 +45,7 @@ body, so the agent can decide whether to retry:
| Input | Limit | | Input | Limit |
| --- | --- | | --- | --- |
| tickers per call (`get_quotes`, `compare_stocks`) | 20, duplicates removed | | tickers per call (`get_quotes`, `compare_stocks`) | 20, duplicates removed |
| `get_intraday` `tail` | 400 minutes (default: the whole session) |
| `screen_stocks` `limit` | 200 | | `screen_stocks` `limit` | 200 |
| `search_investors` `limit` | 100 | | `search_investors` `limit` | 100 |
| `get_ownership_changes` `limit` | 300 per page (default 200); page with `offset` | | `get_ownership_changes` `limit` | 300 per page (default 200); page with `offset` |

View file

@ -75,6 +75,15 @@ Foreign investors' buying and selling of one IDX ticker, session by session (old
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". | | `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). | | `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
## `get_intraday`
Today's session of one IDX stock or index from Stockbit's public page (no login), whichever provider the server uses: `quote` (price, change, volume, `market_status`, `bid`/`offer` only while the market is open, `session_note`) and `points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no per-minute volume. Empty `points` means the stock did not trade this session (see `session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once.
| Input | Type | Required | Description |
| --- | --- | --- | --- |
| `symbol` | string | yes | IDX ticker, e.g. "BBCA"; indices as "^JKSE" or "IHSG". |
| `tail` | integer | no | Only the latest N minutes (default: the whole session, at most 400). |
## `get_investor_holdings` ## `get_investor_holdings`
Every IDX holding (above 1%) of one investor, matched by name. Every IDX holding (above 1%) of one investor, matched by name.
@ -107,7 +116,7 @@ OHLCV price history for one IDX ticker.
## `get_quotes` ## `get_quotes`
Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. When the server runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. "open", "close"), `bid`/`offer` (best price, only while the market is open) and `session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 minutes.
| Input | Type | Required | Description | | Input | Type | Required | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |

View file

@ -28,7 +28,7 @@ in scripts. Ask for JSON with `-o json`, `IDX_OUTPUT=json`, or
```toml ```toml
[general] [general]
provider = "msn" # msn | yahoo provider = "msn" # msn | yahoo | stockbit
history_provider = "auto" # auto | yahoo | msn history_provider = "auto" # auto | yahoo | msn
exchange = "JK" exchange = "JK"
output = "auto" # auto | table | json | jsonl output = "auto" # auto | table | json | jsonl

View file

@ -139,7 +139,7 @@ TLKM.JK 2,250 <span class="up">+0.45%</span> 87,159,900
<div> <div>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="4" y="7" width="16" height="12" rx="3"></rect><path d="M12 3v4M9 13h.01M15 13h.01"></path></svg> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="4" y="7" width="16" height="12" rx="3"></rect><path d="M12 3v4M9 13h.01M15 13h.01"></path></svg>
<h2>Agents</h2> <h2>Agents</h2>
<p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 11 tools for any MCP client.</p> <p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 15 tools for any MCP client.</p>
<code>npx -y idx-mcp</code> <code>npx -y idx-mcp</code>
</div> </div>
</div> </div>