mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 01:35:36 +00:00
feat: login-free Stockbit intraday and quote provider
Read Stockbit's public symbol page (__NEXT_DATA__) with a plain ureq request and an honest user agent: `stocks intraday` (today by the minute, any provider), `--provider stockbit` quotes with the best bid/offer only while the market is open, and MCP `get_intraday`. - UPSTREAMBLOCKED error (exit 4) for a CDN 403; never retried - requests paced 1 s apart across idx processes, 5 s snapshot cache - global --provider flag (yahoo, msn, stockbit) - roadmap: L1 ships from Stockbit; depth stays a login follow-on
This commit is contained in:
parent
018d9ba936
commit
74e5bb0536
65 changed files with 2320 additions and 381 deletions
11
README.md
11
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
|
||||
|
|
|
|||
27
ROADMAP.md
27
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 "<EXPR>"`: filter stocks by expression
|
||||
- `screen presets` / `screen run <PRESET>`: 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.
|
||||
|
|
|
|||
|
|
@ -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<ProviderKind>,
|
||||
#[arg(long, global = true, help = "Run without network requests")]
|
||||
pub offline: bool,
|
||||
#[arg(long, global = true, help = "Bypass local cache")]
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
},
|
||||
#[command(
|
||||
about = "Today's session price by the minute (Stockbit)",
|
||||
long_about = "Today's session of one IDX stock or index, by the minute, from Stockbit's \
|
||||
public page (no login): the quote, the best bid/offer while the market is open, a sparkline, \
|
||||
and the last minutes. Delayed at least 10 minutes per Stockbit's terms; prices only, no \
|
||||
per-minute volume. A stock that did not trade has no minutes, which is not an error. \
|
||||
Indices use Yahoo's tickers (^JKSE, ^JKLQ45) or Stockbit's (IHSG, LQ45).",
|
||||
after_help = "Examples:\n idx stocks intraday BBCA\n idx stocks intraday ^JKSE\n idx -o json stocks intraday BBCA | jq '.points[-1]'"
|
||||
)]
|
||||
Intraday {
|
||||
/// Single ticker symbol (e.g. BBCA).
|
||||
symbol: String,
|
||||
},
|
||||
#[command(
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,9 @@ impl IdxConfig {
|
|||
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<chrono::Utc>) -> Ve
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn pct_cell(change_pct: Option<f64>, no_color: bool) -> Cell {
|
||||
match change_pct {
|
||||
None => cell("-"),
|
||||
Some(pct) if no_color => cell(format!("{pct:+.2}%")),
|
||||
Some(pct) if pct >= 0.0 => cell(format!("{pct:+.2}%")).fg(Color::Green),
|
||||
Some(pct) => cell(format!("{pct:+.2}%")).fg(Color::Red),
|
||||
}
|
||||
}
|
||||
|
||||
/// Printed under every Stockbit table: what the data is and is not.
|
||||
const STOCKBIT_SOURCE_NOTE: &str = "Source: Stockbit's public page, delayed at least 10 \
|
||||
minutes (Stockbit's terms); bid/offer only while the market is open.";
|
||||
|
||||
fn opt_idr(value: Option<i64>) -> String {
|
||||
value.map(format_idr).unwrap_or_else(|| "-".to_string())
|
||||
}
|
||||
|
||||
fn wib_minute(time: chrono::DateTime<chrono::FixedOffset>) -> String {
|
||||
time.format("%Y-%m-%d %H:%M").to_string()
|
||||
}
|
||||
|
||||
/// Quotes from `--provider stockbit`: Stockbit has no market cap or 52-week
|
||||
/// range, but a best bid/offer and the market's state.
|
||||
pub fn print_stockbit_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec![
|
||||
"SYMBOL", "PRICE", "CHG", "CHG%", "VOLUME", "BID", "OFFER", "MARKET", "AS OF",
|
||||
]);
|
||||
for q in quotes {
|
||||
table.add_row(vec![
|
||||
cell(&q.symbol),
|
||||
cell(format_idr(q.price)),
|
||||
cell(
|
||||
q.change
|
||||
.map(|c| format!("{c:+}"))
|
||||
.unwrap_or_else(|| "-".to_string()),
|
||||
),
|
||||
pct_cell(q.change_pct, no_color),
|
||||
cell(q.volume.map(format_u64).unwrap_or_else(|| "-".to_string())),
|
||||
cell(opt_idr(q.bid)),
|
||||
cell(opt_idr(q.offer)),
|
||||
cell(q.market_status.as_deref().unwrap_or("-")),
|
||||
cell(q.as_of.map(wib_minute).unwrap_or_else(|| "-".to_string())),
|
||||
]);
|
||||
}
|
||||
println!("{table}");
|
||||
for q in quotes {
|
||||
if let Some(note) = &q.session_note {
|
||||
println!(
|
||||
"Note: {}: {}",
|
||||
terminal_safe(&q.symbol),
|
||||
terminal_safe(note)
|
||||
);
|
||||
}
|
||||
}
|
||||
for note in stale_quote_notes(quotes, chrono::Utc::now()) {
|
||||
println!("{}", terminal_safe(¬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<f64> = day.points.iter().map(|p| p.price).collect();
|
||||
if let (Some(first), Some(last)) = (day.points.first(), day.points.last()) {
|
||||
let (low, high) = prices
|
||||
.iter()
|
||||
.fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(*p), hi.max(*p)));
|
||||
println!();
|
||||
println!("{}", sparkline(&prices, SPARKLINE_WIDTH));
|
||||
println!(
|
||||
"{} to {} WIB · low {} · high {} · {} minutes",
|
||||
first.time.format("%H:%M"),
|
||||
last.time.format("%H:%M"),
|
||||
format_price(low),
|
||||
format_price(high),
|
||||
day.points.len()
|
||||
);
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL)
|
||||
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||
.set_header(vec!["TIME", "PRICE"]);
|
||||
let start = day.points.len().saturating_sub(INTRADAY_TAIL);
|
||||
for point in &day.points[start..] {
|
||||
table.add_row(vec![
|
||||
cell(point.time.format("%H:%M")),
|
||||
cell(format_price(point.price)),
|
||||
]);
|
||||
}
|
||||
println!("{table}");
|
||||
} else if q.session_note.is_none() {
|
||||
println!("No trades this session.");
|
||||
}
|
||||
println!("{STOCKBIT_SOURCE_NOTE}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Whole Rupiah for stocks, two decimals for an index level.
|
||||
fn format_price(price: f64) -> String {
|
||||
let cents = (price * 100.0).round() as i64;
|
||||
if cents % 100 == 0 {
|
||||
format_idr(cents / 100)
|
||||
} else {
|
||||
format!("{}.{:02}", format_idr(cents / 100), (cents % 100).abs())
|
||||
}
|
||||
}
|
||||
|
||||
/// One block per bucket of points (the bucket's last price), scaled from the
|
||||
/// lowest to the highest price.
|
||||
fn sparkline(prices: &[f64], width: usize) -> String {
|
||||
const BARS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
||||
let width = width.min(prices.len());
|
||||
if width == 0 {
|
||||
return String::new();
|
||||
}
|
||||
let (low, high) = prices
|
||||
.iter()
|
||||
.fold((f64::MAX, f64::MIN), |(lo, hi), p| (lo.min(*p), hi.max(*p)));
|
||||
(1..=width)
|
||||
.map(|i| {
|
||||
let price = prices[i * prices.len() / width - 1];
|
||||
if high > low {
|
||||
BARS[((price - low) / (high - low) * 7.0).round() as usize]
|
||||
} else {
|
||||
BARS[3]
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn print_history(symbol: &str, history: &[Ohlc]) -> Result<(), IdxError> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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 <KEY>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY>"
|
||||
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 <KEY> <VALUE>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY> <VALUE>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <NAME>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <NAME>"
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <ROOT>"
|
||||
opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --provider --offline --no-cache --help <ROOT>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <ALIAS> <ENTITY>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <ALIAS> <ENTITY>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||
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 <KEEP> <MERGE>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEEP> <MERGE>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||
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 <QUERY>"
|
||||
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <QUERY>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOLS>..."
|
||||
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOLS>..."
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --statement --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
fi
|
||||
case "${prev}" in
|
||||
--output)
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-o)
|
||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--provider)
|
||||
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=()
|
||||
;;
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOLS>..."
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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 <SYMBOL>"
|
||||
opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
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 <SYMBOL>"
|
||||
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
|
|
@ -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=()
|
||||
;;
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
{"code":"SYMBOLNOTFOUND","error":true,"message":"symbol not found: ZZZZ.JK"}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
exit: 3
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: symbol not found: ZZZZ.JK
|
||||
|
|
@ -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)"}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
exit: 5
|
||||
--- stdout
|
||||
--- stderr
|
||||
Error: unsupported: BBCA.JK: Stockbit has no fundamentals; command requires --provider msn or yahoo
|
||||
|
|
@ -10,11 +10,12 @@ Commands:
|
|||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
--- stderr
|
||||
|
|
|
|||
|
|
@ -28,6 +28,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
|
|||
|
||||
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> 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> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
--- stderr
|
||||
|
|
|
|||
4
crates/idx-cli/tests/golden/stockbit-quote.json.golden
Normal file
4
crates/idx-cli/tests/golden/stockbit-quote.json.golden
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
[{"symbol":"BBCA.JK","price":5175,"change":-100,"change_pct":-1.9,"volume":286928500,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":5275,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close"},{"symbol":"MABA.JK","price":50,"change":0,"change_pct":0.0,"volume":0,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":50,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close","session_note":"Stockbit lists the stock as suspended"}]
|
||||
--- stderr
|
||||
12
crates/idx-cli/tests/golden/stockbit-quote.table.golden
Normal file
12
crates/idx-cli/tests/golden/stockbit-quote.table.golden
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
┌─────────┬───────┬──────┬────────┬─────────────┬─────┬───────┬────────┬───────┐
|
||||
│ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ BID ┆ OFFER ┆ MARKET ┆ AS OF │
|
||||
╞═════════╪═══════╪══════╪════════╪═════════════╪═════╪═══════╪════════╪═══════╡
|
||||
│ BBCA.JK ┆ 5,175 ┆ -100 ┆ -1.90% ┆ 286,928,500 ┆ - ┆ - ┆ close ┆ - │
|
||||
├╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌┼╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
|
||||
│ MABA.JK ┆ 50 ┆ +0 ┆ +0.00% ┆ 0 ┆ - ┆ - ┆ close ┆ - │
|
||||
└─────────┴───────┴──────┴────────┴─────────────┴─────┴───────┴────────┴───────┘
|
||||
Note: MABA.JK: Stockbit lists the stock as suspended
|
||||
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
|
||||
--- stderr
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
4
crates/idx-cli/tests/golden/stocks-intraday.json.golden
Normal file
4
crates/idx-cli/tests/golden/stocks-intraday.json.golden
Normal file
File diff suppressed because one or more lines are too long
22
crates/idx-cli/tests/golden/stocks-intraday.table.golden
Normal file
22
crates/idx-cli/tests/golden/stocks-intraday.table.golden
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
exit: 0
|
||||
--- stdout
|
||||
BBCA.JK 5,175 -100 (-1.90%)
|
||||
prev close 5,275 · volume 286,928,500 · market close · last trade 2026-10-22 16:14 WIB
|
||||
|
||||
▄▅▇█▅▇▇▇▅▇▇▇▇▇▇██▇▅▇▅▅███▇▅▇▅▇▇▇▅▇▅▅▄▅▄▅▄▄▅▅▅▅▅▇▅▄▄▄▄▄▄▂▄▄▄▄
|
||||
08:58 to 16:14 WIB · low 5,125 · high 5,250 · 335 minutes
|
||||
┌───────┬───────┐
|
||||
│ TIME ┆ PRICE │
|
||||
╞═══════╪═══════╡
|
||||
│ 16:10 ┆ 5,175 │
|
||||
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
|
||||
│ 16:11 ┆ 5,175 │
|
||||
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
|
||||
│ 16:12 ┆ 5,175 │
|
||||
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
|
||||
│ 16:13 ┆ 5,175 │
|
||||
├╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌┤
|
||||
│ 16:14 ┆ 5,175 │
|
||||
└───────┴───────┘
|
||||
Source: Stockbit's public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open.
|
||||
--- stderr
|
||||
|
|
@ -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_<code>.html`;
|
||||
/// a code without a fixture is Stockbit's 404. `IDX_MOCK_STOCKBIT_STATUS`
|
||||
/// (e.g. `403`) answers that HTTP status instead, and `IDX_MOCK_ERROR` fails
|
||||
/// as for the other providers.
|
||||
pub struct MockStockbitProvider;
|
||||
|
||||
impl MockStockbitProvider {
|
||||
fn snapshot(&self, symbol: &str) -> Result<Intraday, IdxError> {
|
||||
if std::env::var("IDX_MOCK_ERROR").is_ok() {
|
||||
return Err(IdxError::ProviderUnavailable);
|
||||
}
|
||||
if let Ok(status) = std::env::var("IDX_MOCK_STOCKBIT_STATUS") {
|
||||
return Err(stockbit::status_error(
|
||||
status.parse().unwrap_or(500),
|
||||
symbol,
|
||||
));
|
||||
}
|
||||
let code = stockbit::stockbit_code(symbol)?;
|
||||
let path = format!(
|
||||
"tests/fixtures/stockbit_symbol_{}.html",
|
||||
code.to_ascii_lowercase()
|
||||
);
|
||||
match std::fs::read_to_string(path) {
|
||||
Ok(html) => stockbit::parse_symbol_page(symbol, &html),
|
||||
Err(_) => Err(stockbit::status_error(404, symbol)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QuoteProvider for MockStockbitProvider {
|
||||
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
|
||||
let mut quote = self.snapshot(symbol)?.quote;
|
||||
// Quotes are judged against the clock (behind the session, a week
|
||||
// old); a fixed trade time would make their output age.
|
||||
quote.as_of = None;
|
||||
Ok(quote)
|
||||
}
|
||||
}
|
||||
|
||||
impl FundamentalsProvider for MockStockbitProvider {
|
||||
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
|
||||
Err(stockbit::fundamentals_unsupported(symbol))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntradayProvider for MockStockbitProvider {
|
||||
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError> {
|
||||
self.snapshot(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
/// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from>
|
||||
/// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an
|
||||
/// unknown code.
|
||||
|
|
|
|||
|
|
@ -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<Vec<crate::flow::types::StockDay>, IdxError>;
|
||||
}
|
||||
|
||||
/// Today's session of one IDX symbol: quote and price per minute. Served by
|
||||
/// Stockbit's public page whichever market provider is selected.
|
||||
pub trait IntradayProvider {
|
||||
/// `symbol` is resolved, e.g. `BBCA.JK`.
|
||||
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError>;
|
||||
}
|
||||
|
||||
pub trait FundamentalsProvider {
|
||||
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
|
||||
}
|
||||
|
|
@ -221,7 +229,10 @@ fn mock_enabled() -> bool {
|
|||
|
||||
#[cfg(feature = "testing")]
|
||||
fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
|
||||
Some(Box::new(mock::MockProvider::from_fixtures(provider)))
|
||||
match provider {
|
||||
ProviderKind::Stockbit => Some(Box::new(mock::MockStockbitProvider)),
|
||||
_ => Some(Box::new(mock::MockProvider::from_fixtures(provider))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "testing"))]
|
||||
|
|
@ -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<dyn TradingSummaryProvider> {
|
|||
Box::new(idx::IdxSiteProvider)
|
||||
}
|
||||
|
||||
pub fn intraday_provider() -> Box<dyn IntradayProvider> {
|
||||
#[cfg(feature = "testing")]
|
||||
if mock::enabled() {
|
||||
return Box::new(mock::MockStockbitProvider);
|
||||
}
|
||||
Box::new(stockbit::StockbitProvider::new())
|
||||
}
|
||||
|
||||
/// Today in WIB, the date IDX trades on.
|
||||
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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,10 @@ pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result<Quote, Id
|
|||
avg_volume: round_u64(quote.average_volume),
|
||||
as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()),
|
||||
behind_session: false,
|
||||
bid: None,
|
||||
offer: None,
|
||||
market_status: None,
|
||||
session_note: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -720,6 +724,10 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
|
|||
avg_volume: round_u64(q.average_volume),
|
||||
as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()),
|
||||
behind_session: false,
|
||||
bid: None,
|
||||
offer: None,
|
||||
market_status: None,
|
||||
session_note: None,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
|
|||
193
crates/idx-core/src/api/stockbit/mod.rs
Normal file
193
crates/idx-core/src/api/stockbit/mod.rs
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
//! Stockbit's public symbol page: a quote, the best bid/offer and today's
|
||||
//! price per minute for one IDX symbol, without a login or key.
|
||||
//!
|
||||
//! `GET https://stockbit.com/symbol/{CODE}` server-renders everything into
|
||||
//! the page's `__NEXT_DATA__` JSON (see `parse`). Stockbit's own JSON API
|
||||
//! (`exodus.stockbit.com`) needs a logged-in bearer token and is not used.
|
||||
//!
|
||||
//! Access rules this module keeps:
|
||||
//! - An honest user agent. CloudFront in front of the page answers a static
|
||||
//! HTTP 403 to `curl/`, `axios/`, `Go-http-client/` and empty agents; it
|
||||
//! does not fingerprint TLS, so no browser impersonation is involved.
|
||||
//! - One request per symbol, at least `MIN_INTERVAL` apart across every idx
|
||||
//! process of the user (see `pace`), no retries, and a 403 surfaces as
|
||||
//! `UPSTREAMBLOCKED` at once.
|
||||
//! - Stockbit's terms describe its market data as delayed at least 10
|
||||
//! minutes and for personal use; frontends say so.
|
||||
|
||||
mod parse;
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub use parse::{parse_symbol_page, status_error, stockbit_code};
|
||||
|
||||
use super::types::{Fundamentals, Intraday, Quote};
|
||||
use super::{FundamentalsProvider, IntradayProvider, QuoteProvider};
|
||||
use crate::error::IdxError;
|
||||
|
||||
pub const SOURCE: &str = "stockbit";
|
||||
/// Identifies idx-cli and where to reach its maintainers.
|
||||
pub const USER_AGENT: &str = concat!(
|
||||
"idx-cli/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (+",
|
||||
env!("CARGO_PKG_REPOSITORY"),
|
||||
")"
|
||||
);
|
||||
/// Longest a snapshot is served from cache: enough to coalesce a burst (an
|
||||
/// agent asking for a quote and the intraday series), short enough that a
|
||||
/// session view stays current.
|
||||
pub(crate) const CACHE_TTL_SECS: u64 = 5;
|
||||
/// Least time between two requests to stockbit.com from one process.
|
||||
const MIN_INTERVAL: Duration = Duration::from_secs(1);
|
||||
const BASE_URL: &str = "https://stockbit.com/symbol/";
|
||||
|
||||
pub struct StockbitProvider {
|
||||
agent: ureq::Agent,
|
||||
}
|
||||
|
||||
impl StockbitProvider {
|
||||
pub fn new() -> Self {
|
||||
let agent = ureq::Agent::config_builder()
|
||||
.timeout_connect(Some(Duration::from_secs(5)))
|
||||
.timeout_recv_body(Some(Duration::from_secs(10)))
|
||||
// A server that stalls before sending headers must not hold a
|
||||
// call (and an MCP call slot) forever.
|
||||
.timeout_global(Some(Duration::from_secs(20)))
|
||||
.build()
|
||||
.into();
|
||||
Self { agent }
|
||||
}
|
||||
|
||||
/// The quote and today's series for a resolved symbol (`BBCA.JK`).
|
||||
pub fn snapshot(&self, resolved: &str) -> Result<Intraday, IdxError> {
|
||||
let code = stockbit_code(resolved)?;
|
||||
let html = self.page(&code, resolved)?;
|
||||
parse_symbol_page(resolved, &html)
|
||||
}
|
||||
|
||||
fn page(&self, code: &str, resolved: &str) -> Result<String, IdxError> {
|
||||
pace();
|
||||
let response = self
|
||||
.agent
|
||||
.get(format!("{BASE_URL}{code}"))
|
||||
.header("User-Agent", USER_AGENT)
|
||||
.header("Accept", "text/html")
|
||||
.call();
|
||||
match response {
|
||||
Ok(ok) => ok
|
||||
.into_body()
|
||||
.read_to_string()
|
||||
.map_err(|e| IdxError::Http(format!("stockbit: {e}"))),
|
||||
Err(ureq::Error::StatusCode(status)) => Err(status_error(status, resolved)),
|
||||
Err(err) => Err(IdxError::Http(format!("stockbit: {err}"))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for StockbitProvider {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until `MIN_INTERVAL` has passed since the previous request. Threads
|
||||
/// of one process queue on a mutex; separate processes (a shell loop of
|
||||
/// `idx` calls, a CLI beside idx-mcp) share the time of the last request in
|
||||
/// `<cache>/stockbit.pace` under an exclusive file lock. When that file is
|
||||
/// unusable, pacing is per process.
|
||||
fn pace() {
|
||||
static LAST: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
let mut last = LAST.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
if let Some(prev) = *last {
|
||||
let wait = MIN_INTERVAL.saturating_sub(prev.elapsed());
|
||||
if !wait.is_zero() {
|
||||
std::thread::sleep(wait);
|
||||
}
|
||||
}
|
||||
// Best effort: the in-process pacing above already holds.
|
||||
let _ = pace_across_processes();
|
||||
*last = Some(Instant::now());
|
||||
}
|
||||
|
||||
fn pace_across_processes() -> std::io::Result<()> {
|
||||
let dir = crate::cache::cache_dir().map_err(|e| std::io::Error::other(e.to_string()))?;
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(dir.join("stockbit.pace"))?;
|
||||
// Released when `file` is dropped, after the new time is written.
|
||||
file.lock()?;
|
||||
let mut raw = String::new();
|
||||
file.read_to_string(&mut raw)?;
|
||||
if let Ok(prev_ms) = raw.trim().parse::<u128>() {
|
||||
// A clock that went backwards waits at most one interval.
|
||||
let elapsed = now_ms().saturating_sub(prev_ms);
|
||||
let wait = MIN_INTERVAL.as_millis().saturating_sub(elapsed);
|
||||
if wait > 0 {
|
||||
std::thread::sleep(Duration::from_millis(wait as u64));
|
||||
}
|
||||
}
|
||||
stamp(&mut file)
|
||||
}
|
||||
|
||||
fn stamp(file: &mut File) -> std::io::Result<()> {
|
||||
file.set_len(0)?;
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
write!(file, "{}", now_ms())
|
||||
}
|
||||
|
||||
fn now_ms() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_millis())
|
||||
}
|
||||
|
||||
impl QuoteProvider for StockbitProvider {
|
||||
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
|
||||
self.snapshot(symbol).map(|day| day.quote)
|
||||
}
|
||||
}
|
||||
|
||||
impl FundamentalsProvider for StockbitProvider {
|
||||
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
|
||||
Err(fundamentals_unsupported(symbol))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntradayProvider for StockbitProvider {
|
||||
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError> {
|
||||
self.snapshot(symbol)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fundamentals_unsupported(symbol: &str) -> IdxError {
|
||||
IdxError::Unsupported(format!(
|
||||
"{symbol}: Stockbit has no fundamentals; command requires --provider msn or yahoo"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::USER_AGENT;
|
||||
|
||||
/// CloudFront answers HTTP 403 to agents containing these (probed
|
||||
/// 2026-10-22); the honest agent must never match one.
|
||||
#[test]
|
||||
fn user_agent_is_honest_and_not_on_the_cdn_blocklist() {
|
||||
assert!(USER_AGENT.starts_with("idx-cli/"));
|
||||
assert!(USER_AGENT.contains("github.com/0xrsydn/idx-cli"));
|
||||
for blocked in ["curl/", "axios/", "Go-http-client/"] {
|
||||
assert!(
|
||||
!USER_AGENT.contains(blocked),
|
||||
"{USER_AGENT} contains {blocked}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
440
crates/idx-core/src/api/stockbit/parse.rs
Normal file
440
crates/idx-core/src/api/stockbit/parse.rs
Normal file
|
|
@ -0,0 +1,440 @@
|
|||
//! Pure parsing of a Stockbit symbol page: the `__NEXT_DATA__` JSON that
|
||||
//! Next.js renders into `https://stockbit.com/symbol/{CODE}`. No I/O.
|
||||
//!
|
||||
//! Shapes (recorded 2026-10-22 after the close, see
|
||||
//! `tests/fixtures/stockbit_symbol_*.html`):
|
||||
//! - `props.pageProps.company`: the quote. Numbers are mostly strings
|
||||
//! (`price: "5175"`, an index `"5387.90478515625"`, `change: "+59.54"`),
|
||||
//! `percentage` is a JSON number, `orderbook.{bid,offer}.price` is
|
||||
//! `"5175.000000"` or `"N/A"`, `market_hour.status` is e.g. `close`, and
|
||||
//! `status` is `STATUS_ACTIVE`, `STATUS_SUSPENDED` or `STATUS_DELISTED`.
|
||||
//! `date`/`time` (`"22 Oct 2026"`, `"Thu 16:14"`) are the last trade only
|
||||
//! when the stock traded: a suspended one carries today's date.
|
||||
//! - `props.pageProps.chart.prices[]`: one row per minute,
|
||||
//! `formatted_date: "2026-10-22 16:14:00"` (WIB, no offset) and
|
||||
//! `value: "5175"`. The first row is a synthetic anchor at the previous
|
||||
//! close with an empty `formatted_date`. Per-minute OHLC and volume are
|
||||
//! empty strings, so only the price is kept.
|
||||
//! - An unknown code renders the 404 page: `page: "/404"`, empty pageProps.
|
||||
|
||||
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::api::idx_offset;
|
||||
use crate::api::types::{Intraday, IntradayPoint, Quote};
|
||||
use crate::error::IdxError;
|
||||
|
||||
/// Stockbit's market status while continuous trading runs. Bid and offer are
|
||||
/// reported only then: outside it they are the closing book.
|
||||
const MARKET_OPEN: &str = "open";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct NextData {
|
||||
page: String,
|
||||
props: Props,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Props {
|
||||
#[serde(rename = "pageProps", default)]
|
||||
page_props: PageProps,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct PageProps {
|
||||
company: Option<RawCompany>,
|
||||
chart: Option<RawChart>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawCompany {
|
||||
price: Value,
|
||||
#[serde(default)]
|
||||
previous: Value,
|
||||
#[serde(default)]
|
||||
change: Value,
|
||||
#[serde(default)]
|
||||
percentage: Value,
|
||||
#[serde(default)]
|
||||
volume: Value,
|
||||
date: Option<String>,
|
||||
time: Option<String>,
|
||||
status: Option<String>,
|
||||
orderbook: Option<RawOrderbook>,
|
||||
market_hour: Option<RawMarketHour>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawOrderbook {
|
||||
bid: Option<RawLevel>,
|
||||
offer: Option<RawLevel>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawLevel {
|
||||
#[serde(default)]
|
||||
price: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawMarketHour {
|
||||
status: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct RawChart {
|
||||
#[serde(default)]
|
||||
prices: Vec<RawPoint>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RawPoint {
|
||||
#[serde(default)]
|
||||
formatted_date: String,
|
||||
#[serde(default)]
|
||||
value: Value,
|
||||
}
|
||||
|
||||
/// Stockbit's code for a resolved symbol: `BBCA.JK` -> `BBCA`, and Yahoo's
|
||||
/// index tickers to Stockbit's (`^JKSE` -> `IHSG`, `^JKLQ45` -> `LQ45`).
|
||||
pub fn stockbit_code(resolved: &str) -> Result<String, IdxError> {
|
||||
let unsupported = || {
|
||||
IdxError::Unsupported(format!(
|
||||
"{resolved}: Stockbit covers IDX (.JK) symbols only"
|
||||
))
|
||||
};
|
||||
let code = resolved.strip_suffix(".JK").ok_or_else(unsupported)?;
|
||||
let code = match code.strip_prefix('^') {
|
||||
Some("JKSE") => "IHSG",
|
||||
Some("JKLQ45") => "LQ45",
|
||||
Some(_) => return Err(unsupported()),
|
||||
None => code,
|
||||
};
|
||||
if code.is_empty() || !code.chars().all(|c| c.is_ascii_alphanumeric()) {
|
||||
return Err(unsupported());
|
||||
}
|
||||
Ok(code.to_string())
|
||||
}
|
||||
|
||||
/// The error for a non-success HTTP status from stockbit.com. A 403 is
|
||||
/// CloudFront's static block page (it refuses some user agents outright),
|
||||
/// not a challenge, so nothing is retried.
|
||||
pub fn status_error(status: u16, resolved: &str) -> IdxError {
|
||||
match status {
|
||||
403 => IdxError::UpstreamBlocked(
|
||||
"stockbit.com refused this client (HTTP 403 from its CDN); idx does not retry a \
|
||||
block. Try again later or from another network; `--provider msn` serves quotes \
|
||||
without Stockbit"
|
||||
.to_string(),
|
||||
),
|
||||
404 => IdxError::SymbolNotFound(resolved.to_string()),
|
||||
429 => IdxError::RateLimited,
|
||||
_ => IdxError::Http(format!("stockbit: HTTP {status}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// The quote and today's per-minute prices from a symbol page, stamped with
|
||||
/// `resolved` (e.g. `BBCA.JK`).
|
||||
pub fn parse_symbol_page(resolved: &str, html: &str) -> Result<Intraday, IdxError> {
|
||||
let data: NextData = serde_json::from_str(next_data(html)?)
|
||||
.map_err(|e| layout_error(format!("__NEXT_DATA__ is not the expected JSON: {e}")))?;
|
||||
if data.page == "/404" {
|
||||
return Err(IdxError::SymbolNotFound(resolved.to_string()));
|
||||
}
|
||||
let company = data
|
||||
.props
|
||||
.page_props
|
||||
.company
|
||||
.ok_or_else(|| layout_error("no company in the page".to_string()))?;
|
||||
let points = parse_points(&data.props.page_props.chart.unwrap_or_default())?;
|
||||
let quote = parse_quote(resolved, &company)?;
|
||||
Ok(Intraday { quote, points })
|
||||
}
|
||||
|
||||
fn layout_error(detail: String) -> IdxError {
|
||||
IdxError::ParseError(format!(
|
||||
"stockbit symbol page: {detail} (the page layout changed?)"
|
||||
))
|
||||
}
|
||||
|
||||
/// The JSON inside `<script id="__NEXT_DATA__" ...>...</script>`.
|
||||
fn next_data(html: &str) -> Result<&str, IdxError> {
|
||||
let missing = || layout_error("no __NEXT_DATA__ script".to_string());
|
||||
let tag = html.find("id=\"__NEXT_DATA__\"").ok_or_else(missing)?;
|
||||
let start = tag + html[tag..].find('>').ok_or_else(missing)? + 1;
|
||||
let len = html[start..].find("</script>").ok_or_else(missing)?;
|
||||
Ok(&html[start..start + len])
|
||||
}
|
||||
|
||||
fn parse_quote(resolved: &str, company: &RawCompany) -> Result<Quote, IdxError> {
|
||||
let price = number(&company.price)
|
||||
.ok_or_else(|| layout_error(format!("price {} is not a number", company.price)))?;
|
||||
let price = price.round() as i64;
|
||||
let prev_close = number(&company.previous).map(|p| p.round() as i64);
|
||||
let change = number(&company.change)
|
||||
.map(|c| c.round() as i64)
|
||||
.or_else(|| prev_close.map(|prev| price - prev));
|
||||
let volume = count(&company.volume);
|
||||
let listing = company.status.as_deref().unwrap_or_default();
|
||||
let market_status = company
|
||||
.market_hour
|
||||
.as_ref()
|
||||
.and_then(|m| m.status.clone())
|
||||
.filter(|s| !s.is_empty());
|
||||
let open = market_status.as_deref() == Some(MARKET_OPEN);
|
||||
let level = |side: Option<&RawLevel>| {
|
||||
side.and_then(|l| number(&l.price))
|
||||
.filter(|p| *p > 0.0)
|
||||
.map(|p| p.round() as i64)
|
||||
};
|
||||
let book = company.orderbook.as_ref();
|
||||
let (bid, offer) = if open {
|
||||
(
|
||||
level(book.and_then(|b| b.bid.as_ref())),
|
||||
level(book.and_then(|b| b.offer.as_ref())),
|
||||
)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let traded = volume.is_some_and(|v| v > 0);
|
||||
let as_of = if listing == "STATUS_ACTIVE" && traded {
|
||||
last_trade(company.date.as_deref(), company.time.as_deref())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// Facts about this symbol only; the market's state is `market_status`.
|
||||
// An untraded stock can still have a flat chart at the previous close.
|
||||
let session_note = match listing {
|
||||
"STATUS_SUSPENDED" => Some("Stockbit lists the stock as suspended".to_string()),
|
||||
"STATUS_DELISTED" => Some("Stockbit lists the stock as delisted".to_string()),
|
||||
_ if volume == Some(0) => Some("no trades this session".to_string()),
|
||||
_ => None,
|
||||
};
|
||||
Ok(Quote {
|
||||
symbol: resolved.to_string(),
|
||||
price,
|
||||
change,
|
||||
change_pct: number(&company.percentage),
|
||||
volume,
|
||||
market_cap: None,
|
||||
week52_high: None,
|
||||
week52_low: None,
|
||||
week52_position: None,
|
||||
range_signal: None,
|
||||
prev_close,
|
||||
avg_volume: None,
|
||||
as_of,
|
||||
behind_session: false,
|
||||
bid,
|
||||
offer,
|
||||
market_status,
|
||||
session_note,
|
||||
})
|
||||
}
|
||||
|
||||
/// Today's prices, oldest first, one per minute. The anchor row (no
|
||||
/// timestamp) is dropped; any other row that does not parse means the
|
||||
/// layout changed.
|
||||
fn parse_points(chart: &RawChart) -> Result<Vec<IntradayPoint>, IdxError> {
|
||||
let mut points = Vec::with_capacity(chart.prices.len());
|
||||
for row in &chart.prices {
|
||||
if row.formatted_date.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let bad = || layout_error(format!("chart row {:?} {}", row.formatted_date, row.value));
|
||||
let local = NaiveDateTime::parse_from_str(&row.formatted_date, "%Y-%m-%d %H:%M:%S")
|
||||
.map_err(|_| bad())?;
|
||||
let time = wib(local).ok_or_else(bad)?;
|
||||
let price = number(&row.value).ok_or_else(bad)?;
|
||||
points.push(IntradayPoint { time, price });
|
||||
}
|
||||
points.sort_by_key(|p| p.time);
|
||||
// A repeated minute keeps its last row.
|
||||
points.reverse();
|
||||
points.dedup_by_key(|p| p.time);
|
||||
points.reverse();
|
||||
Ok(points)
|
||||
}
|
||||
|
||||
/// `"22 Oct 2026"` and `"Thu 16:14"` as a WIB time.
|
||||
fn last_trade(date: Option<&str>, time: Option<&str>) -> Option<DateTime<FixedOffset>> {
|
||||
let date = NaiveDate::parse_from_str(date?.trim(), "%d %b %Y").ok()?;
|
||||
let clock = time?.trim().rsplit(' ').next()?;
|
||||
let clock = NaiveTime::parse_from_str(clock, "%H:%M").ok()?;
|
||||
wib(date.and_time(clock))
|
||||
}
|
||||
|
||||
fn wib(local: NaiveDateTime) -> Option<DateTime<FixedOffset>> {
|
||||
idx_offset().from_local_datetime(&local).single()
|
||||
}
|
||||
|
||||
/// A JSON number or numeric string (`"+59.54"`, `"5175.000000"`); `"N/A"`,
|
||||
/// `"NA"` and empty strings are absent.
|
||||
fn number(value: &Value) -> Option<f64> {
|
||||
let n = match value {
|
||||
Value::Number(n) => n.as_f64()?,
|
||||
Value::String(s) => s.trim().parse::<f64>().ok()?,
|
||||
_ => return None,
|
||||
};
|
||||
n.is_finite().then_some(n)
|
||||
}
|
||||
|
||||
fn count(value: &Value) -> Option<u64> {
|
||||
match value {
|
||||
Value::Number(n) => n.as_u64(),
|
||||
Value::String(s) => s.trim().parse::<u64>().ok(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fixture(name: &str) -> String {
|
||||
std::fs::read_to_string(format!(
|
||||
"{}/tests/fixtures/{name}",
|
||||
env!("CARGO_MANIFEST_DIR")
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The recorded BBCA page with Stockbit's market status set to `status`.
|
||||
fn bbca_with_status(status: &str) -> String {
|
||||
let page = fixture("stockbit_symbol_bbca.html");
|
||||
assert!(page.contains(r#""status":"close""#));
|
||||
page.replace(r#""status":"close""#, &format!(r#""status":"{status}""#))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closed_session_keeps_the_series_and_hides_the_closing_book() {
|
||||
let day = parse_symbol_page("BBCA.JK", &fixture("stockbit_symbol_bbca.html")).unwrap();
|
||||
let q = &day.quote;
|
||||
assert_eq!(
|
||||
(q.price, q.prev_close, q.change),
|
||||
(5175, Some(5275), Some(-100))
|
||||
);
|
||||
assert_eq!(q.change_pct, Some(-1.9));
|
||||
assert_eq!(q.volume, Some(286_928_500));
|
||||
assert_eq!(q.market_status.as_deref(), Some("close"));
|
||||
assert_eq!((q.bid, q.offer), (None, None));
|
||||
assert_eq!(q.session_note, None);
|
||||
assert_eq!(q.as_of.unwrap().to_rfc3339(), "2026-10-22T16:14:00+07:00");
|
||||
|
||||
// 336 rows less the anchor at the previous close.
|
||||
assert_eq!(day.points.len(), 335);
|
||||
let first = &day.points[0];
|
||||
assert_eq!(first.time.to_rfc3339(), "2026-10-22T08:58:00+07:00");
|
||||
assert_eq!(first.price, 5125.0);
|
||||
let last = day.points.last().unwrap();
|
||||
assert_eq!(last.time.to_rfc3339(), "2026-10-22T16:14:00+07:00");
|
||||
assert_eq!(last.price, 5175.0);
|
||||
assert!(day.points.windows(2).all(|w| w[0].time < w[1].time));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_market_reports_best_bid_and_offer() {
|
||||
let day = parse_symbol_page("BBCA.JK", &bbca_with_status(MARKET_OPEN)).unwrap();
|
||||
assert_eq!((day.quote.bid, day.quote.offer), (Some(5175), Some(5200)));
|
||||
assert_eq!(day.quote.market_status.as_deref(), Some(MARKET_OPEN));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn any_other_market_status_hides_bid_and_offer() {
|
||||
for status in ["pre_open", "break", "OPEN", ""] {
|
||||
let day = parse_symbol_page("BBCA.JK", &bbca_with_status(status)).unwrap();
|
||||
assert_eq!((day.quote.bid, day.quote.offer), (None, None), "{status:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn index_keeps_decimals_in_the_series_and_rounds_the_quote() {
|
||||
let day = parse_symbol_page("^JKSE.JK", &fixture("stockbit_symbol_ihsg.html")).unwrap();
|
||||
let q = &day.quote;
|
||||
assert_eq!(
|
||||
(q.price, q.prev_close, q.change),
|
||||
(5388, Some(5328), Some(60))
|
||||
);
|
||||
// An index has no book even while open.
|
||||
assert_eq!((q.bid, q.offer), (None, None));
|
||||
assert_eq!(day.points.last().unwrap().price, 5387.90);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suspended_and_delisted_stocks_are_empty_sessions_not_errors() {
|
||||
for (file, note) in [
|
||||
(
|
||||
"stockbit_symbol_maba.html",
|
||||
"Stockbit lists the stock as suspended",
|
||||
),
|
||||
(
|
||||
"stockbit_symbol_myrx.html",
|
||||
"Stockbit lists the stock as delisted",
|
||||
),
|
||||
] {
|
||||
let day = parse_symbol_page("X.JK", &fixture(file)).unwrap();
|
||||
assert!(day.points.is_empty(), "{file}");
|
||||
assert_eq!(day.quote.session_note.as_deref(), Some(note), "{file}");
|
||||
// The page's date/time is not a trade time when nothing traded.
|
||||
assert_eq!(day.quote.as_of, None, "{file}");
|
||||
}
|
||||
}
|
||||
|
||||
/// HOME and BIMA on 2026-10-22: active, a flat chart at the previous
|
||||
/// close, volume 0. Without a note they read as a traded flat session.
|
||||
#[test]
|
||||
fn an_untraded_active_stock_says_so_even_with_a_flat_chart() {
|
||||
let page = fixture("stockbit_symbol_bbca.html");
|
||||
assert!(page.contains(r#""volume":"286928500""#));
|
||||
let page = page.replace(r#""volume":"286928500""#, r#""volume":"0""#);
|
||||
let day = parse_symbol_page("BBCA.JK", &page).unwrap();
|
||||
assert!(!day.points.is_empty());
|
||||
assert_eq!(
|
||||
day.quote.session_note.as_deref(),
|
||||
Some("no trades this session")
|
||||
);
|
||||
assert_eq!(day.quote.as_of, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_code_is_symbol_not_found() {
|
||||
let err = parse_symbol_page("ZZZZ.JK", &fixture("stockbit_symbol_zzzz_404.html"));
|
||||
assert!(matches!(err, Err(IdxError::SymbolNotFound(s)) if s == "ZZZZ.JK"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_page_without_next_data_is_a_layout_change() {
|
||||
let err = parse_symbol_page("BBCA.JK", &fixture("stockbit_blocked_403.html"));
|
||||
assert!(matches!(err, Err(IdxError::ParseError(_))), "{err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_403_is_upstream_blocked_and_404_is_not_found() {
|
||||
assert!(matches!(
|
||||
status_error(403, "BBCA.JK"),
|
||||
IdxError::UpstreamBlocked(_)
|
||||
));
|
||||
assert!(
|
||||
matches!(status_error(404, "BBCA.JK"), IdxError::SymbolNotFound(s) if s == "BBCA.JK")
|
||||
);
|
||||
assert!(matches!(
|
||||
status_error(429, "BBCA.JK"),
|
||||
IdxError::RateLimited
|
||||
));
|
||||
assert!(matches!(status_error(502, "BBCA.JK"), IdxError::Http(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codes_map_idx_symbols_and_index_tickers() {
|
||||
assert_eq!(stockbit_code("BBCA.JK").unwrap(), "BBCA");
|
||||
assert_eq!(stockbit_code("^JKSE.JK").unwrap(), "IHSG");
|
||||
assert_eq!(stockbit_code("^JKLQ45.JK").unwrap(), "LQ45");
|
||||
for other in ["AAPL.US", "^GSPC.JK", "BRK-B.JK", ".JK"] {
|
||||
assert!(
|
||||
matches!(stockbit_code(other), Err(IdxError::Unsupported(_))),
|
||||
"{other}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<i64>,
|
||||
/// Best offer in IDR (whole Rupiah), gated like `bid`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub offer: Option<i64>,
|
||||
/// The provider's own session state, verbatim (Stockbit
|
||||
/// `market_hour.status`, e.g. `close`). Absent for providers without one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub market_status: Option<String>,
|
||||
/// What the provider says about this symbol's session: suspended,
|
||||
/// delisted, or no trades. Never inferred.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub session_note: Option<String>,
|
||||
}
|
||||
|
||||
/// One price of today's session, minute by minute (Stockbit's chart).
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct IntradayPoint {
|
||||
/// The minute, in WIB.
|
||||
pub time: DateTime<FixedOffset>,
|
||||
/// Last price at that minute in IDR; indices carry two decimals.
|
||||
pub price: f64,
|
||||
}
|
||||
|
||||
/// The latest session of one IDX symbol: its quote and the price per minute.
|
||||
/// Stockbit's public page carries no per-minute OHLC or volume.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Intraday {
|
||||
pub quote: Quote,
|
||||
/// Oldest first; empty when the symbol did not trade (suspended,
|
||||
/// delisted, or no session yet), never an error.
|
||||
pub points: Vec<IntradayPoint>,
|
||||
}
|
||||
|
||||
/// OHLC candle data normalized from Yahoo Finance chart indicators.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,10 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
|
|||
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
|
||||
.map(|t| t.with_timezone(&crate::api::idx_offset())),
|
||||
behind_session: false,
|
||||
bid: None,
|
||||
offer: None,
|
||||
market_status: None,
|
||||
session_note: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ impl CachePolicy {
|
|||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Fetched<T> {
|
||||
pub data: T,
|
||||
/// Provider that produced the data (`msn` or `yahoo`).
|
||||
/// Provider that produced the data (e.g. `msn`, `yahoo`, `ipot`, `stockbit`).
|
||||
pub source: &'static str,
|
||||
pub fetched_at: DateTime<Utc>,
|
||||
/// True when an expired cache entry was served (offline, or after a
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Vec<Fetched<Quote>>, 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<Fetched<Intraday>, IdxError> {
|
||||
let idx = self.idx;
|
||||
let resolved = idx.resolve(symbol)?;
|
||||
let provider = intraday_provider();
|
||||
cached_fetch(
|
||||
&idx.cache,
|
||||
CacheSpec {
|
||||
bucket: "stockbit-intraday",
|
||||
key: &resolved,
|
||||
ttl_secs: idx.config.quote_ttl.min(stockbit::CACHE_TTL_SECS),
|
||||
subject: &resolved,
|
||||
source: stockbit::SOURCE,
|
||||
},
|
||||
policy,
|
||||
|| provider.intraday(&resolved),
|
||||
Ok,
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolves the history source for an optional `--history-provider`
|
||||
/// override, without fetching.
|
||||
pub fn history_source(&self, mode: Option<HistoryProviderKind>) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
20
crates/idx-core/tests/fixtures/stockbit_blocked_403.html
vendored
Normal file
20
crates/idx-core/tests/fixtures/stockbit_blocked_403.html
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
|
||||
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
|
||||
<TITLE>ERROR: The request could not be satisfied</TITLE>
|
||||
</HEAD><BODY>
|
||||
<H1>403 ERROR</H1>
|
||||
<H2>The request could not be satisfied.</H2>
|
||||
<HR noshade size="1px">
|
||||
Request blocked.
|
||||
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
|
||||
<BR clear="all">
|
||||
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
|
||||
<BR clear="all">
|
||||
<HR noshade size="1px">
|
||||
<PRE>
|
||||
Generated by cloudfront (CloudFront)
|
||||
Request ID: VajZoZu7lzhasdjnxU37dHaztROLXHQ6DMpJzTPZoDhkd4I2dVc2mw==
|
||||
</PRE>
|
||||
<ADDRESS>
|
||||
</ADDRESS>
|
||||
</BODY></HTML>
|
||||
1
crates/idx-core/tests/fixtures/stockbit_symbol_bbca.html
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_symbol_bbca.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_symbol_ihsg.html
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_symbol_ihsg.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
crates/idx-core/tests/fixtures/stockbit_symbol_maba.html
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_symbol_maba.html
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Stockbit</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"symbol":"MABA","company":{"symbol":"MABA","name":"Marga Abhinaya Abadi Tbk","price":"50","previous":"50","change":"0.00","percentage":0,"volume":"0","date":"22 Oct 2026","time":"Thu 04:07","updated":"2022-10-17T01:22:50+07:00","status":"STATUS_SUSPENDED","tradeable":0,"type_company":"Saham","exchange":"IDX","orderbook":{"bid":{"price":"N/A","volume":"N/A"},"offer":{"price":"N/A","volume":"N/A"}},"market_hour":{"status":"close","time_left":52792,"formatted_time_left":"14 jam 39 menit 52 detik","suspend_info":""}},"chart":{"cagr":"","change":0,"drawdown":"","markingpoint":"","percentage":"","prices":[],"timeframe":"today","xaxisopt":"","previous":50,"line_weight":0,"previous_timeframe_price":null,"chart_type":"PRICE_CHART_TYPE_UNSPECIFIED","interval_in_minutes":0,"allowed_chart_type":[],"max_candles":0}},"__N_SSP":true},"page":"/symbol/[symbol]","query":{"symbol":"MABA"},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>
|
||||
1
crates/idx-core/tests/fixtures/stockbit_symbol_myrx.html
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_symbol_myrx.html
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Stockbit</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"symbol":"MYRX","company":{"symbol":"MYRX","name":"Hanson International Tbk.","price":"50","previous":"50","change":"0.00","percentage":0,"volume":"0","date":"21 Jul 2025","time":"Mon 07:56","updated":"2025-07-21T08:53:24+07:00","status":"STATUS_DELISTED","tradeable":0,"type_company":"Saham","exchange":"IDX","orderbook":{"bid":{"price":"N/A","volume":"N/A"},"offer":{"price":"N/A","volume":"N/A"}},"market_hour":{"status":"close","time_left":52561,"formatted_time_left":"14 jam 36 menit 1 detik","suspend_info":""}},"chart":{"cagr":"","change":0,"drawdown":"","markingpoint":"","percentage":"","prices":[],"timeframe":"today","xaxisopt":"","previous":50,"line_weight":0,"previous_timeframe_price":null,"chart_type":"PRICE_CHART_TYPE_UNSPECIFIED","interval_in_minutes":0,"allowed_chart_type":[],"max_candles":0}},"__N_SSP":true},"page":"/symbol/[symbol]","query":{"symbol":"MYRX"},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>
|
||||
1
crates/idx-core/tests/fixtures/stockbit_symbol_zzzz_404.html
vendored
Normal file
1
crates/idx-core/tests/fixtures/stockbit_symbol_zzzz_404.html
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><title>Error | Stockbit - Investasi Saham Bersama Komunitas Saham Terbesar di Indonesia</title></head><body><div id="__next"></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{},"__N_SSP":true},"page":"/404","query":{},"buildId":"v3.54.5","isFallback":false,"gssp":true}</script></body></html>
|
||||
|
|
@ -29,6 +29,15 @@ pub struct HistoryParams {
|
|||
pub interval: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct IntradayParams {
|
||||
/// IDX ticker, e.g. "BBCA"; indices as "^JKSE" or "IHSG".
|
||||
pub symbol: String,
|
||||
/// Only the latest N minutes (default: the whole session, at most 400).
|
||||
#[serde(default)]
|
||||
pub tail: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
pub struct ScreenParams {
|
||||
/// Screen to run (default "top-performers").
|
||||
|
|
|
|||
|
|
@ -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<SymbolsParams>) -> 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<IntradayParams>) -> ToolResult {
|
||||
let IntradayParams { symbol, tail } = params.0;
|
||||
self.live(move |idx| {
|
||||
let tail = bounded_limit("tail", tail, MAX_INTRADAY_POINTS, MAX_INTRADAY_POINTS)?;
|
||||
let mut day = idx.market().intraday(&symbol, CachePolicy::Default)?;
|
||||
let points = &mut day.data.points;
|
||||
points.drain(..points.len().saturating_sub(tail));
|
||||
Ok(day)
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[tool(description = "OHLCV price history for one IDX ticker.")]
|
||||
async fn get_price_history(&self, params: Parameters<HistoryParams>) -> ToolResult {
|
||||
let HistoryParams {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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/<version> (+repo)` passes. A 403 is
|
||||
`UPSTREAMBLOCKED` and is not retried. Requests are at least 1 s apart
|
||||
across the user's idx processes (the last request's time sits in
|
||||
`<cache>/stockbit.pace` under a file lock, so a shell loop of `idx` calls is
|
||||
paced too), and snapshots are cached for 5 s (at most `quote_ttl`), which
|
||||
coalesces an agent's quote-then-intraday burst across MCP calls.
|
||||
- Stockbit's terms call its data delayed at least 10 minutes and for
|
||||
personal use; frontends say so. Fixtures (`tests/fixtures/stockbit_*`) are
|
||||
pages recorded 2026-10-22 after the close, cut to the fields read.
|
||||
|
||||
### Capability Gating
|
||||
```
|
||||
MarketDataProvider = QuoteProvider + FundamentalsProvider
|
||||
|
|
@ -161,8 +199,13 @@ Factory functions:
|
|||
history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)>
|
||||
broker_provider() → Box<dyn BrokerSummaryProvider>
|
||||
trading_summary_provider() → Box<dyn TradingSummaryProvider>
|
||||
intraday_provider() → Box<dyn IntradayProvider>
|
||||
```
|
||||
|
||||
`ProviderKind::Stockbit` implements quotes only: fundamentals are
|
||||
`UNSUPPORTED`, the MSN-only capabilities say "requires --provider msn", and
|
||||
`auto` history falls back to Yahoo.
|
||||
|
||||
## Ownership Module (SQLite-backed)
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <old-idx>` runs every case with both binaries back to back, each
|
||||
with its own config and cache, and fails on a different exit code, any added,
|
||||
missing, or retyped JSON key, or changed values for `profile`, `financials`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <old-idx>, every case also runs the old binary back to back
|
||||
# and fails on any difference in exit code or JSON structure, and on any value
|
||||
|
|
@ -26,7 +28,8 @@ usage() {
|
|||
Usage: scripts/live-smoke.sh [options]
|
||||
|
||||
Options:
|
||||
--group <stocks|ownership> Restrict to a group. Repeatable. Default: both
|
||||
--group <name> stocks, stockbit, or ownership. Repeatable.
|
||||
Default: all three
|
||||
--symbol <ticker> Primary ticker. Default: BBCA
|
||||
--bin <path> Use an existing idx binary (skips cargo build)
|
||||
--compare <old-idx> Also run an older idx binary and diff the results
|
||||
|
|
@ -46,12 +49,12 @@ while [[ $# -gt 0 ]]; do
|
|||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -60,6 +65,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
|
|||
|
||||
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> 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> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx stocks quote`
|
||||
|
|
@ -100,19 +107,67 @@ Arguments:
|
|||
<SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
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] <SYMBOL>
|
||||
|
||||
Arguments:
|
||||
<SYMBOL>
|
||||
Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT>
|
||||
Output format [default: auto = table on a terminal, json when piped]
|
||||
|
||||
[possible values: auto, table, json, jsonl]
|
||||
|
||||
--no-color
|
||||
Disable colored output
|
||||
|
||||
-q, --quiet
|
||||
Suppress non-essential output
|
||||
|
||||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
--no-cache
|
||||
Bypass local cache
|
||||
|
||||
-h, --help
|
||||
Print help (see a summary with '-h')
|
||||
|
||||
Examples:
|
||||
idx stocks intraday BBCA
|
||||
idx stocks intraday ^JKSE
|
||||
idx -o json stocks intraday BBCA | jq '.points[-1]'
|
||||
```
|
||||
|
||||
### `idx stocks history`
|
||||
|
|
@ -140,6 +195,8 @@ Options:
|
|||
Suppress non-essential output
|
||||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline
|
||||
Run without network requests
|
||||
--no-cache
|
||||
|
|
@ -173,6 +230,8 @@ Options:
|
|||
Suppress non-essential output
|
||||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline
|
||||
Run without network requests
|
||||
--no-cache
|
||||
|
|
@ -196,13 +255,14 @@ Arguments:
|
|||
<SYMBOL> Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx stocks growth BBCA
|
||||
|
|
@ -220,13 +280,14 @@ Arguments:
|
|||
<SYMBOL> Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx stocks valuation BBCA
|
||||
|
|
@ -244,13 +305,14 @@ Arguments:
|
|||
<SYMBOL> Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx stocks risk BBCA
|
||||
|
|
@ -268,13 +330,14 @@ Arguments:
|
|||
<SYMBOL> Single ticker symbol (e.g. BBCA)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx stocks fundamental BBCA
|
||||
|
|
@ -337,6 +400,11 @@ Options:
|
|||
|
||||
[default: all]
|
||||
|
||||
--provider <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 <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 <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 <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -502,13 +580,14 @@ Arguments:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `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 <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:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
--forecast Only include forward earnings rows
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--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> 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 <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:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx stocks insights`
|
||||
|
|
@ -597,13 +679,14 @@ Arguments:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx stocks news`
|
||||
|
|
@ -617,14 +700,15 @@ Arguments:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> [default: 10]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
--limit <LIMIT> [default: 10]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx stocks screen`
|
||||
|
|
@ -635,16 +719,17 @@ MSN screener
|
|||
Usage: idx stocks screen [OPTIONS]
|
||||
|
||||
Options:
|
||||
--filter <FILTER> [default: top-performers]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
--region <REGION> [default: id]
|
||||
--limit <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 <FILTER> [default: top-performers]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
--region <REGION> [default: id]
|
||||
--limit <LIMIT> [default: 50]
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx stocks compare`
|
||||
|
|
@ -658,13 +743,14 @@ Arguments:
|
|||
<SYMBOLS>... One or more symbols, comma-separated or space-separated; `-` reads them from stdin
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx stocks compare BBCA BBRI BMRI
|
||||
|
|
@ -704,6 +790,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -733,13 +824,14 @@ Create default configuration file
|
|||
Usage: idx config init [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx config get`
|
||||
|
|
@ -753,13 +845,14 @@ Arguments:
|
|||
<KEY> Key such as general.provider (see `idx config --help`)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx config set`
|
||||
|
|
@ -774,13 +867,14 @@ Arguments:
|
|||
<VALUE>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx config path`
|
||||
|
|
@ -791,13 +885,14 @@ Print configuration file path
|
|||
Usage: idx config path [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
## `idx cache`
|
||||
|
|
@ -813,13 +908,14 @@ Commands:
|
|||
help Print this message or the help of the given subcommand(s)
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx cache info`
|
||||
|
|
@ -830,13 +926,14 @@ Show cache usage information
|
|||
Usage: idx cache info [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx cache clear`
|
||||
|
|
@ -847,13 +944,14 @@ Clear all cached files
|
|||
Usage: idx cache clear [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
## `idx ownership`
|
||||
|
|
@ -899,6 +997,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -924,15 +1027,16 @@ Discover the latest IDX-hosted ownership report URLs
|
|||
Usage: idx ownership discover [OPTIONS]
|
||||
|
||||
Options:
|
||||
--family <FAMILY> Report family to discover: above1 (default), all, above5, or investor-type [default: above1]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--limit <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 <FAMILY> Report family to discover: above1 (default), all, above5, or investor-type [default: above1]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--limit <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 <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 <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 <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -1056,14 +1170,15 @@ Arguments:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--source <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> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--source <SOURCE> [default: all]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx ownership entity`
|
||||
|
|
@ -1077,13 +1192,14 @@ Arguments:
|
|||
<NAME> Entity name or part of one, case-insensitive; the shortest match is shown
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
||||
Examples:
|
||||
idx ownership entity "ASTRA INTERNATIONAL"
|
||||
|
|
@ -1121,6 +1237,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -1149,6 +1270,7 @@ Options:
|
|||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
|
|
@ -1162,15 +1284,16 @@ Rank tickers by ownership concentration
|
|||
Usage: idx ownership concentration [OPTIONS]
|
||||
|
||||
Options:
|
||||
--by <BY> [default: hhi]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
--top <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 <BY> [default: hhi]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
--top <TOP> [default: 20]
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx ownership flow`
|
||||
|
|
@ -1184,13 +1307,14 @@ Arguments:
|
|||
<SYMBOL>
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx ownership changes`
|
||||
|
|
@ -1223,6 +1347,11 @@ Options:
|
|||
-v, --verbose...
|
||||
Increase verbosity (-v, -vv)
|
||||
|
||||
--provider <PROVIDER>
|
||||
Market data provider [default: general.provider, else msn]
|
||||
|
||||
[possible values: yahoo, msn, stockbit]
|
||||
|
||||
--offline
|
||||
Run without network requests
|
||||
|
||||
|
|
@ -1248,15 +1377,16 @@ Arguments:
|
|||
<ROOT> Ticker code or entity name to start from
|
||||
|
||||
Options:
|
||||
--depth <DEPTH> Traversal depth (default 2) [default: 2]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--format <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 <DEPTH> Traversal depth (default 2) [default: 2]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--format <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 <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> 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> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
#### `idx ownership resolve unresolved`
|
||||
|
|
@ -1290,14 +1421,15 @@ List unresolved or low-confidence entity aliases
|
|||
Usage: idx ownership resolve unresolved [OPTIONS]
|
||||
|
||||
Options:
|
||||
--limit <LIMIT> [default: 50]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
--limit <LIMIT> [default: 50]
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
#### `idx ownership resolve map`
|
||||
|
|
@ -1312,13 +1444,14 @@ Arguments:
|
|||
<ENTITY> The canonical entity name to map to
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
#### `idx ownership resolve merge`
|
||||
|
|
@ -1333,13 +1466,14 @@ Arguments:
|
|||
<MERGE> Entity ID to merge and delete
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
### `idx ownership releases`
|
||||
|
|
@ -1350,13 +1484,14 @@ List imported KSEI releases
|
|||
Usage: idx ownership releases [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
## `idx completions`
|
||||
|
|
@ -1370,13 +1505,14 @@ Arguments:
|
|||
<SHELL> [possible values: bash, zsh, fish]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
||||
## `idx version`
|
||||
|
|
@ -1387,11 +1523,12 @@ Show idx-cli version
|
|||
Usage: idx version [OPTIONS]
|
||||
|
||||
Options:
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
-o, --output <OUTPUT> Output format [default: auto = table on a terminal, json when piped] [possible values: auto, table, json, jsonl]
|
||||
--no-color Disable colored output
|
||||
-q, --quiet Suppress non-essential output
|
||||
-v, --verbose... Increase verbosity (-v, -vv)
|
||||
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||
--offline Run without network requests
|
||||
--no-cache Bypass local cache
|
||||
-h, --help Print help
|
||||
```
|
||||
|
|
|
|||
75
web/src/content/docs/cli/stockbit.md
Normal file
75
web/src/content/docs/cli/stockbit.md
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
title: Stockbit
|
||||
description: Today's session by the minute and quotes with best bid/offer, from Stockbit's public page. Opt-in, no account.
|
||||
---
|
||||
|
||||
[Stockbit](https://stockbit.com)'s public symbol page carries a quote, the
|
||||
best bid and offer, and today's price for every minute of the session. idx
|
||||
reads that page directly. It needs no Stockbit account, no login and no
|
||||
helper tools.
|
||||
|
||||
```bash
|
||||
idx stocks intraday BBCA # today's session, by the minute
|
||||
idx stocks intraday ^JKSE # an index (or IHSG, LQ45)
|
||||
idx --provider stockbit stocks quote BBCA # quote with bid/offer
|
||||
idx -o json stocks intraday BBCA | jq '.points | length'
|
||||
```
|
||||
|
||||
## What you get
|
||||
|
||||
| | `stocks intraday` | `--provider stockbit stocks quote` |
|
||||
| --- | --- | --- |
|
||||
| Price, change, previous close, volume | yes | yes |
|
||||
| Best bid and offer | while the market is open | while the market is open |
|
||||
| Market status (`market_status`) | yes | yes |
|
||||
| Price per minute (`points`) | yes | no |
|
||||
| Market cap, 52-week range | no | no |
|
||||
|
||||
- **Delayed.** Stockbit's terms describe its market data as "a real-time
|
||||
snapshot, delayed at least 10 minutes". It is not a live feed.
|
||||
- **Bid and offer only while trading.** Outside the session, Stockbit still
|
||||
shows the book as it closed. idx leaves `bid` and `offer` out then, so the
|
||||
closing book never passes for a live one.
|
||||
- **Top of the book only.** One bid and one offer price, no depth. The full
|
||||
order book needs a Stockbit login, which idx does not do.
|
||||
- **Prices only.** The page has no per-minute volume or OHLC.
|
||||
- **Empty sessions are data.** A suspended or delisted stock answers with
|
||||
`points: []` and a `session_note` saying so (`Stockbit lists the stock as
|
||||
suspended`). An active stock with no volume says `no trades this session`,
|
||||
even when its chart is a flat line at the previous close. Neither is an
|
||||
error.
|
||||
|
||||
`--provider stockbit` (or `IDX_PROVIDER=stockbit`, or
|
||||
`idx config set general.provider stockbit`) changes only where quotes come
|
||||
from. History still comes from Yahoo. Company data, fundamentals and
|
||||
screening answer `UNSUPPORTED` under it, so use MSN for those.
|
||||
|
||||
## How idx talks to Stockbit
|
||||
|
||||
- One request per symbol to `https://stockbit.com/symbol/<CODE>`, at least one
|
||||
second apart across all your idx processes (a shell loop of `idx` calls
|
||||
included), cached for 5 seconds.
|
||||
- An honest user agent, `idx-cli/<version> (+https://github.com/0xrsydn/idx-cli)`,
|
||||
not a browser disguise. Stockbit's CDN refuses some generic agents
|
||||
(`curl/…`) outright but lets this one through. No curl-impersonate
|
||||
needed.
|
||||
- No retries. If Stockbit's CDN refuses your machine, the command fails at
|
||||
once with `UPSTREAMBLOCKED` (exit `4`):
|
||||
|
||||
```json
|
||||
{"error":true,"code":"UPSTREAMBLOCKED","message":"upstream blocked: stockbit.com refused this client (HTTP 403 from its CDN); ..."}
|
||||
```
|
||||
|
||||
Wait, or try from another network. `--provider msn` serves quotes without
|
||||
Stockbit.
|
||||
|
||||
## Terms
|
||||
|
||||
Stockbit's [terms](https://stockbit.com/terms) allow its content for personal,
|
||||
non-commercial use. They prohibit data mining, robots and redistribution
|
||||
without Stockbit's written consent. idx is not affiliated with or endorsed by
|
||||
Stockbit. Use this for looking at a stock now and then, not for crawling the
|
||||
market or republishing prices.
|
||||
|
||||
Over MCP, the same data comes from the `get_intraday` tool, and from
|
||||
`get_quotes` when the server runs with `IDX_PROVIDER=stockbit`.
|
||||
|
|
@ -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/).
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
| --- | --- | --- | --- |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ TLKM.JK 2,250 <span class="up">+0.45%</span> 87,159,900
|
|||
<div>
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="4" y="7" width="16" height="12" rx="3"></rect><path d="M12 3v4M9 13h.01M15 13h.01"></path></svg>
|
||||
<h2>Agents</h2>
|
||||
<p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 11 tools for any MCP client.</p>
|
||||
<p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 15 tools for any MCP client.</p>
|
||||
<code>npx -y idx-mcp</code>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue