diff --git a/README.md b/README.md index 4e3b22a..bd6b5b4 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ idx stocks history BBCA --period 3mo idx stocks technical BBCA idx stocks fundamental BBCA idx stocks compare BBCA BBRI BMRI +idx stocks intraday BBCA # today by the minute (Stockbit, delayed) idx -o json stocks quote BBCA # 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 `fundamental` report; multi-symbol `compare`; MSN stock `screen` - 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` +- 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 ### Ownership (KSEI shareholders above 1%) @@ -158,8 +163,8 @@ change it to `auto` with `idx config set general.output auto`. ## MCP server -`idx-mcp` serves the same data to MCP clients over stdio, as 14 tools: -`get_quotes`, `get_price_history`, `screen_stocks`, `get_company`, +`idx-mcp` serves the same data to MCP clients over stdio, as 15 tools: +`get_quotes`, `get_intraday`, `get_price_history`, `screen_stocks`, `get_company`, `analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`, `get_foreign_flow`, `get_shareholders`, `get_investor_holdings`, `search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as diff --git a/ROADMAP.md b/ROADMAP.md index 56cf036..d7c453c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -4,16 +4,23 @@ Open work only. Shipped behavior is described in `README.md` and `docs/`; history lives in git. ## 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 sectors`: sector performance overview - `screen query ""`: filter stocks by expression - `screen presets` / `screen run `: built-in screener presets (graduate `screen` out of `stocks` only if these grow into a richer workflow) - Multi-period financial statements (today `financials` is single-period) -- Order book (bid/offer depth): no free, login-free source exists. IDX sells - delayed depth to vendors and Stockbit's needs a user login. Revisit if a - permitted source appears; do not ship the closing best bid/offer as one. +- Order book depth: no free, login-free source exists (IDX sells delayed depth + to vendors). Planned as the Stockbit login follow-on: opt-in with the + 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 cumulative top-N net) and a price-move phase (quiet accumulation, markup), 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 - `alerts`: price alerts with a daemon - 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 - 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 curl-impersonate profile (only `curl_safari184` in October 2026) and on the 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 can still fail. - `stocks technical` reports the SMA200 trend as unavailable when fewer than 200 daily candles are returned. -- MSN's `low-pe` screen returned no priced stocks on 2026-10-14 (upstream; - other filters work). +- MSN's `low-pe` screen returns no priced stocks (`PARSEERROR`; still so on + 2026-10-23, upstream; other filters work). - MSN history for IDX is price-only (`1mo`, `3mo`, `1y`); Yahoo stays the default OHLCV source. diff --git a/crates/idx-cli/src/cli/mod.rs b/crates/idx-cli/src/cli/mod.rs index 1ced634..4b4ac70 100644 --- a/crates/idx-cli/src/cli/mod.rs +++ b/crates/idx-cli/src/cli/mod.rs @@ -5,6 +5,7 @@ pub mod ownership; pub mod stocks; use clap::{Parser, Subcommand, ValueEnum}; +use idx_core::config::ProviderKind; use crate::output::OutputSetting; @@ -39,6 +40,13 @@ pub struct Cli { pub quiet: bool, #[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")] pub verbose: u8, + #[arg( + long, + value_enum, + global = true, + help = "Market data provider [default: general.provider, else msn]" + )] + pub provider: Option, #[arg(long, global = true, help = "Run without network requests")] pub offline: bool, #[arg(long, global = true, help = "Bypass local cache")] diff --git a/crates/idx-cli/src/cli/stocks.rs b/crates/idx-cli/src/cli/stocks.rs index d27e576..e538a34 100644 --- a/crates/idx-cli/src/cli/stocks.rs +++ b/crates/idx-cli/src/cli/stocks.rs @@ -6,8 +6,8 @@ use crate::config::{HistoryProviderKind, IdxConfig}; use crate::output::{ render_bandar, render_broker_summary, render_compare, render_earnings, render_financials, render_foreign_flow, render_fundamental, render_growth, render_history, render_insights, - render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment, - render_technical, render_valuation, + render_intraday, render_news, render_profile, render_quotes, render_risk, render_screener, + render_sentiment, render_technical, render_valuation, }; use idx_core::analysis::BandarQuery; use idx_core::analysis::bandar::DEFAULT_TOP; @@ -126,13 +126,26 @@ pub struct StocksCmd { pub enum StocksSubcommand { #[command( about = "Get real-time stock quotes", - after_help = "Examples:\n idx stocks quote BBCA\n idx stocks quote BBCA,BBRI,BMRI\n idx -o json stocks quote BBCA\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 { /// One or more symbols, comma-separated or space-separated; `-` reads them from stdin. #[arg(required = true)] symbols: Vec, }, + #[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( about = "Get historical OHLC data", 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 } => { let quotes = idx.market().quotes(&expand_symbols(symbols)?, policy)?; let quotes: Vec<_> = quotes.into_iter().map(|q| q.data).collect(); - render_quotes("es, format, no_color) + render_quotes("es, 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 { symbol, diff --git a/crates/idx-cli/src/config.rs b/crates/idx-cli/src/config.rs index a0ad8f3..69eac1f 100644 --- a/crates/idx-cli/src/config.rs +++ b/crates/idx-cli/src/config.rs @@ -34,6 +34,9 @@ impl IdxConfig { pub fn load_with_cli(cli: &Cli) -> Result { let mut core = CoreConfig::load()?; core.verbose = cli.verbose > 0; + if let Some(provider) = cli.provider { + core.provider = provider; + } let mut setting = OutputSetting::Auto; let mut no_color = false; diff --git a/crates/idx-cli/src/output/mod.rs b/crates/idx-cli/src/output/mod.rs index 3429e04..62d35b2 100644 --- a/crates/idx-cli/src/output/mod.rs +++ b/crates/idx-cli/src/output/mod.rs @@ -14,10 +14,11 @@ use idx_core::analysis::fundamental::{ }; use idx_core::analysis::report::TechnicalReport; use idx_core::api::types::{ - CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote, - SentimentData, + CompanyProfile, EarningsReport, FinancialStatements, InsightData, Intraday, NewsItem, Ohlc, + Quote, SentimentData, }; use idx_core::broker::types::BrokerSummary; +use idx_core::config::ProviderKind; use idx_core::error::IdxError; use idx_core::flow::types::ForeignFlow; @@ -67,15 +68,30 @@ impl OutputFormat { pub fn render_quotes( quotes: &[Quote], + provider: ProviderKind, format: &OutputFormat, no_color: bool, ) -> Result<(), IdxError> { match format { + OutputFormat::Table if provider == ProviderKind::Stockbit => { + table::print_stockbit_quotes(quotes, no_color) + } OutputFormat::Table => table::print_quotes(quotes, no_color), 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( summary: &BrokerSummary, format: &OutputFormat, diff --git a/crates/idx-cli/src/output/table.rs b/crates/idx-cli/src/output/table.rs index 4481b62..6fc60bd 100644 --- a/crates/idx-cli/src/output/table.rs +++ b/crates/idx-cli/src/output/table.rs @@ -11,8 +11,8 @@ use idx_core::analysis::fundamental::{ use idx_core::analysis::report::TechnicalReport; use idx_core::analysis::signals::Signal; use idx_core::api::types::{ - CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, - Quote, SentimentData, + CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, Intraday, + NewsItem, Ohlc, Quote, SentimentData, }; use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary}; use idx_core::error::IdxError; @@ -61,12 +61,7 @@ pub fn print_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> { ]); for q in quotes { - let pct_cell = match q.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), - }; + let pct_cell = pct_cell(q.change_pct, no_color); table.add_row(vec![ cell(&q.symbol), cell(format_idr(q.price)), @@ -121,6 +116,182 @@ fn stale_quote_notes(quotes: &[Quote], now: chrono::DateTime) -> Ve .collect() } +fn pct_cell(change_pct: Option, 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) -> String { + value.map(format_idr).unwrap_or_else(|| "-".to_string()) +} + +fn wib_minute(time: chrono::DateTime) -> 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(¬e)); + } + 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 = 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> { println!( "{}", @@ -1410,6 +1581,10 @@ mod tests { avg_volume: None, as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()), behind_session: false, + bid: None, + offer: None, + market_status: None, + session_note: None, } } diff --git a/crates/idx-cli/tests/cli.rs b/crates/idx-cli/tests/cli.rs index abdade7..82bf3fc 100644 --- a/crates/idx-cli/tests/cli.rs +++ b/crates/idx-cli/tests/cli.rs @@ -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] fn invalid_quote_ttl_env_returns_non_zero() { test_bin("invalid-quote-ttl") diff --git a/crates/idx-cli/tests/golden/cases.toml b/crates/idx-cli/tests/golden/cases.toml index 2cb5311..00e1ff3 100644 --- a/crates/idx-cli/tests/golden/cases.toml +++ b/crates/idx-cli/tests/golden/cases.toml @@ -244,6 +244,25 @@ name = "yahoo-fundamental" args = ["stocks", "fundamental", "BBCA"] 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) -------------------- [[case]] @@ -334,6 +353,19 @@ env = { IDX_MOCK_ERROR = "1" } name = "error-fetch-bing-unsupported" 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 -------------------------------------------------------------------- [[case]] diff --git a/crates/idx-cli/tests/golden/completions-bash.golden b/crates/idx-cli/tests/golden/completions-bash.golden index aff5968..998698f 100644 --- a/crates/idx-cli/tests/golden/completions-bash.golden +++ b/crates/idx-cli/tests/golden/completions-bash.golden @@ -204,6 +204,9 @@ _idx() { idx__help__stocks,insights) cmd="idx__help__stocks__insights" ;; + idx__help__stocks,intraday) + cmd="idx__help__stocks__intraday" + ;; idx__help__stocks,news) cmd="idx__help__stocks__news" ;; @@ -378,6 +381,9 @@ _idx() { idx__stocks,insights) cmd="idx__stocks__insights" ;; + idx__stocks,intraday) + cmd="idx__stocks__intraday" + ;; idx__stocks,news) cmd="idx__stocks__news" ;; @@ -435,6 +441,9 @@ _idx() { idx__stocks__help,insights) cmd="idx__stocks__help__insights" ;; + idx__stocks__help,intraday) + cmd="idx__stocks__help__intraday" + ;; idx__stocks__help,news) cmd="idx__stocks__help__news" ;; @@ -466,7 +475,7 @@ _idx() { case "${cmd}" in 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -480,6 +489,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -488,7 +501,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -502,6 +515,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -510,7 +527,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -524,6 +541,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -588,7 +609,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -602,6 +623,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -610,7 +635,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -624,6 +649,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -632,7 +661,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -646,6 +675,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -654,7 +687,7 @@ _idx() { return 0 ;; idx__config__get) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -668,6 +701,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -760,7 +797,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -774,6 +811,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -782,7 +823,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -796,6 +837,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -804,7 +849,7 @@ _idx() { return 0 ;; idx__config__set) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -818,6 +863,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1218,7 +1267,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1371,6 +1420,20 @@ _idx() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 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) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then @@ -1498,7 +1561,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1512,6 +1575,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1520,7 +1587,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1542,6 +1609,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1550,7 +1621,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1572,6 +1643,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1580,7 +1655,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1602,6 +1677,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1610,7 +1689,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1632,6 +1711,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1640,7 +1723,7 @@ _idx() { return 0 ;; idx__ownership__entity) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1654,6 +1737,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1662,7 +1749,7 @@ _idx() { return 0 ;; idx__ownership__flow) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1676,6 +1763,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1684,7 +1775,7 @@ _idx() { return 0 ;; idx__ownership__graph) - opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1706,6 +1797,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -1966,7 +2061,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -1992,6 +2087,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2000,7 +2099,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2014,6 +2113,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2022,7 +2125,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2036,6 +2139,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2114,7 +2221,7 @@ _idx() { return 0 ;; idx__ownership__resolve__map) - 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 4 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2128,6 +2235,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2136,7 +2247,7 @@ _idx() { return 0 ;; idx__ownership__resolve__merge) - 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 4 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2150,6 +2261,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2158,7 +2273,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2176,6 +2291,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2184,7 +2303,7 @@ _idx() { return 0 ;; idx__ownership__search) - 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 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2202,6 +2321,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2210,7 +2333,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2228,6 +2351,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2236,7 +2363,7 @@ _idx() { return 0 ;; idx__ownership__ticker) - opts="-o -q -v -h --source --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --source --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2254,6 +2381,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2262,7 +2393,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2276,6 +2407,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2284,7 +2419,7 @@ _idx() { return 0 ;; idx__stocks__bandar) - opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --offline --no-cache --help ..." + opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help ..." if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2322,6 +2457,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2330,7 +2469,7 @@ _idx() { return 0 ;; idx__stocks__broker) - opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2364,6 +2503,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2372,7 +2515,7 @@ _idx() { return 0 ;; idx__stocks__compare) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2386,6 +2529,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2394,7 +2541,7 @@ _idx() { return 0 ;; idx__stocks__earnings) - opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2408,6 +2555,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2416,7 +2567,7 @@ _idx() { return 0 ;; idx__stocks__financials) - opts="-o -q -v -h --statement --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --statement --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2434,6 +2585,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2442,7 +2597,7 @@ _idx() { return 0 ;; idx__stocks__flow) - opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2472,6 +2627,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2480,7 +2639,7 @@ _idx() { return 0 ;; idx__stocks__fundamental) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2494,6 +2653,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2502,7 +2665,7 @@ _idx() { return 0 ;; idx__stocks__growth) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2516,6 +2679,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2524,7 +2691,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2691,6 +2858,20 @@ _idx() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) 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) opts="" if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then @@ -2804,7 +2985,7 @@ _idx() { return 0 ;; idx__stocks__history) - opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2830,6 +3011,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2838,7 +3023,7 @@ _idx() { return 0 ;; idx__stocks__insights) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2852,6 +3037,36 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) 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 " + 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=() ;; @@ -2860,7 +3075,7 @@ _idx() { return 0 ;; idx__stocks__news) - 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 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2878,6 +3093,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2886,7 +3105,7 @@ _idx() { return 0 ;; idx__stocks__profile) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2900,6 +3119,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2908,7 +3131,7 @@ _idx() { return 0 ;; idx__stocks__quote) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2922,6 +3145,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2930,7 +3157,7 @@ _idx() { return 0 ;; idx__stocks__risk) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2944,6 +3171,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2952,7 +3183,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -2978,6 +3209,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -2986,7 +3221,7 @@ _idx() { return 0 ;; idx__stocks__sentiment) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -3000,6 +3235,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -3008,7 +3247,7 @@ _idx() { return 0 ;; idx__stocks__technical) - opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --offline --no-cache --help " + opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help " if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -3026,6 +3265,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -3034,7 +3277,7 @@ _idx() { return 0 ;; idx__stocks__valuation) - 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -3048,6 +3291,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; @@ -3056,7 +3303,7 @@ _idx() { return 0 ;; 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 COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -3070,6 +3317,10 @@ _idx() { COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}")) return 0 ;; + --provider) + COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}")) + return 0 + ;; *) COMPREPLY=() ;; diff --git a/crates/idx-cli/tests/golden/error-intraday-blocked.json.golden b/crates/idx-cli/tests/golden/error-intraday-blocked.json.golden new file mode 100644 index 0000000..402cc2b --- /dev/null +++ b/crates/idx-cli/tests/golden/error-intraday-blocked.json.golden @@ -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"} diff --git a/crates/idx-cli/tests/golden/error-intraday-blocked.table.golden b/crates/idx-cli/tests/golden/error-intraday-blocked.table.golden new file mode 100644 index 0000000..fd793c7 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-intraday-blocked.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.json.golden b/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.json.golden new file mode 100644 index 0000000..33601b3 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.json.golden @@ -0,0 +1,4 @@ +exit: 3 +--- stdout +--- stderr +{"code":"SYMBOLNOTFOUND","error":true,"message":"symbol not found: ZZZZ.JK"} diff --git a/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.table.golden b/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.table.golden new file mode 100644 index 0000000..0673de7 --- /dev/null +++ b/crates/idx-cli/tests/golden/error-intraday-unknown-symbol.table.golden @@ -0,0 +1,4 @@ +exit: 3 +--- stdout +--- stderr +Error: symbol not found: ZZZZ.JK diff --git a/crates/idx-cli/tests/golden/error-invalid-provider.json.golden b/crates/idx-cli/tests/golden/error-invalid-provider.json.golden index 7e9cef0..02963d8 100644 --- a/crates/idx-cli/tests/golden/error-invalid-provider.json.golden +++ b/crates/idx-cli/tests/golden/error-invalid-provider.json.golden @@ -1,4 +1,4 @@ exit: 2 --- stdout --- stderr -{"code":"CONFIGERROR","error":true,"message":"config error: invalid provider 'bogus' (expected yahoo or msn)"} +{"code":"CONFIGERROR","error":true,"message":"config error: invalid provider 'bogus' (expected yahoo, msn, or stockbit)"} diff --git a/crates/idx-cli/tests/golden/error-invalid-provider.table.golden b/crates/idx-cli/tests/golden/error-invalid-provider.table.golden index 37464ed..3be5d0a 100644 --- a/crates/idx-cli/tests/golden/error-invalid-provider.table.golden +++ b/crates/idx-cli/tests/golden/error-invalid-provider.table.golden @@ -1,4 +1,4 @@ exit: 2 --- stdout --- stderr -Error: config error: invalid provider 'bogus' (expected yahoo or msn) +Error: config error: invalid provider 'bogus' (expected yahoo, msn, or stockbit) diff --git a/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.json.golden b/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.json.golden new file mode 100644 index 0000000..85f90cb --- /dev/null +++ b/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.json.golden @@ -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"} diff --git a/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.table.golden b/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.table.golden new file mode 100644 index 0000000..905987e --- /dev/null +++ b/crates/idx-cli/tests/golden/error-stockbit-fundamental-unsupported.table.golden @@ -0,0 +1,4 @@ +exit: 5 +--- stdout +--- stderr +Error: unsupported: BBCA.JK: Stockbit has no fundamentals; command requires --provider msn or yahoo diff --git a/crates/idx-cli/tests/golden/help-cache.golden b/crates/idx-cli/tests/golden/help-cache.golden index 27f05b0..95c3001 100644 --- a/crates/idx-cli/tests/golden/help-cache.golden +++ b/crates/idx-cli/tests/golden/help-cache.golden @@ -10,11 +10,12 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 --- stderr diff --git a/crates/idx-cli/tests/golden/help-config.golden b/crates/idx-cli/tests/golden/help-config.golden index 845a21d..305e163 100644 --- a/crates/idx-cli/tests/golden/help-config.golden +++ b/crates/idx-cli/tests/golden/help-config.golden @@ -28,6 +28,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests diff --git a/crates/idx-cli/tests/golden/help-ownership.golden b/crates/idx-cli/tests/golden/help-ownership.golden index a7e4741..202f0da 100644 --- a/crates/idx-cli/tests/golden/help-ownership.golden +++ b/crates/idx-cli/tests/golden/help-ownership.golden @@ -40,6 +40,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests diff --git a/crates/idx-cli/tests/golden/help-root.golden b/crates/idx-cli/tests/golden/help-root.golden index 2814cde..5593677 100644 --- a/crates/idx-cli/tests/golden/help-root.golden +++ b/crates/idx-cli/tests/golden/help-root.golden @@ -29,6 +29,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests diff --git a/crates/idx-cli/tests/golden/help-stocks.golden b/crates/idx-cli/tests/golden/help-stocks.golden index 368e4a9..af937f3 100644 --- a/crates/idx-cli/tests/golden/help-stocks.golden +++ b/crates/idx-cli/tests/golden/help-stocks.golden @@ -6,6 +6,7 @@ Usage: idx stocks [OPTIONS] Commands: quote Get real-time stock quotes + intraday Today's session price by the minute (Stockbit) history Get historical OHLC data technical Run technical 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) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 --- stderr diff --git a/crates/idx-cli/tests/golden/stockbit-quote.json.golden b/crates/idx-cli/tests/golden/stockbit-quote.json.golden new file mode 100644 index 0000000..5f33145 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-quote.json.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stockbit-quote.table.golden b/crates/idx-cli/tests/golden/stockbit-quote.table.golden new file mode 100644 index 0000000..e8ee346 --- /dev/null +++ b/crates/idx-cli/tests/golden/stockbit-quote.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden b/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden new file mode 100644 index 0000000..b284ebc --- /dev/null +++ b/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stocks-intraday-suspended.json.golden b/crates/idx-cli/tests/golden/stocks-intraday-suspended.json.golden new file mode 100644 index 0000000..f29ffce --- /dev/null +++ b/crates/idx-cli/tests/golden/stocks-intraday-suspended.json.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stocks-intraday-suspended.table.golden b/crates/idx-cli/tests/golden/stocks-intraday-suspended.table.golden new file mode 100644 index 0000000..bfb97a3 --- /dev/null +++ b/crates/idx-cli/tests/golden/stocks-intraday-suspended.table.golden @@ -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 diff --git a/crates/idx-cli/tests/golden/stocks-intraday.json.golden b/crates/idx-cli/tests/golden/stocks-intraday.json.golden new file mode 100644 index 0000000..54882c9 --- /dev/null +++ b/crates/idx-cli/tests/golden/stocks-intraday.json.golden @@ -0,0 +1,4 @@ +exit: 0 +--- stdout +{"quote":{"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":"2026-10-22T16:14:00+07:00","behind_session":false,"market_status":"close"},"points":[{"time":"2026-10-22T08:58:00+07:00","price":5125.0},{"time":"2026-10-22T09:00:00+07:00","price":5125.0},{"time":"2026-10-22T09:01:00+07:00","price":5175.0},{"time":"2026-10-22T09:02:00+07:00","price":5150.0},{"time":"2026-10-22T09:03:00+07:00","price":5175.0},{"time":"2026-10-22T09:04:00+07:00","price":5175.0},{"time":"2026-10-22T09:05:00+07:00","price":5175.0},{"time":"2026-10-22T09:06:00+07:00","price":5175.0},{"time":"2026-10-22T09:07:00+07:00","price":5200.0},{"time":"2026-10-22T09:08:00+07:00","price":5175.0},{"time":"2026-10-22T09:09:00+07:00","price":5200.0},{"time":"2026-10-22T09:10:00+07:00","price":5200.0},{"time":"2026-10-22T09:11:00+07:00","price":5200.0},{"time":"2026-10-22T09:12:00+07:00","price":5200.0},{"time":"2026-10-22T09:13:00+07:00","price":5200.0},{"time":"2026-10-22T09:14:00+07:00","price":5225.0},{"time":"2026-10-22T09:15:00+07:00","price":5200.0},{"time":"2026-10-22T09:16:00+07:00","price":5225.0},{"time":"2026-10-22T09:17:00+07:00","price":5225.0},{"time":"2026-10-22T09:18:00+07:00","price":5200.0},{"time":"2026-10-22T09:19:00+07:00","price":5225.0},{"time":"2026-10-22T09:20:00+07:00","price":5250.0},{"time":"2026-10-22T09:21:00+07:00","price":5225.0},{"time":"2026-10-22T09:22:00+07:00","price":5225.0},{"time":"2026-10-22T09:23:00+07:00","price":5225.0},{"time":"2026-10-22T09:24:00+07:00","price":5225.0},{"time":"2026-10-22T09:25:00+07:00","price":5200.0},{"time":"2026-10-22T09:26:00+07:00","price":5200.0},{"time":"2026-10-22T09:27:00+07:00","price":5225.0},{"time":"2026-10-22T09:28:00+07:00","price":5225.0},{"time":"2026-10-22T09:29:00+07:00","price":5250.0},{"time":"2026-10-22T09:30:00+07:00","price":5225.0},{"time":"2026-10-22T09:31:00+07:00","price":5225.0},{"time":"2026-10-22T09:32:00+07:00","price":5200.0},{"time":"2026-10-22T09:33:00+07:00","price":5225.0},{"time":"2026-10-22T09:34:00+07:00","price":5225.0},{"time":"2026-10-22T09:35:00+07:00","price":5225.0},{"time":"2026-10-22T09:36:00+07:00","price":5250.0},{"time":"2026-10-22T09:37:00+07:00","price":5225.0},{"time":"2026-10-22T09:38:00+07:00","price":5225.0},{"time":"2026-10-22T09:39:00+07:00","price":5225.0},{"time":"2026-10-22T09:40:00+07:00","price":5225.0},{"time":"2026-10-22T09:41:00+07:00","price":5225.0},{"time":"2026-10-22T09:42:00+07:00","price":5225.0},{"time":"2026-10-22T09:43:00+07:00","price":5225.0},{"time":"2026-10-22T09:44:00+07:00","price":5225.0},{"time":"2026-10-22T09:45:00+07:00","price":5200.0},{"time":"2026-10-22T09:46:00+07:00","price":5200.0},{"time":"2026-10-22T09:47:00+07:00","price":5225.0},{"time":"2026-10-22T09:48:00+07:00","price":5200.0},{"time":"2026-10-22T09:49:00+07:00","price":5225.0},{"time":"2026-10-22T09:50:00+07:00","price":5225.0},{"time":"2026-10-22T09:51:00+07:00","price":5225.0},{"time":"2026-10-22T09:52:00+07:00","price":5225.0},{"time":"2026-10-22T09:53:00+07:00","price":5225.0},{"time":"2026-10-22T09:54:00+07:00","price":5200.0},{"time":"2026-10-22T09:55:00+07:00","price":5225.0},{"time":"2026-10-22T09:56:00+07:00","price":5225.0},{"time":"2026-10-22T09:57:00+07:00","price":5225.0},{"time":"2026-10-22T09:58:00+07:00","price":5225.0},{"time":"2026-10-22T09:59:00+07:00","price":5225.0},{"time":"2026-10-22T10:00:00+07:00","price":5250.0},{"time":"2026-10-22T10:01:00+07:00","price":5250.0},{"time":"2026-10-22T10:02:00+07:00","price":5250.0},{"time":"2026-10-22T10:03:00+07:00","price":5250.0},{"time":"2026-10-22T10:04:00+07:00","price":5250.0},{"time":"2026-10-22T10:05:00+07:00","price":5225.0},{"time":"2026-10-22T10:06:00+07:00","price":5225.0},{"time":"2026-10-22T10:07:00+07:00","price":5225.0},{"time":"2026-10-22T10:08:00+07:00","price":5225.0},{"time":"2026-10-22T10:09:00+07:00","price":5225.0},{"time":"2026-10-22T10:10:00+07:00","price":5225.0},{"time":"2026-10-22T10:11:00+07:00","price":5225.0},{"time":"2026-10-22T10:12:00+07:00","price":5225.0},{"time":"2026-10-22T10:13:00+07:00","price":5225.0},{"time":"2026-10-22T10:14:00+07:00","price":5225.0},{"time":"2026-10-22T10:15:00+07:00","price":5225.0},{"time":"2026-10-22T10:16:00+07:00","price":5225.0},{"time":"2026-10-22T10:17:00+07:00","price":5225.0},{"time":"2026-10-22T10:18:00+07:00","price":5225.0},{"time":"2026-10-22T10:19:00+07:00","price":5225.0},{"time":"2026-10-22T10:20:00+07:00","price":5225.0},{"time":"2026-10-22T10:21:00+07:00","price":5225.0},{"time":"2026-10-22T10:22:00+07:00","price":5250.0},{"time":"2026-10-22T10:23:00+07:00","price":5250.0},{"time":"2026-10-22T10:24:00+07:00","price":5250.0},{"time":"2026-10-22T10:25:00+07:00","price":5250.0},{"time":"2026-10-22T10:26:00+07:00","price":5250.0},{"time":"2026-10-22T10:27:00+07:00","price":5250.0},{"time":"2026-10-22T10:28:00+07:00","price":5250.0},{"time":"2026-10-22T10:29:00+07:00","price":5250.0},{"time":"2026-10-22T10:30:00+07:00","price":5225.0},{"time":"2026-10-22T10:31:00+07:00","price":5250.0},{"time":"2026-10-22T10:32:00+07:00","price":5250.0},{"time":"2026-10-22T10:33:00+07:00","price":5250.0},{"time":"2026-10-22T10:34:00+07:00","price":5225.0},{"time":"2026-10-22T10:35:00+07:00","price":5250.0},{"time":"2026-10-22T10:36:00+07:00","price":5250.0},{"time":"2026-10-22T10:37:00+07:00","price":5250.0},{"time":"2026-10-22T10:38:00+07:00","price":5225.0},{"time":"2026-10-22T10:39:00+07:00","price":5250.0},{"time":"2026-10-22T10:40:00+07:00","price":5250.0},{"time":"2026-10-22T10:41:00+07:00","price":5250.0},{"time":"2026-10-22T10:42:00+07:00","price":5225.0},{"time":"2026-10-22T10:43:00+07:00","price":5225.0},{"time":"2026-10-22T10:44:00+07:00","price":5200.0},{"time":"2026-10-22T10:45:00+07:00","price":5200.0},{"time":"2026-10-22T10:46:00+07:00","price":5200.0},{"time":"2026-10-22T10:47:00+07:00","price":5200.0},{"time":"2026-10-22T10:48:00+07:00","price":5225.0},{"time":"2026-10-22T10:49:00+07:00","price":5225.0},{"time":"2026-10-22T10:50:00+07:00","price":5225.0},{"time":"2026-10-22T10:51:00+07:00","price":5200.0},{"time":"2026-10-22T10:52:00+07:00","price":5225.0},{"time":"2026-10-22T10:53:00+07:00","price":5225.0},{"time":"2026-10-22T10:54:00+07:00","price":5200.0},{"time":"2026-10-22T10:55:00+07:00","price":5200.0},{"time":"2026-10-22T10:56:00+07:00","price":5225.0},{"time":"2026-10-22T10:57:00+07:00","price":5200.0},{"time":"2026-10-22T10:58:00+07:00","price":5200.0},{"time":"2026-10-22T10:59:00+07:00","price":5225.0},{"time":"2026-10-22T11:00:00+07:00","price":5200.0},{"time":"2026-10-22T11:01:00+07:00","price":5200.0},{"time":"2026-10-22T11:02:00+07:00","price":5225.0},{"time":"2026-10-22T11:03:00+07:00","price":5225.0},{"time":"2026-10-22T11:04:00+07:00","price":5200.0},{"time":"2026-10-22T11:05:00+07:00","price":5225.0},{"time":"2026-10-22T11:06:00+07:00","price":5250.0},{"time":"2026-10-22T11:07:00+07:00","price":5225.0},{"time":"2026-10-22T11:08:00+07:00","price":5225.0},{"time":"2026-10-22T11:09:00+07:00","price":5250.0},{"time":"2026-10-22T11:10:00+07:00","price":5250.0},{"time":"2026-10-22T11:11:00+07:00","price":5225.0},{"time":"2026-10-22T11:12:00+07:00","price":5250.0},{"time":"2026-10-22T11:13:00+07:00","price":5250.0},{"time":"2026-10-22T11:14:00+07:00","price":5250.0},{"time":"2026-10-22T11:15:00+07:00","price":5250.0},{"time":"2026-10-22T11:16:00+07:00","price":5250.0},{"time":"2026-10-22T11:17:00+07:00","price":5250.0},{"time":"2026-10-22T11:18:00+07:00","price":5225.0},{"time":"2026-10-22T11:19:00+07:00","price":5225.0},{"time":"2026-10-22T11:20:00+07:00","price":5225.0},{"time":"2026-10-22T11:21:00+07:00","price":5225.0},{"time":"2026-10-22T11:22:00+07:00","price":5200.0},{"time":"2026-10-22T11:23:00+07:00","price":5225.0},{"time":"2026-10-22T11:24:00+07:00","price":5200.0},{"time":"2026-10-22T11:25:00+07:00","price":5200.0},{"time":"2026-10-22T11:26:00+07:00","price":5225.0},{"time":"2026-10-22T11:27:00+07:00","price":5225.0},{"time":"2026-10-22T11:28:00+07:00","price":5200.0},{"time":"2026-10-22T11:29:00+07:00","price":5225.0},{"time":"2026-10-22T11:30:00+07:00","price":5225.0},{"time":"2026-10-22T11:31:00+07:00","price":5200.0},{"time":"2026-10-22T11:32:00+07:00","price":5200.0},{"time":"2026-10-22T11:33:00+07:00","price":5225.0},{"time":"2026-10-22T11:34:00+07:00","price":5225.0},{"time":"2026-10-22T11:35:00+07:00","price":5200.0},{"time":"2026-10-22T11:36:00+07:00","price":5225.0},{"time":"2026-10-22T11:37:00+07:00","price":5225.0},{"time":"2026-10-22T11:38:00+07:00","price":5225.0},{"time":"2026-10-22T11:39:00+07:00","price":5200.0},{"time":"2026-10-22T11:40:00+07:00","price":5225.0},{"time":"2026-10-22T11:41:00+07:00","price":5225.0},{"time":"2026-10-22T11:42:00+07:00","price":5200.0},{"time":"2026-10-22T11:43:00+07:00","price":5225.0},{"time":"2026-10-22T11:44:00+07:00","price":5200.0},{"time":"2026-10-22T11:45:00+07:00","price":5225.0},{"time":"2026-10-22T11:46:00+07:00","price":5225.0},{"time":"2026-10-22T11:47:00+07:00","price":5225.0},{"time":"2026-10-22T11:48:00+07:00","price":5250.0},{"time":"2026-10-22T11:49:00+07:00","price":5250.0},{"time":"2026-10-22T11:50:00+07:00","price":5225.0},{"time":"2026-10-22T11:51:00+07:00","price":5225.0},{"time":"2026-10-22T11:52:00+07:00","price":5225.0},{"time":"2026-10-22T11:53:00+07:00","price":5225.0},{"time":"2026-10-22T11:54:00+07:00","price":5225.0},{"time":"2026-10-22T11:55:00+07:00","price":5225.0},{"time":"2026-10-22T11:56:00+07:00","price":5225.0},{"time":"2026-10-22T11:57:00+07:00","price":5225.0},{"time":"2026-10-22T11:58:00+07:00","price":5250.0},{"time":"2026-10-22T11:59:00+07:00","price":5250.0},{"time":"2026-10-22T13:30:00+07:00","price":5225.0},{"time":"2026-10-22T13:31:00+07:00","price":5200.0},{"time":"2026-10-22T13:32:00+07:00","price":5200.0},{"time":"2026-10-22T13:33:00+07:00","price":5200.0},{"time":"2026-10-22T13:34:00+07:00","price":5200.0},{"time":"2026-10-22T13:35:00+07:00","price":5225.0},{"time":"2026-10-22T13:36:00+07:00","price":5225.0},{"time":"2026-10-22T13:37:00+07:00","price":5225.0},{"time":"2026-10-22T13:38:00+07:00","price":5200.0},{"time":"2026-10-22T13:39:00+07:00","price":5225.0},{"time":"2026-10-22T13:40:00+07:00","price":5200.0},{"time":"2026-10-22T13:41:00+07:00","price":5225.0},{"time":"2026-10-22T13:42:00+07:00","price":5225.0},{"time":"2026-10-22T13:43:00+07:00","price":5200.0},{"time":"2026-10-22T13:44:00+07:00","price":5200.0},{"time":"2026-10-22T13:45:00+07:00","price":5175.0},{"time":"2026-10-22T13:46:00+07:00","price":5175.0},{"time":"2026-10-22T13:47:00+07:00","price":5175.0},{"time":"2026-10-22T13:48:00+07:00","price":5175.0},{"time":"2026-10-22T13:49:00+07:00","price":5200.0},{"time":"2026-10-22T13:50:00+07:00","price":5200.0},{"time":"2026-10-22T13:51:00+07:00","price":5175.0},{"time":"2026-10-22T13:52:00+07:00","price":5200.0},{"time":"2026-10-22T13:53:00+07:00","price":5200.0},{"time":"2026-10-22T13:54:00+07:00","price":5175.0},{"time":"2026-10-22T13:55:00+07:00","price":5175.0},{"time":"2026-10-22T13:56:00+07:00","price":5175.0},{"time":"2026-10-22T13:57:00+07:00","price":5200.0},{"time":"2026-10-22T13:58:00+07:00","price":5200.0},{"time":"2026-10-22T13:59:00+07:00","price":5200.0},{"time":"2026-10-22T14:00:00+07:00","price":5200.0},{"time":"2026-10-22T14:01:00+07:00","price":5175.0},{"time":"2026-10-22T14:02:00+07:00","price":5200.0},{"time":"2026-10-22T14:03:00+07:00","price":5200.0},{"time":"2026-10-22T14:04:00+07:00","price":5175.0},{"time":"2026-10-22T14:05:00+07:00","price":5175.0},{"time":"2026-10-22T14:06:00+07:00","price":5175.0},{"time":"2026-10-22T14:07:00+07:00","price":5175.0},{"time":"2026-10-22T14:08:00+07:00","price":5175.0},{"time":"2026-10-22T14:09:00+07:00","price":5175.0},{"time":"2026-10-22T14:10:00+07:00","price":5175.0},{"time":"2026-10-22T14:11:00+07:00","price":5200.0},{"time":"2026-10-22T14:12:00+07:00","price":5200.0},{"time":"2026-10-22T14:13:00+07:00","price":5200.0},{"time":"2026-10-22T14:14:00+07:00","price":5175.0},{"time":"2026-10-22T14:15:00+07:00","price":5200.0},{"time":"2026-10-22T14:16:00+07:00","price":5175.0},{"time":"2026-10-22T14:17:00+07:00","price":5175.0},{"time":"2026-10-22T14:18:00+07:00","price":5175.0},{"time":"2026-10-22T14:19:00+07:00","price":5175.0},{"time":"2026-10-22T14:20:00+07:00","price":5200.0},{"time":"2026-10-22T14:21:00+07:00","price":5200.0},{"time":"2026-10-22T14:22:00+07:00","price":5175.0},{"time":"2026-10-22T14:23:00+07:00","price":5175.0},{"time":"2026-10-22T14:24:00+07:00","price":5200.0},{"time":"2026-10-22T14:25:00+07:00","price":5175.0},{"time":"2026-10-22T14:26:00+07:00","price":5200.0},{"time":"2026-10-22T14:27:00+07:00","price":5200.0},{"time":"2026-10-22T14:28:00+07:00","price":5200.0},{"time":"2026-10-22T14:29:00+07:00","price":5200.0},{"time":"2026-10-22T14:30:00+07:00","price":5175.0},{"time":"2026-10-22T14:31:00+07:00","price":5200.0},{"time":"2026-10-22T14:32:00+07:00","price":5175.0},{"time":"2026-10-22T14:33:00+07:00","price":5200.0},{"time":"2026-10-22T14:34:00+07:00","price":5200.0},{"time":"2026-10-22T14:35:00+07:00","price":5200.0},{"time":"2026-10-22T14:36:00+07:00","price":5225.0},{"time":"2026-10-22T14:37:00+07:00","price":5200.0},{"time":"2026-10-22T14:38:00+07:00","price":5200.0},{"time":"2026-10-22T14:39:00+07:00","price":5200.0},{"time":"2026-10-22T14:40:00+07:00","price":5200.0},{"time":"2026-10-22T14:41:00+07:00","price":5200.0},{"time":"2026-10-22T14:42:00+07:00","price":5200.0},{"time":"2026-10-22T14:43:00+07:00","price":5200.0},{"time":"2026-10-22T14:44:00+07:00","price":5200.0},{"time":"2026-10-22T14:45:00+07:00","price":5200.0},{"time":"2026-10-22T14:46:00+07:00","price":5200.0},{"time":"2026-10-22T14:47:00+07:00","price":5175.0},{"time":"2026-10-22T14:48:00+07:00","price":5175.0},{"time":"2026-10-22T14:49:00+07:00","price":5200.0},{"time":"2026-10-22T14:50:00+07:00","price":5200.0},{"time":"2026-10-22T14:51:00+07:00","price":5200.0},{"time":"2026-10-22T14:52:00+07:00","price":5175.0},{"time":"2026-10-22T14:53:00+07:00","price":5200.0},{"time":"2026-10-22T14:54:00+07:00","price":5200.0},{"time":"2026-10-22T14:55:00+07:00","price":5225.0},{"time":"2026-10-22T14:56:00+07:00","price":5225.0},{"time":"2026-10-22T14:57:00+07:00","price":5225.0},{"time":"2026-10-22T14:58:00+07:00","price":5200.0},{"time":"2026-10-22T14:59:00+07:00","price":5200.0},{"time":"2026-10-22T15:00:00+07:00","price":5175.0},{"time":"2026-10-22T15:01:00+07:00","price":5200.0},{"time":"2026-10-22T15:02:00+07:00","price":5200.0},{"time":"2026-10-22T15:03:00+07:00","price":5200.0},{"time":"2026-10-22T15:04:00+07:00","price":5200.0},{"time":"2026-10-22T15:05:00+07:00","price":5200.0},{"time":"2026-10-22T15:06:00+07:00","price":5200.0},{"time":"2026-10-22T15:07:00+07:00","price":5175.0},{"time":"2026-10-22T15:08:00+07:00","price":5200.0},{"time":"2026-10-22T15:09:00+07:00","price":5200.0},{"time":"2026-10-22T15:10:00+07:00","price":5175.0},{"time":"2026-10-22T15:11:00+07:00","price":5175.0},{"time":"2026-10-22T15:12:00+07:00","price":5175.0},{"time":"2026-10-22T15:13:00+07:00","price":5200.0},{"time":"2026-10-22T15:14:00+07:00","price":5175.0},{"time":"2026-10-22T15:15:00+07:00","price":5175.0},{"time":"2026-10-22T15:16:00+07:00","price":5175.0},{"time":"2026-10-22T15:17:00+07:00","price":5200.0},{"time":"2026-10-22T15:18:00+07:00","price":5175.0},{"time":"2026-10-22T15:19:00+07:00","price":5175.0},{"time":"2026-10-22T15:20:00+07:00","price":5150.0},{"time":"2026-10-22T15:21:00+07:00","price":5175.0},{"time":"2026-10-22T15:22:00+07:00","price":5175.0},{"time":"2026-10-22T15:23:00+07:00","price":5175.0},{"time":"2026-10-22T15:24:00+07:00","price":5175.0},{"time":"2026-10-22T15:25:00+07:00","price":5150.0},{"time":"2026-10-22T15:26:00+07:00","price":5175.0},{"time":"2026-10-22T15:27:00+07:00","price":5175.0},{"time":"2026-10-22T15:28:00+07:00","price":5175.0},{"time":"2026-10-22T15:29:00+07:00","price":5175.0},{"time":"2026-10-22T15:30:00+07:00","price":5200.0},{"time":"2026-10-22T15:31:00+07:00","price":5200.0},{"time":"2026-10-22T15:32:00+07:00","price":5200.0},{"time":"2026-10-22T15:33:00+07:00","price":5200.0},{"time":"2026-10-22T15:34:00+07:00","price":5200.0},{"time":"2026-10-22T15:35:00+07:00","price":5175.0},{"time":"2026-10-22T15:36:00+07:00","price":5200.0},{"time":"2026-10-22T15:37:00+07:00","price":5175.0},{"time":"2026-10-22T15:38:00+07:00","price":5175.0},{"time":"2026-10-22T15:39:00+07:00","price":5175.0},{"time":"2026-10-22T15:40:00+07:00","price":5150.0},{"time":"2026-10-22T15:41:00+07:00","price":5150.0},{"time":"2026-10-22T15:42:00+07:00","price":5175.0},{"time":"2026-10-22T15:43:00+07:00","price":5175.0},{"time":"2026-10-22T15:44:00+07:00","price":5200.0},{"time":"2026-10-22T15:45:00+07:00","price":5200.0},{"time":"2026-10-22T15:46:00+07:00","price":5175.0},{"time":"2026-10-22T15:47:00+07:00","price":5200.0},{"time":"2026-10-22T15:48:00+07:00","price":5175.0},{"time":"2026-10-22T15:49:00+07:00","price":5175.0},{"time":"2026-10-22T16:00:00+07:00","price":5175.0},{"time":"2026-10-22T16:02:00+07:00","price":5175.0},{"time":"2026-10-22T16:03:00+07:00","price":5175.0},{"time":"2026-10-22T16:04:00+07:00","price":5175.0},{"time":"2026-10-22T16:05:00+07:00","price":5175.0},{"time":"2026-10-22T16:06:00+07:00","price":5175.0},{"time":"2026-10-22T16:07:00+07:00","price":5175.0},{"time":"2026-10-22T16:08:00+07:00","price":5175.0},{"time":"2026-10-22T16:09:00+07:00","price":5175.0},{"time":"2026-10-22T16:10:00+07:00","price":5175.0},{"time":"2026-10-22T16:11:00+07:00","price":5175.0},{"time":"2026-10-22T16:12:00+07:00","price":5175.0},{"time":"2026-10-22T16:13:00+07:00","price":5175.0},{"time":"2026-10-22T16:14:00+07:00","price":5175.0}]} +--- stderr diff --git a/crates/idx-cli/tests/golden/stocks-intraday.table.golden b/crates/idx-cli/tests/golden/stocks-intraday.table.golden new file mode 100644 index 0000000..35c6cf8 --- /dev/null +++ b/crates/idx-cli/tests/golden/stocks-intraday.table.golden @@ -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 diff --git a/crates/idx-core/src/api/mock.rs b/crates/idx-core/src/api/mock.rs index 64c8694..1a90c92 100644 --- a/crates/idx-core/src/api/mock.rs +++ b/crates/idx-core/src/api/mock.rs @@ -1,10 +1,10 @@ //! Fixture-backed provider for tests, enabled by the `testing` feature and //! `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::{ - BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, QuoteProvider, - TradingSummaryProvider, idx, ipot, msn, yahoo, + BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, IntradayProvider, QuoteProvider, + TradingSummaryProvider, idx, ipot, msn, stockbit, yahoo, }; use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery}; use crate::config::ProviderKind; @@ -45,6 +45,10 @@ impl MockProvider { match provider { ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose), 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_.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 { + 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 { + 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 { + Err(stockbit::fundamentals_unsupported(symbol)) + } +} + +impl IntradayProvider for MockStockbitProvider { + fn intraday(&self, symbol: &str) -> Result { + self.snapshot(symbol) + } +} + /// Broker summaries from `tests/fixtures/ipot_brokersummary__ /// [_][__].html`; a query without a fixture reads as an /// unknown code. diff --git a/crates/idx-core/src/api/mod.rs b/crates/idx-core/src/api/mod.rs index 3d3a8fe..8ff9fdd 100644 --- a/crates/idx-core/src/api/mod.rs +++ b/crates/idx-core/src/api/mod.rs @@ -3,6 +3,7 @@ pub mod ipot; #[cfg(feature = "testing")] pub mod mock; pub mod msn; +pub mod stockbit; pub mod types; pub mod yahoo; @@ -13,7 +14,7 @@ use crate::config::{HistoryProviderKind, ProviderKind}; use crate::error::IdxError; use types::{ 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 @@ -64,6 +65,13 @@ pub trait TradingSummaryProvider { ) -> Result, 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; +} + pub trait FundamentalsProvider { fn fundamentals(&self, symbol: &str) -> Result; } @@ -221,7 +229,10 @@ fn mock_enabled() -> bool { #[cfg(feature = "testing")] fn mock_market(provider: ProviderKind) -> Option> { - 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"))] @@ -274,6 +285,18 @@ fn build_selected_provider( news: 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 { Box::new(idx::IdxSiteProvider) } +pub fn intraday_provider() -> Box { + #[cfg(feature = "testing")] + if mock::enabled() { + return Box::new(mock::MockStockbitProvider); + } + Box::new(stockbit::StockbitProvider::new()) +} + /// Today in WIB, the date IDX trades on. pub(crate) fn today_wib() -> chrono::NaiveDate { #[cfg(feature = "testing")] @@ -312,8 +343,7 @@ pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind) HistoryProviderKind::Yahoo => ProviderKind::Yahoo, HistoryProviderKind::Msn => ProviderKind::Msn, HistoryProviderKind::Auto => match provider { - ProviderKind::Yahoo => ProviderKind::Yahoo, - ProviderKind::Msn => ProviderKind::Yahoo, + ProviderKind::Yahoo | ProviderKind::Msn | ProviderKind::Stockbit => ProviderKind::Yahoo, }, } } @@ -339,6 +369,9 @@ pub fn history_provider( match resolved { ProviderKind::Yahoo => Ok((resolved, Box::new(yahoo::YahooProvider::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(), + )), } } diff --git a/crates/idx-core/src/api/msn/map.rs b/crates/idx-core/src/api/msn/map.rs index a643841..165bd07 100644 --- a/crates/idx-core/src/api/msn/map.rs +++ b/crates/idx-core/src/api/msn/map.rs @@ -71,6 +71,10 @@ pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result Result 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 { + let code = stockbit_code(resolved)?; + let html = self.page(&code, resolved)?; + parse_symbol_page(resolved, &html) + } + + fn page(&self, code: &str, resolved: &str) -> Result { + 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 +/// `/stockbit.pace` under an exclusive file lock. When that file is +/// unusable, pacing is per process. +fn pace() { + static LAST: Mutex> = 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::() { + // 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 { + self.snapshot(symbol).map(|day| day.quote) + } +} + +impl FundamentalsProvider for StockbitProvider { + fn fundamentals(&self, symbol: &str) -> Result { + Err(fundamentals_unsupported(symbol)) + } +} + +impl IntradayProvider for StockbitProvider { + fn intraday(&self, symbol: &str) -> Result { + 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}" + ); + } + } +} diff --git a/crates/idx-core/src/api/stockbit/parse.rs b/crates/idx-core/src/api/stockbit/parse.rs new file mode 100644 index 0000000..54c5ecd --- /dev/null +++ b/crates/idx-core/src/api/stockbit/parse.rs @@ -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, + chart: Option, +} + +#[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, + time: Option, + status: Option, + orderbook: Option, + market_hour: Option, +} + +#[derive(Debug, Deserialize)] +struct RawOrderbook { + bid: Option, + offer: Option, +} + +#[derive(Debug, Deserialize)] +struct RawLevel { + #[serde(default)] + price: Value, +} + +#[derive(Debug, Deserialize)] +struct RawMarketHour { + status: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct RawChart { + #[serde(default)] + prices: Vec, +} + +#[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 { + 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 { + 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 ``. +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("").ok_or_else(missing)?; + Ok(&html[start..start + len]) +} + +fn parse_quote(resolved: &str, company: &RawCompany) -> Result { + 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, 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> { + 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> { + 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 { + let n = match value { + Value::Number(n) => n.as_f64()?, + Value::String(s) => s.trim().parse::().ok()?, + _ => return None, + }; + n.is_finite().then_some(n) +} + +fn count(value: &Value) -> Option { + match value { + Value::Number(n) => n.as_u64(), + Value::String(s) => s.trim().parse::().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}" + ); + } + } +} diff --git a/crates/idx-core/src/api/types.rs b/crates/idx-core/src/api/types.rs index deaaafa..1da6d4d 100644 --- a/crates/idx-core/src/api/types.rs +++ b/crates/idx-core/src/api/types.rs @@ -57,6 +57,41 @@ pub struct Quote { /// quote is returned, not when it is cached. #[serde(default)] 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, + /// Best offer in IDR (whole Rupiah), gated like `bid`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub offer: Option, + /// 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, + /// 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, +} + +/// 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, + /// 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, } /// OHLC candle data normalized from Yahoo Finance chart indicators. diff --git a/crates/idx-core/src/api/yahoo/map.rs b/crates/idx-core/src/api/yahoo/map.rs index f349b85..7bbfc67 100644 --- a/crates/idx-core/src/api/yahoo/map.rs +++ b/crates/idx-core/src/api/yahoo/map.rs @@ -65,6 +65,10 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result { 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 fetched_at: DateTime, /// True when an expired cache entry was served (offline, or after a diff --git a/crates/idx-core/src/config.rs b/crates/idx-core/src/config.rs index a76c104..3e988da 100644 --- a/crates/idx-core/src/config.rs +++ b/crates/idx-core/src/config.rs @@ -13,10 +13,14 @@ use crate::error::IdxError; pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"]; #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[serde(rename_all = "lowercase")] +#[cfg_attr(feature = "clap", value(rename_all = "lower"))] pub enum ProviderKind { Yahoo, Msn, + // Opt-in: quotes from Stockbit's public page (delayed, no login). + Stockbit, } #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)] @@ -34,6 +38,7 @@ impl ProviderKind { match self { Self::Yahoo => "yahoo", Self::Msn => "msn", + Self::Stockbit => "stockbit", } } @@ -42,9 +47,11 @@ impl ProviderKind { Ok(Self::Yahoo) } else if value.eq_ignore_ascii_case("msn") { Ok(Self::Msn) + } else if value.eq_ignore_ascii_case("stockbit") { + Ok(Self::Stockbit) } else { Err(IdxError::ConfigError(format!( - "invalid provider '{value}' (expected yahoo or msn)" + "invalid provider '{value}' (expected yahoo, msn, or stockbit)" ))) } } diff --git a/crates/idx-core/src/error.rs b/crates/idx-core/src/error.rs index d991c6b..84279a9 100644 --- a/crates/idx-core/src/error.rs +++ b/crates/idx-core/src/error.rs @@ -34,6 +34,10 @@ pub enum IdxError { InvalidInput(String), #[error("no market data: {0}")] 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)] @@ -53,6 +57,7 @@ pub enum ErrorCode { PdfParseError, InvalidInput, NoMarketData, + UpstreamBlocked, } /// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`. @@ -80,6 +85,7 @@ impl IdxError { Self::PdfParseError(_) => ErrorCode::PdfParseError, Self::InvalidInput(_) => ErrorCode::InvalidInput, Self::NoMarketData(_) => ErrorCode::NoMarketData, + Self::UpstreamBlocked(_) => ErrorCode::UpstreamBlocked, } } @@ -93,7 +99,8 @@ impl IdxError { | ErrorCode::RateLimited | ErrorCode::ProviderUnavailable | ErrorCode::Offline - | ErrorCode::AuthError => 4, + | ErrorCode::AuthError + | ErrorCode::UpstreamBlocked => 4, ErrorCode::Unsupported => 5, ErrorCode::Io | ErrorCode::ParseError @@ -119,4 +126,11 @@ mod tests { let unsupported = IdxError::Unsupported("history unavailable".to_string()); 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); + } } diff --git a/crates/idx-core/src/market.rs b/crates/idx-core/src/market.rs index 748458a..e3a0c09 100644 --- a/crates/idx-core/src/market.rs +++ b/crates/idx-core/src/market.rs @@ -6,8 +6,8 @@ use std::str::FromStr; use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday}; -use crate::api::types::{Interval, Ohlc, Period, Quote}; -use crate::api::{history_provider, history_source, idx_offset}; +use crate::api::types::{Interval, Intraday, Ohlc, Period, Quote}; +use crate::api::{history_provider, history_source, idx_offset, intraday_provider, stockbit}; use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch}; use crate::config::{HistoryProviderKind, ProviderKind}; use crate::error::IdxError; @@ -34,6 +34,10 @@ impl Market<'_> { ) -> Result>, IdxError> { let idx = self.idx; 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 mut quotes = Vec::with_capacity(symbols.len()); for symbol in symbols { @@ -43,7 +47,7 @@ impl Market<'_> { CacheSpec { bucket: &bucket, key: &resolved, - ttl_secs: idx.config.quote_ttl, + ttl_secs, subject: &resolved, source: idx.provider.kind().as_str(), }, @@ -60,6 +64,32 @@ impl Market<'_> { 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, 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` /// override, without fetching. pub fn history_source(&self, mode: Option) -> HistorySource { @@ -380,6 +410,10 @@ mod tests { avg_volume: None, as_of: None, behind_session: false, + bid: None, + offer: None, + market_status: None, + session_note: None, } } diff --git a/crates/idx-core/tests/fixtures/stockbit_blocked_403.html b/crates/idx-core/tests/fixtures/stockbit_blocked_403.html new file mode 100644 index 0000000..9ad7b8a --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_blocked_403.html @@ -0,0 +1,20 @@ + + +ERROR: The request could not be satisfied + +

403 ERROR

+

The request could not be satisfied.

+
+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. +
+If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation. +
+
+
+Generated by cloudfront (CloudFront)
+Request ID: VajZoZu7lzhasdjnxU37dHaztROLXHQ6DMpJzTPZoDhkd4I2dVc2mw==
+
+
+
+ diff --git a/crates/idx-core/tests/fixtures/stockbit_symbol_bbca.html b/crates/idx-core/tests/fixtures/stockbit_symbol_bbca.html new file mode 100644 index 0000000..33945bb --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_symbol_bbca.html @@ -0,0 +1 @@ +Stockbit
diff --git a/crates/idx-core/tests/fixtures/stockbit_symbol_ihsg.html b/crates/idx-core/tests/fixtures/stockbit_symbol_ihsg.html new file mode 100644 index 0000000..a6ed659 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_symbol_ihsg.html @@ -0,0 +1 @@ +Stockbit
diff --git a/crates/idx-core/tests/fixtures/stockbit_symbol_maba.html b/crates/idx-core/tests/fixtures/stockbit_symbol_maba.html new file mode 100644 index 0000000..446208f --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_symbol_maba.html @@ -0,0 +1 @@ +Stockbit
diff --git a/crates/idx-core/tests/fixtures/stockbit_symbol_myrx.html b/crates/idx-core/tests/fixtures/stockbit_symbol_myrx.html new file mode 100644 index 0000000..d24b432 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_symbol_myrx.html @@ -0,0 +1 @@ +Stockbit
diff --git a/crates/idx-core/tests/fixtures/stockbit_symbol_zzzz_404.html b/crates/idx-core/tests/fixtures/stockbit_symbol_zzzz_404.html new file mode 100644 index 0000000..acfc10e --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_symbol_zzzz_404.html @@ -0,0 +1 @@ +Error | Stockbit - Investasi Saham Bersama Komunitas Saham Terbesar di Indonesia
diff --git a/crates/idx-mcp/src/params.rs b/crates/idx-mcp/src/params.rs index 88ee860..6f97aea 100644 --- a/crates/idx-mcp/src/params.rs +++ b/crates/idx-mcp/src/params.rs @@ -29,6 +29,15 @@ pub struct HistoryParams { pub interval: Option, } +#[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, +} + #[derive(Debug, Deserialize, JsonSchema)] pub struct ScreenParams { /// Screen to run (default "top-performers"). diff --git a/crates/idx-mcp/src/server.rs b/crates/idx-mcp/src/server.rs index 08580f9..41927c7 100644 --- a/crates/idx-mcp/src/server.rs +++ b/crates/idx-mcp/src/server.rs @@ -26,8 +26,8 @@ use tokio::sync::Semaphore; use crate::params::{ AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams, - CompanySection, EntityParams, FlowParams, HistoryParams, ScreenParams, SearchParams, - ShareholdersParams, SymbolsParams, SyncParams, + CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, ScreenParams, + SearchParams, ShareholdersParams, SymbolsParams, SyncParams, }; 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. const MAX_CHANGES_LIMIT: usize = 300; 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. 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) \ 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." +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) -> ToolResult { 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 } + #[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) -> 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.")] async fn get_price_history(&self, params: Parameters) -> ToolResult { let HistoryParams { diff --git a/crates/idx-mcp/tests/golden/tools.json b/crates/idx-mcp/tests/golden/tools.json index d2ac9b4..375ee44 100644 --- a/crates/idx-mcp/tests/golden/tools.json +++ b/crates/idx-mcp/tests/golden/tools.json @@ -287,6 +287,33 @@ }, "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.", "inputSchema": { @@ -397,7 +424,7 @@ "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": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { diff --git a/crates/idx-mcp/tests/mcp.rs b/crates/idx-mcp/tests/mcp.rs index 928b160..e3902a8 100644 --- a/crates/idx-mcp/tests/mcp.rs +++ b/crates/idx-mcp/tests/mcp.rs @@ -9,13 +9,14 @@ use idx_core::config::CoreConfig; use idx_core::investor::{ImportSource, Investor}; use serde_json::{Value, json}; -const TOOLS: [&str; 14] = [ +const TOOLS: [&str; 15] = [ "analyze_bandar", "analyze_stock", "compare_stocks", "get_broker_summary", "get_company", "get_foreign_flow", + "get_intraday", "get_investor_holdings", "get_ownership_changes", "get_price_history", @@ -36,6 +37,10 @@ struct Client { impl Client { 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(); if seed { 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_DATA_HOME", home.path().join("data")) .env("IDX_USE_MOCK_PROVIDER", "1") + .envs(env.iter().copied()) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::null()) @@ -299,6 +305,63 @@ fn live_tools_wrap_data_with_freshness() { 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] fn errors_carry_cli_codes() { let mut client = Client::start(&[], false); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ae55edd..0343bd0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -55,6 +55,9 @@ command parses arguments, makes one idx-core call, and renders the result. (`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries). - **IDX (idx.co.id)** — the daily trading summary behind `stocks flow --source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow). +- **Stockbit (public page)** — `stocks intraday` whichever provider is + selected (`IntradayProvider`), and quotes under the opt-in + `--provider stockbit`. See [Stockbit](#stockbit). ### MSN Endpoints - `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 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/ (+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 + `/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 ``` MarketDataProvider = QuoteProvider + FundamentalsProvider @@ -161,8 +199,13 @@ Factory functions: history_provider(kind, mode, verbose) → Result<(ProviderKind, Box)> broker_provider() → Box trading_summary_provider() → Box + intraday_provider() → Box ``` +`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) 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. - Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/ - `PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`, `5` `UNSUPPORTED`, `1` everything else + `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. ## CLI Output and Offline Cache diff --git a/docs/DESIGN.md b/docs/DESIGN.md index fbb6a02..f9e8b20 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -184,7 +184,7 @@ is another 20-30% smaller than pretty JSON. | 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... | | 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` | | 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` | - | 4 | upstream unavailable, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR` | + | 4 | upstream unavailable or blocked, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR`, `UPSTREAMBLOCKED` | | 5 | unsupported | `UNSUPPORTED` | - JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths diff --git a/docs/SMOKE.md b/docs/SMOKE.md index 5f56439..70f43b1 100644 --- a/docs/SMOKE.md +++ b/docs/SMOKE.md @@ -42,12 +42,12 @@ contents. The native macOS installer job runs separately to exercise BSD tools. ## 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 validates each JSON output shape. It is opt-in and never part of the gate. ```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 --bin ./result/bin/idx 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 them is fine), plus `compare BUMI,ADRO,AIMS`, 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 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. +A case whose upstream refuses us (`UPSTREAMBLOCKED`, an HTTP 403) prints +`BLOCKED (HTTP 403)` and counts as a failure, never as a pass. + `--compare ` runs every case with both binaries back to back, each with its own config and cache, and fails on a different exit code, any added, missing, or retyped JSON key, or changed values for `profile`, `financials`, diff --git a/flake.nix b/flake.nix index b3b9b45..bda6f06 100644 --- a/flake.nix +++ b/flake.nix @@ -288,7 +288,7 @@ echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' sleep 2 } | 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" ''; # The website's generated pages match the CLI help and the MCP diff --git a/scripts/live-smoke.sh b/scripts/live-smoke.sh index 98dbfb9..7281509 100755 --- a/scripts/live-smoke.sh +++ b/scripts/live-smoke.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Live provider drift check: runs shipped commands against real MSN, Yahoo, -# and IDX endpoints and validates the JSON output shape. Deterministic CLI -# behavior is covered by tests/cli.rs and tests/golden.rs; this answers "do the -# upstreams still look the way our parsers expect?" +# IPOT, IDX, and Stockbit endpoints and validates the JSON output shape. +# Deterministic CLI behavior is covered by tests/cli.rs and tests/golden.rs; +# 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 , 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 @@ -26,7 +28,8 @@ usage() { Usage: scripts/live-smoke.sh [options] Options: - --group Restrict to a group. Repeatable. Default: both + --group stocks, stockbit, or ownership. Repeatable. + Default: all three --symbol Primary ticker. Default: BBCA --bin Use an existing idx binary (skips cargo build) --compare 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 ;; esac done -[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks ownership) +[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks stockbit ownership) for group in "${GROUPS_SELECTED[@]}"; do case "$group" in - stocks|ownership) ;; - *) echo "invalid --group '$group' (expected stocks or ownership)" >&2; exit 2 ;; + stocks|stockbit|ownership) ;; + *) echo "invalid --group '$group' (expected stocks, stockbit, or ownership)" >&2; exit 2 ;; esac 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. contract() { 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"))' ;; + "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 "*) echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;; "stocks technical "*) @@ -172,6 +177,11 @@ run_case() { fi fi 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)") echo " FAIL exit" return @@ -238,6 +248,13 @@ if selected stocks; then run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx" 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 run_case discover "ownership discover --limit 1" if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then diff --git a/web/astro.config.mjs b/web/astro.config.mjs index b9a0091..c745c23 100644 --- a/web/astro.config.mjs +++ b/web/astro.config.mjs @@ -31,7 +31,7 @@ export default defineConfig({ }, { 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', diff --git a/web/src/content/docs/cli/output.md b/web/src/content/docs/cli/output.md index ed72dba..60a5659 100644 --- a/web/src/content/docs/cli/output.md +++ b/web/src/content/docs/cli/output.md @@ -32,7 +32,7 @@ stdout is not a terminal, and with `--no-color`. | `0` | success | | | `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 | -| `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 | | `1` | anything else | read the message | diff --git a/web/src/content/docs/cli/reference.md b/web/src/content/docs/cli/reference.md index 5a89111..8cf8a18 100644 --- a/web/src/content/docs/cli/reference.md +++ b/web/src/content/docs/cli/reference.md @@ -41,6 +41,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -60,6 +65,7 @@ Usage: idx stocks [OPTIONS] Commands: quote Get real-time stock quotes + intraday Today's session price by the minute (Stockbit) history Get historical OHLC data technical Run technical 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) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx stocks quote` @@ -100,19 +107,67 @@ Arguments: ... One or more symbols, comma-separated or space-separated; `-` reads them from stdin Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks quote BBCA idx stocks quote BBCA,BBRI,BMRI idx -o json stocks quote BBCA 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] + +Arguments: + + Single ticker symbol (e.g. BBCA) + +Options: + -o, --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 + 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` @@ -140,6 +195,8 @@ Options: Suppress non-essential output -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit] --offline Run without network requests --no-cache @@ -173,6 +230,8 @@ Options: Suppress non-essential output -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit] --offline Run without network requests --no-cache @@ -196,13 +255,14 @@ Arguments: Single ticker symbol (e.g. BBCA) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks growth BBCA @@ -220,13 +280,14 @@ Arguments: Single ticker symbol (e.g. BBCA) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks valuation BBCA @@ -244,13 +305,14 @@ Arguments: Single ticker symbol (e.g. BBCA) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks risk BBCA @@ -268,13 +330,14 @@ Arguments: Single ticker symbol (e.g. BBCA) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks fundamental BBCA @@ -337,6 +400,11 @@ Options: [default: all] + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -409,17 +477,22 @@ Options: [default: all] + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests - --no-cache - Bypass local cache - --top Tier behind the headline signal: 1, 3, 5 or 10 brokers per side [default: 5] + --no-cache + Bypass local cache + -h, --help Print help (see a summary with '-h') @@ -474,6 +547,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -502,13 +580,14 @@ Arguments: Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx stocks financials` @@ -527,6 +606,7 @@ Options: --no-color Disable colored output -q, --quiet Suppress non-essential output -v, --verbose... Increase verbosity (-v, -vv) + --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 @@ -548,17 +628,18 @@ Arguments: Options: - --forecast Only include forward earnings rows - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --history Only include historical earnings rows - --no-color Disable colored output - --annual Only include annual periods - -q, --quiet Suppress non-essential output - --quarterly Only include quarterly periods - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --forecast Only include forward earnings rows + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --history Only include historical earnings rows + --no-color Disable colored output + --annual Only include annual periods + -q, --quiet Suppress non-essential output + --quarterly Only include quarterly periods + -v, --verbose... Increase verbosity (-v, -vv) + --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 Examples: idx stocks earnings BBCA @@ -577,13 +658,14 @@ Arguments: Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx stocks insights` @@ -597,13 +679,14 @@ Arguments: Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx stocks news` @@ -617,14 +700,15 @@ Arguments: Options: - --limit [default: 10] - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --limit [default: 10] + -o, --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 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 ``` ### `idx stocks screen` @@ -635,16 +719,17 @@ MSN screener Usage: idx stocks screen [OPTIONS] Options: - --filter [default: top-performers] - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --no-color Disable colored output - --region [default: id] - --limit [default: 50] - -q, --quiet Suppress non-essential output - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --filter [default: top-performers] + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --no-color Disable colored output + --region [default: id] + --limit [default: 50] + -q, --quiet Suppress non-essential output + -v, --verbose... Increase verbosity (-v, -vv) + --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 ``` ### `idx stocks compare` @@ -658,13 +743,14 @@ Arguments: ... One or more symbols, comma-separated or space-separated; `-` reads them from stdin Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx stocks compare BBCA BBRI BMRI @@ -704,6 +790,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -733,13 +824,14 @@ Create default configuration file Usage: idx config init [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx config get` @@ -753,13 +845,14 @@ Arguments: Key such as general.provider (see `idx config --help`) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx config set` @@ -774,13 +867,14 @@ Arguments: Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx config path` @@ -791,13 +885,14 @@ Print configuration file path Usage: idx config path [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ## `idx cache` @@ -813,13 +908,14 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx cache info` @@ -830,13 +926,14 @@ Show cache usage information Usage: idx cache info [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx cache clear` @@ -847,13 +944,14 @@ Clear all cached files Usage: idx cache clear [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ## `idx ownership` @@ -899,6 +997,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -924,15 +1027,16 @@ Discover the latest IDX-hosted ownership report URLs Usage: idx ownership discover [OPTIONS] Options: - --family Report family to discover: above1 (default), all, above5, or investor-type [default: above1] - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --limit Maximum number of discovered report URLs to print [default: 6] - --no-color Disable colored output - -q, --quiet Suppress non-essential output - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --family Report family to discover: above1 (default), all, above5, or investor-type [default: above1] + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --limit Maximum number of discovered report URLs to print [default: 6] + --no-color Disable colored output + -q, --quiet Suppress non-essential output + -v, --verbose... Increase verbosity (-v, -vv) + --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 Examples: idx ownership discover --limit 1 @@ -977,6 +1081,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -1028,6 +1137,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -1056,14 +1170,15 @@ Arguments: Options: - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --source [default: all] - --no-color Disable colored output - -q, --quiet Suppress non-essential output - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --source [default: all] + --no-color Disable colored output + -q, --quiet Suppress non-essential output + -v, --verbose... Increase verbosity (-v, -vv) + --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 ``` ### `idx ownership entity` @@ -1077,13 +1192,14 @@ Arguments: Entity name or part of one, case-insensitive; the shortest match is shown Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 Examples: idx ownership entity "ASTRA INTERNATIONAL" @@ -1121,6 +1237,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -1149,6 +1270,7 @@ Options: --no-color Disable colored output -q, --quiet Suppress non-essential output -v, --verbose... Increase verbosity (-v, -vv) + --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 @@ -1162,15 +1284,16 @@ Rank tickers by ownership concentration Usage: idx ownership concentration [OPTIONS] Options: - --by [default: hhi] - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --no-color Disable colored output - --top [default: 20] - -q, --quiet Suppress non-essential output - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --by [default: hhi] + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --no-color Disable colored output + --top [default: 20] + -q, --quiet Suppress non-essential output + -v, --verbose... Increase verbosity (-v, -vv) + --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 ``` ### `idx ownership flow` @@ -1184,13 +1307,14 @@ Arguments: Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx ownership changes` @@ -1223,6 +1347,11 @@ Options: -v, --verbose... Increase verbosity (-v, -vv) + --provider + Market data provider [default: general.provider, else msn] + + [possible values: yahoo, msn, stockbit] + --offline Run without network requests @@ -1248,15 +1377,16 @@ Arguments: Ticker code or entity name to start from Options: - --depth Traversal depth (default 2) [default: 2] - -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] - --format Output format: table or dot (Graphviz) [default: table] - --no-color Disable colored output - -q, --quiet Suppress non-essential output - -v, --verbose... Increase verbosity (-v, -vv) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --depth Traversal depth (default 2) [default: 2] + -o, --output Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl] + --format Output format: table or dot (Graphviz) [default: table] + --no-color Disable colored output + -q, --quiet Suppress non-essential output + -v, --verbose... Increase verbosity (-v, -vv) + --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 ``` ### `idx ownership resolve` @@ -1273,13 +1403,14 @@ Commands: help Print this message or the help of the given subcommand(s) Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` #### `idx ownership resolve unresolved` @@ -1290,14 +1421,15 @@ List unresolved or low-confidence entity aliases Usage: idx ownership resolve unresolved [OPTIONS] Options: - --limit [default: 50] - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + --limit [default: 50] + -o, --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 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 ``` #### `idx ownership resolve map` @@ -1312,13 +1444,14 @@ Arguments: The canonical entity name to map to Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` #### `idx ownership resolve merge` @@ -1333,13 +1466,14 @@ Arguments: Entity ID to merge and delete Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ### `idx ownership releases` @@ -1350,13 +1484,14 @@ List imported KSEI releases Usage: idx ownership releases [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ## `idx completions` @@ -1370,13 +1505,14 @@ Arguments: [possible values: bash, zsh, fish] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` ## `idx version` @@ -1387,11 +1523,12 @@ Show idx-cli version Usage: idx version [OPTIONS] Options: - -o, --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) - --offline Run without network requests - --no-cache Bypass local cache - -h, --help Print help + -o, --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 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 ``` diff --git a/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md new file mode 100644 index 0000000..e8bec2a --- /dev/null +++ b/web/src/content/docs/cli/stockbit.md @@ -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/`, 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/ (+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`. diff --git a/web/src/content/docs/cli/stocks.md b/web/src/content/docs/cli/stocks.md index e7ad744..dbb2936 100644 --- a/web/src/content/docs/cli/stocks.md +++ b/web/src/content/docs/cli/stocks.md @@ -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` 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 | 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 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_HISTORY_PROVIDER=auto|yahoo|msn`, or the matching -[config keys](/start/config/). +(IDX's own site with `flow --source idx`), and Stockbit serves `intraday`, +whichever provider is selected. Switch with `--provider msn|yahoo|stockbit` +(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/). diff --git a/web/src/content/docs/mcp/index.md b/web/src/content/docs/mcp/index.md index b95f58b..bbd67a4 100644 --- a/web/src/content/docs/mcp/index.md +++ b/web/src/content/docs/mcp/index.md @@ -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) 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, -analysis, comparison, broker summaries, bandarmology, foreign flow, and the KSEI +[15 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's +session by the minute, price history, screening, company data, analysis, +comparison, broker summaries, bandarmology, foreign flow, and the KSEI ownership queries. 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 failed. Ownership tools return their data directly, from the local database. @@ -44,6 +45,7 @@ body, so the agent can decide whether to retry: | Input | Limit | | --- | --- | | tickers per call (`get_quotes`, `compare_stocks`) | 20, duplicates removed | +| `get_intraday` `tail` | 400 minutes (default: the whole session) | | `screen_stocks` `limit` | 200 | | `search_investors` `limit` | 100 | | `get_ownership_changes` `limit` | 300 per page (default 200); page with `offset` | diff --git a/web/src/content/docs/mcp/tools.md b/web/src/content/docs/mcp/tools.md index 13aa94d..f105b5b 100644 --- a/web/src/content/docs/mcp/tools.md +++ b/web/src/content/docs/mcp/tools.md @@ -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". | | `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` Every IDX holding (above 1%) of one investor, matched by name. @@ -107,7 +116,7 @@ OHLCV price history for one IDX ticker. ## `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 | | --- | --- | --- | --- | diff --git a/web/src/content/docs/start/config.md b/web/src/content/docs/start/config.md index 4304b4d..d026b50 100644 --- a/web/src/content/docs/start/config.md +++ b/web/src/content/docs/start/config.md @@ -28,7 +28,7 @@ in scripts. Ask for JSON with `-o json`, `IDX_OUTPUT=json`, or ```toml [general] -provider = "msn" # msn | yahoo +provider = "msn" # msn | yahoo | stockbit history_provider = "auto" # auto | yahoo | msn exchange = "JK" output = "auto" # auto | table | json | jsonl diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index 191d008..2d07090 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -139,7 +139,7 @@ TLKM.JK 2,250 +0.45% 87,159,900

Agents

-

JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 11 tools for any MCP client.

+

JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 15 tools for any MCP client.

npx -y idx-mcp