mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45: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 technical BBCA
|
||||||
idx stocks fundamental BBCA
|
idx stocks fundamental BBCA
|
||||||
idx stocks compare BBCA BBRI BMRI
|
idx stocks compare BBCA BBRI BMRI
|
||||||
|
idx stocks intraday BBCA # today by the minute (Stockbit, delayed)
|
||||||
idx -o json stocks quote BBCA
|
idx -o json stocks quote BBCA
|
||||||
|
|
||||||
# Ownership: download the maintained snapshot once, then query locally
|
# Ownership: download the maintained snapshot once, then query locally
|
||||||
|
|
@ -90,8 +91,12 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31
|
||||||
- Analysis: `technical`, `growth`, `valuation`, `risk`, and a combined
|
- Analysis: `technical`, `growth`, `valuation`, `risk`, and a combined
|
||||||
`fundamental` report; multi-symbol `compare`; MSN stock `screen`
|
`fundamental` report; multi-symbol `compare`; MSN stock `screen`
|
||||||
- Providers: MSN is the default for quotes, fundamentals, and screening;
|
- Providers: MSN is the default for quotes, fundamentals, and screening;
|
||||||
Yahoo serves history. Switch with `IDX_PROVIDER=msn|yahoo` and
|
Yahoo serves history. Switch with `--provider`/`IDX_PROVIDER=msn|yahoo` and
|
||||||
`IDX_HISTORY_PROVIDER=auto|yahoo|msn`
|
`IDX_HISTORY_PROVIDER=auto|yahoo|msn`
|
||||||
|
- Stockbit, opt-in and without a login: `stocks intraday` (today's price by
|
||||||
|
the minute) and `--provider stockbit` quotes with the best bid/offer while
|
||||||
|
the market is open. Delayed at least 10 minutes, for personal use under
|
||||||
|
Stockbit's terms; see the [Stockbit page](https://idx.rasyidanaf.com/cli/stockbit/)
|
||||||
- Local file cache with TTL, and `--offline` mode with stale-cache fallback
|
- Local file cache with TTL, and `--offline` mode with stale-cache fallback
|
||||||
|
|
||||||
### Ownership (KSEI shareholders above 1%)
|
### Ownership (KSEI shareholders above 1%)
|
||||||
|
|
@ -158,8 +163,8 @@ change it to `auto` with `idx config set general.output auto`.
|
||||||
|
|
||||||
## MCP server
|
## MCP server
|
||||||
|
|
||||||
`idx-mcp` serves the same data to MCP clients over stdio, as 14 tools:
|
`idx-mcp` serves the same data to MCP clients over stdio, as 15 tools:
|
||||||
`get_quotes`, `get_price_history`, `screen_stocks`, `get_company`,
|
`get_quotes`, `get_intraday`, `get_price_history`, `screen_stocks`, `get_company`,
|
||||||
`analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`,
|
`analyze_stock`, `compare_stocks`, `get_broker_summary`, `analyze_bandar`,
|
||||||
`get_foreign_flow`, `get_shareholders`, `get_investor_holdings`,
|
`get_foreign_flow`, `get_shareholders`, `get_investor_holdings`,
|
||||||
`search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as
|
`search_investors`, `get_ownership_changes`, and `ownership_status`. Live results come wrapped as
|
||||||
|
|
|
||||||
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.
|
history lives in git.
|
||||||
|
|
||||||
## Stocks
|
## Stocks
|
||||||
- `market summary`: IHSG index and market breadth
|
- `market summary`: market breadth and an index overview (index levels alone
|
||||||
|
are already `stocks intraday ^JKSE` and `--provider stockbit stocks quote IHSG`)
|
||||||
- `market movers`: top gainers, losers, and volume
|
- `market movers`: top gainers, losers, and volume
|
||||||
- `market sectors`: sector performance overview
|
- `market sectors`: sector performance overview
|
||||||
- `screen query "<EXPR>"`: filter stocks by expression
|
- `screen query "<EXPR>"`: filter stocks by expression
|
||||||
- `screen presets` / `screen run <PRESET>`: built-in screener presets
|
- `screen presets` / `screen run <PRESET>`: built-in screener presets
|
||||||
(graduate `screen` out of `stocks` only if these grow into a richer workflow)
|
(graduate `screen` out of `stocks` only if these grow into a richer workflow)
|
||||||
- Multi-period financial statements (today `financials` is single-period)
|
- Multi-period financial statements (today `financials` is single-period)
|
||||||
- Order book (bid/offer depth): no free, login-free source exists. IDX sells
|
- Order book depth: no free, login-free source exists (IDX sells delayed depth
|
||||||
delayed depth to vendors and Stockbit's needs a user login. Revisit if a
|
to vendors). Planned as the Stockbit login follow-on: opt-in with the
|
||||||
permitted source appears; do not ship the closing best bid/offer as one.
|
user's own account, the depth ladder, intraday OHLCV and token refresh from
|
||||||
|
`exodus.stockbit.com`; fzn0x/idx (MIT) maps the endpoints. The public page
|
||||||
|
already gives the best bid/offer (L1) while the market is open.
|
||||||
|
- Observe Stockbit's `market_hour.status` in the pre-open (08:45-09:00) and
|
||||||
|
the closing auction (15:50-16:00) before `--watch` or alerts build on it;
|
||||||
|
`open`, `break` and `close` are known, and the bid/offer gate hides the book
|
||||||
|
for anything but `open`.
|
||||||
- Bandarmology follow-ups: a daily series (per-session readings and the
|
- Bandarmology follow-ups: a daily series (per-session readings and the
|
||||||
cumulative top-N net) and a price-move phase (quiet accumulation, markup),
|
cumulative top-N net) and a price-move phase (quiet accumulation, markup),
|
||||||
calibration of the `top_concentration_v1` thresholds across more sessions,
|
calibration of the `top_concentration_v1` thresholds across more sessions,
|
||||||
|
|
@ -26,7 +33,8 @@ history lives in git.
|
||||||
- `watchlist`: named symbol sets usable by other commands
|
- `watchlist`: named symbol sets usable by other commands
|
||||||
- `alerts`: price alerts with a daemon
|
- `alerts`: price alerts with a daemon
|
||||||
- CSV/TSV output
|
- CSV/TSV output
|
||||||
- Additional providers (Alpha Vantage, Twelve Data, IDX official)
|
- Additional providers (Alpha Vantage, Twelve Data); IDX's own site serves
|
||||||
|
only `stocks flow --source idx` today
|
||||||
|
|
||||||
## Agent integration
|
## Agent integration
|
||||||
- JSON Schema export for command output
|
- JSON Schema export for command output
|
||||||
|
|
@ -39,11 +47,16 @@ history lives in git.
|
||||||
- `stocks flow --source idx` depends on IDX's Cloudflare passing a
|
- `stocks flow --source idx` depends on IDX's Cloudflare passing a
|
||||||
curl-impersonate profile (only `curl_safari184` in October 2026) and on the
|
curl-impersonate profile (only `curl_safari184` in October 2026) and on the
|
||||||
network; the default IPOT source rounds to one decimal.
|
network; the default IPOT source rounds to one decimal.
|
||||||
|
- Stockbit's terms call its data delayed at least 10 minutes and for
|
||||||
|
personal, non-commercial use (on 2026-10-23 the last trade was within a
|
||||||
|
minute of the clock); its CDN refuses some user agents with a 403
|
||||||
|
(`UPSTREAMBLOCKED`). Its order-book sizes are not shown: the page does not
|
||||||
|
say whether they are lots or shares.
|
||||||
- Yahoo can return `429` from datacenter IPs; requests retry with backoff but
|
- Yahoo can return `429` from datacenter IPs; requests retry with backoff but
|
||||||
can still fail.
|
can still fail.
|
||||||
- `stocks technical` reports the SMA200 trend as unavailable when fewer than
|
- `stocks technical` reports the SMA200 trend as unavailable when fewer than
|
||||||
200 daily candles are returned.
|
200 daily candles are returned.
|
||||||
- MSN's `low-pe` screen returned no priced stocks on 2026-10-14 (upstream;
|
- MSN's `low-pe` screen returns no priced stocks (`PARSEERROR`; still so on
|
||||||
other filters work).
|
2026-10-23, upstream; other filters work).
|
||||||
- MSN history for IDX is price-only (`1mo`, `3mo`, `1y`); Yahoo stays the
|
- MSN history for IDX is price-only (`1mo`, `3mo`, `1y`); Yahoo stays the
|
||||||
default OHLCV source.
|
default OHLCV source.
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ pub mod ownership;
|
||||||
pub mod stocks;
|
pub mod stocks;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand, ValueEnum};
|
use clap::{Parser, Subcommand, ValueEnum};
|
||||||
|
use idx_core::config::ProviderKind;
|
||||||
|
|
||||||
use crate::output::OutputSetting;
|
use crate::output::OutputSetting;
|
||||||
|
|
||||||
|
|
@ -39,6 +40,13 @@ pub struct Cli {
|
||||||
pub quiet: bool,
|
pub quiet: bool,
|
||||||
#[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")]
|
#[arg(short, long, global = true, action = clap::ArgAction::Count, help = "Increase verbosity (-v, -vv)")]
|
||||||
pub verbose: u8,
|
pub verbose: u8,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
value_enum,
|
||||||
|
global = true,
|
||||||
|
help = "Market data provider [default: general.provider, else msn]"
|
||||||
|
)]
|
||||||
|
pub provider: Option<ProviderKind>,
|
||||||
#[arg(long, global = true, help = "Run without network requests")]
|
#[arg(long, global = true, help = "Run without network requests")]
|
||||||
pub offline: bool,
|
pub offline: bool,
|
||||||
#[arg(long, global = true, help = "Bypass local cache")]
|
#[arg(long, global = true, help = "Bypass local cache")]
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use crate::config::{HistoryProviderKind, IdxConfig};
|
||||||
use crate::output::{
|
use crate::output::{
|
||||||
render_bandar, render_broker_summary, render_compare, render_earnings, render_financials,
|
render_bandar, render_broker_summary, render_compare, render_earnings, render_financials,
|
||||||
render_foreign_flow, render_fundamental, render_growth, render_history, render_insights,
|
render_foreign_flow, render_fundamental, render_growth, render_history, render_insights,
|
||||||
render_news, render_profile, render_quotes, render_risk, render_screener, render_sentiment,
|
render_intraday, render_news, render_profile, render_quotes, render_risk, render_screener,
|
||||||
render_technical, render_valuation,
|
render_sentiment, render_technical, render_valuation,
|
||||||
};
|
};
|
||||||
use idx_core::analysis::BandarQuery;
|
use idx_core::analysis::BandarQuery;
|
||||||
use idx_core::analysis::bandar::DEFAULT_TOP;
|
use idx_core::analysis::bandar::DEFAULT_TOP;
|
||||||
|
|
@ -126,13 +126,26 @@ pub struct StocksCmd {
|
||||||
pub enum StocksSubcommand {
|
pub enum StocksSubcommand {
|
||||||
#[command(
|
#[command(
|
||||||
about = "Get real-time stock quotes",
|
about = "Get real-time stock quotes",
|
||||||
after_help = "Examples:\n idx stocks quote BBCA\n idx stocks quote BBCA,BBRI,BMRI\n idx -o json stocks quote BBCA\n echo BBCA BBRI | idx stocks quote -"
|
after_help = "Examples:\n idx stocks quote BBCA\n idx stocks quote BBCA,BBRI,BMRI\n idx -o json stocks quote BBCA\n echo BBCA BBRI | idx stocks quote -\n idx --provider stockbit stocks quote BBCA # with bid/offer while the market is open"
|
||||||
)]
|
)]
|
||||||
Quote {
|
Quote {
|
||||||
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
|
/// One or more symbols, comma-separated or space-separated; `-` reads them from stdin.
|
||||||
#[arg(required = true)]
|
#[arg(required = true)]
|
||||||
symbols: Vec<String>,
|
symbols: Vec<String>,
|
||||||
},
|
},
|
||||||
|
#[command(
|
||||||
|
about = "Today's session price by the minute (Stockbit)",
|
||||||
|
long_about = "Today's session of one IDX stock or index, by the minute, from Stockbit's \
|
||||||
|
public page (no login): the quote, the best bid/offer while the market is open, a sparkline, \
|
||||||
|
and the last minutes. Delayed at least 10 minutes per Stockbit's terms; prices only, no \
|
||||||
|
per-minute volume. A stock that did not trade has no minutes, which is not an error. \
|
||||||
|
Indices use Yahoo's tickers (^JKSE, ^JKLQ45) or Stockbit's (IHSG, LQ45).",
|
||||||
|
after_help = "Examples:\n idx stocks intraday BBCA\n idx stocks intraday ^JKSE\n idx -o json stocks intraday BBCA | jq '.points[-1]'"
|
||||||
|
)]
|
||||||
|
Intraday {
|
||||||
|
/// Single ticker symbol (e.g. BBCA).
|
||||||
|
symbol: String,
|
||||||
|
},
|
||||||
#[command(
|
#[command(
|
||||||
about = "Get historical OHLC data",
|
about = "Get historical OHLC data",
|
||||||
after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk"
|
after_help = "Examples:\n idx stocks history BBCA --period 3mo\n idx stocks history BBCA --period 1y --interval 1wk"
|
||||||
|
|
@ -313,7 +326,11 @@ pub fn handle(
|
||||||
StocksSubcommand::Quote { symbols } => {
|
StocksSubcommand::Quote { symbols } => {
|
||||||
let quotes = idx.market().quotes(&expand_symbols(symbols)?, policy)?;
|
let quotes = idx.market().quotes(&expand_symbols(symbols)?, policy)?;
|
||||||
let quotes: Vec<_> = quotes.into_iter().map(|q| q.data).collect();
|
let quotes: Vec<_> = quotes.into_iter().map(|q| q.data).collect();
|
||||||
render_quotes("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 {
|
StocksSubcommand::History {
|
||||||
symbol,
|
symbol,
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,9 @@ impl IdxConfig {
|
||||||
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
|
pub fn load_with_cli(cli: &Cli) -> Result<Self, IdxError> {
|
||||||
let mut core = CoreConfig::load()?;
|
let mut core = CoreConfig::load()?;
|
||||||
core.verbose = cli.verbose > 0;
|
core.verbose = cli.verbose > 0;
|
||||||
|
if let Some(provider) = cli.provider {
|
||||||
|
core.provider = provider;
|
||||||
|
}
|
||||||
let mut setting = OutputSetting::Auto;
|
let mut setting = OutputSetting::Auto;
|
||||||
let mut no_color = false;
|
let mut no_color = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,11 @@ use idx_core::analysis::fundamental::{
|
||||||
};
|
};
|
||||||
use idx_core::analysis::report::TechnicalReport;
|
use idx_core::analysis::report::TechnicalReport;
|
||||||
use idx_core::api::types::{
|
use idx_core::api::types::{
|
||||||
CompanyProfile, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc, Quote,
|
CompanyProfile, EarningsReport, FinancialStatements, InsightData, Intraday, NewsItem, Ohlc,
|
||||||
SentimentData,
|
Quote, SentimentData,
|
||||||
};
|
};
|
||||||
use idx_core::broker::types::BrokerSummary;
|
use idx_core::broker::types::BrokerSummary;
|
||||||
|
use idx_core::config::ProviderKind;
|
||||||
use idx_core::error::IdxError;
|
use idx_core::error::IdxError;
|
||||||
use idx_core::flow::types::ForeignFlow;
|
use idx_core::flow::types::ForeignFlow;
|
||||||
|
|
||||||
|
|
@ -67,15 +68,30 @@ impl OutputFormat {
|
||||||
|
|
||||||
pub fn render_quotes(
|
pub fn render_quotes(
|
||||||
quotes: &[Quote],
|
quotes: &[Quote],
|
||||||
|
provider: ProviderKind,
|
||||||
format: &OutputFormat,
|
format: &OutputFormat,
|
||||||
no_color: bool,
|
no_color: bool,
|
||||||
) -> Result<(), IdxError> {
|
) -> Result<(), IdxError> {
|
||||||
match format {
|
match format {
|
||||||
|
OutputFormat::Table if provider == ProviderKind::Stockbit => {
|
||||||
|
table::print_stockbit_quotes(quotes, no_color)
|
||||||
|
}
|
||||||
OutputFormat::Table => table::print_quotes(quotes, no_color),
|
OutputFormat::Table => table::print_quotes(quotes, no_color),
|
||||||
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes),
|
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(quotes),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn render_intraday(
|
||||||
|
day: &Intraday,
|
||||||
|
format: &OutputFormat,
|
||||||
|
no_color: bool,
|
||||||
|
) -> Result<(), IdxError> {
|
||||||
|
match format {
|
||||||
|
OutputFormat::Table => table::print_intraday(day, no_color),
|
||||||
|
OutputFormat::Json | OutputFormat::Jsonl => json::print_json(day),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn render_broker_summary(
|
pub fn render_broker_summary(
|
||||||
summary: &BrokerSummary,
|
summary: &BrokerSummary,
|
||||||
format: &OutputFormat,
|
format: &OutputFormat,
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ use idx_core::analysis::fundamental::{
|
||||||
use idx_core::analysis::report::TechnicalReport;
|
use idx_core::analysis::report::TechnicalReport;
|
||||||
use idx_core::analysis::signals::Signal;
|
use idx_core::analysis::signals::Signal;
|
||||||
use idx_core::api::types::{
|
use idx_core::api::types::{
|
||||||
CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, NewsItem, Ohlc,
|
CompanyProfile, EarningsData, EarningsReport, FinancialStatements, InsightData, Intraday,
|
||||||
Quote, SentimentData,
|
NewsItem, Ohlc, Quote, SentimentData,
|
||||||
};
|
};
|
||||||
use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary};
|
use idx_core::broker::types::{BrokerBoard, BrokerFlow, BrokerInvestor, BrokerKind, BrokerSummary};
|
||||||
use idx_core::error::IdxError;
|
use idx_core::error::IdxError;
|
||||||
|
|
@ -61,12 +61,7 @@ pub fn print_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
for q in quotes {
|
for q in quotes {
|
||||||
let pct_cell = match q.change_pct {
|
let pct_cell = pct_cell(q.change_pct, no_color);
|
||||||
None => cell("-"),
|
|
||||||
Some(pct) if no_color => cell(format!("{pct:+.2}%")),
|
|
||||||
Some(pct) if pct >= 0.0 => cell(format!("{pct:+.2}%")).fg(Color::Green),
|
|
||||||
Some(pct) => cell(format!("{pct:+.2}%")).fg(Color::Red),
|
|
||||||
};
|
|
||||||
table.add_row(vec![
|
table.add_row(vec![
|
||||||
cell(&q.symbol),
|
cell(&q.symbol),
|
||||||
cell(format_idr(q.price)),
|
cell(format_idr(q.price)),
|
||||||
|
|
@ -121,6 +116,182 @@ fn stale_quote_notes(quotes: &[Quote], now: chrono::DateTime<chrono::Utc>) -> Ve
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pct_cell(change_pct: Option<f64>, no_color: bool) -> Cell {
|
||||||
|
match change_pct {
|
||||||
|
None => cell("-"),
|
||||||
|
Some(pct) if no_color => cell(format!("{pct:+.2}%")),
|
||||||
|
Some(pct) if pct >= 0.0 => cell(format!("{pct:+.2}%")).fg(Color::Green),
|
||||||
|
Some(pct) => cell(format!("{pct:+.2}%")).fg(Color::Red),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Printed under every Stockbit table: what the data is and is not.
|
||||||
|
const STOCKBIT_SOURCE_NOTE: &str = "Source: Stockbit's public page, delayed at least 10 \
|
||||||
|
minutes (Stockbit's terms); bid/offer only while the market is open.";
|
||||||
|
|
||||||
|
fn opt_idr(value: Option<i64>) -> String {
|
||||||
|
value.map(format_idr).unwrap_or_else(|| "-".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wib_minute(time: chrono::DateTime<chrono::FixedOffset>) -> String {
|
||||||
|
time.format("%Y-%m-%d %H:%M").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quotes from `--provider stockbit`: Stockbit has no market cap or 52-week
|
||||||
|
/// range, but a best bid/offer and the market's state.
|
||||||
|
pub fn print_stockbit_quotes(quotes: &[Quote], no_color: bool) -> Result<(), IdxError> {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table
|
||||||
|
.load_preset(UTF8_FULL)
|
||||||
|
.set_content_arrangement(ContentArrangement::Dynamic)
|
||||||
|
.set_header(vec![
|
||||||
|
"SYMBOL", "PRICE", "CHG", "CHG%", "VOLUME", "BID", "OFFER", "MARKET", "AS OF",
|
||||||
|
]);
|
||||||
|
for q in quotes {
|
||||||
|
table.add_row(vec![
|
||||||
|
cell(&q.symbol),
|
||||||
|
cell(format_idr(q.price)),
|
||||||
|
cell(
|
||||||
|
q.change
|
||||||
|
.map(|c| format!("{c:+}"))
|
||||||
|
.unwrap_or_else(|| "-".to_string()),
|
||||||
|
),
|
||||||
|
pct_cell(q.change_pct, no_color),
|
||||||
|
cell(q.volume.map(format_u64).unwrap_or_else(|| "-".to_string())),
|
||||||
|
cell(opt_idr(q.bid)),
|
||||||
|
cell(opt_idr(q.offer)),
|
||||||
|
cell(q.market_status.as_deref().unwrap_or("-")),
|
||||||
|
cell(q.as_of.map(wib_minute).unwrap_or_else(|| "-".to_string())),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
println!("{table}");
|
||||||
|
for q in quotes {
|
||||||
|
if let Some(note) = &q.session_note {
|
||||||
|
println!(
|
||||||
|
"Note: {}: {}",
|
||||||
|
terminal_safe(&q.symbol),
|
||||||
|
terminal_safe(note)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for note in stale_quote_notes(quotes, chrono::Utc::now()) {
|
||||||
|
println!("{}", terminal_safe(¬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> {
|
pub fn print_history(symbol: &str, history: &[Ohlc]) -> Result<(), IdxError> {
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
|
|
@ -1410,6 +1581,10 @@ mod tests {
|
||||||
avg_volume: None,
|
avg_volume: None,
|
||||||
as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()),
|
as_of: Some(chrono::DateTime::parse_from_rfc3339(as_of).unwrap()),
|
||||||
behind_session: false,
|
behind_session: false,
|
||||||
|
bid: None,
|
||||||
|
offer: None,
|
||||||
|
market_status: None,
|
||||||
|
session_note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2116,6 +2116,27 @@ fn invalid_provider_env_honors_json_output() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn provider_flag_overrides_env() {
|
||||||
|
let quotes = run_success_json(
|
||||||
|
test_bin("provider-flag")
|
||||||
|
.env("IDX_PROVIDER", "msn")
|
||||||
|
.env("IDX_USE_MOCK_PROVIDER", "1")
|
||||||
|
.args([
|
||||||
|
"-o",
|
||||||
|
"json",
|
||||||
|
"--provider",
|
||||||
|
"stockbit",
|
||||||
|
"stocks",
|
||||||
|
"quote",
|
||||||
|
"BBCA",
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
// Only Stockbit reports the market's state; MSN has a market cap.
|
||||||
|
assert_eq!(quotes[0]["market_status"], "close");
|
||||||
|
assert_eq!(quotes[0]["market_cap"], Value::Null);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_quote_ttl_env_returns_non_zero() {
|
fn invalid_quote_ttl_env_returns_non_zero() {
|
||||||
test_bin("invalid-quote-ttl")
|
test_bin("invalid-quote-ttl")
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,25 @@ name = "yahoo-fundamental"
|
||||||
args = ["stocks", "fundamental", "BBCA"]
|
args = ["stocks", "fundamental", "BBCA"]
|
||||||
env = { IDX_PROVIDER = "yahoo" }
|
env = { IDX_PROVIDER = "yahoo" }
|
||||||
|
|
||||||
|
# --- stocks: Stockbit (intraday, and the opt-in quote provider) -----------------
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "stocks-intraday"
|
||||||
|
args = ["stocks", "intraday", "BBCA"]
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "stocks-intraday-index"
|
||||||
|
args = ["stocks", "intraday", "^JKSE"]
|
||||||
|
formats = ["table"]
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "stocks-intraday-suspended"
|
||||||
|
args = ["stocks", "intraday", "MABA"]
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "stockbit-quote"
|
||||||
|
args = ["--provider", "stockbit", "stocks", "quote", "BBCA,MABA"]
|
||||||
|
|
||||||
# --- stocks: broker summaries (IPOT) and foreign flow (IDX) --------------------
|
# --- stocks: broker summaries (IPOT) and foreign flow (IDX) --------------------
|
||||||
|
|
||||||
[[case]]
|
[[case]]
|
||||||
|
|
@ -334,6 +353,19 @@ env = { IDX_MOCK_ERROR = "1" }
|
||||||
name = "error-fetch-bing-unsupported"
|
name = "error-fetch-bing-unsupported"
|
||||||
args = ["ownership", "import", "--fetch-bing", "BBCA"]
|
args = ["ownership", "import", "--fetch-bing", "BBCA"]
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "error-intraday-blocked"
|
||||||
|
args = ["stocks", "intraday", "BBCA"]
|
||||||
|
env = { IDX_MOCK_STOCKBIT_STATUS = "403" }
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "error-intraday-unknown-symbol"
|
||||||
|
args = ["stocks", "intraday", "ZZZZ"]
|
||||||
|
|
||||||
|
[[case]]
|
||||||
|
name = "error-stockbit-fundamental-unsupported"
|
||||||
|
args = ["--provider", "stockbit", "stocks", "fundamental", "BBCA"]
|
||||||
|
|
||||||
# --- ownership --------------------------------------------------------------------
|
# --- ownership --------------------------------------------------------------------
|
||||||
|
|
||||||
[[case]]
|
[[case]]
|
||||||
|
|
|
||||||
|
|
@ -204,6 +204,9 @@ _idx() {
|
||||||
idx__help__stocks,insights)
|
idx__help__stocks,insights)
|
||||||
cmd="idx__help__stocks__insights"
|
cmd="idx__help__stocks__insights"
|
||||||
;;
|
;;
|
||||||
|
idx__help__stocks,intraday)
|
||||||
|
cmd="idx__help__stocks__intraday"
|
||||||
|
;;
|
||||||
idx__help__stocks,news)
|
idx__help__stocks,news)
|
||||||
cmd="idx__help__stocks__news"
|
cmd="idx__help__stocks__news"
|
||||||
;;
|
;;
|
||||||
|
|
@ -378,6 +381,9 @@ _idx() {
|
||||||
idx__stocks,insights)
|
idx__stocks,insights)
|
||||||
cmd="idx__stocks__insights"
|
cmd="idx__stocks__insights"
|
||||||
;;
|
;;
|
||||||
|
idx__stocks,intraday)
|
||||||
|
cmd="idx__stocks__intraday"
|
||||||
|
;;
|
||||||
idx__stocks,news)
|
idx__stocks,news)
|
||||||
cmd="idx__stocks__news"
|
cmd="idx__stocks__news"
|
||||||
;;
|
;;
|
||||||
|
|
@ -435,6 +441,9 @@ _idx() {
|
||||||
idx__stocks__help,insights)
|
idx__stocks__help,insights)
|
||||||
cmd="idx__stocks__help__insights"
|
cmd="idx__stocks__help__insights"
|
||||||
;;
|
;;
|
||||||
|
idx__stocks__help,intraday)
|
||||||
|
cmd="idx__stocks__help__intraday"
|
||||||
|
;;
|
||||||
idx__stocks__help,news)
|
idx__stocks__help,news)
|
||||||
cmd="idx__stocks__help__news"
|
cmd="idx__stocks__help__news"
|
||||||
;;
|
;;
|
||||||
|
|
@ -466,7 +475,7 @@ _idx() {
|
||||||
|
|
||||||
case "${cmd}" in
|
case "${cmd}" in
|
||||||
idx)
|
idx)
|
||||||
opts="-o -q -v -V -h --output --no-color --quiet --verbose --offline --no-cache --version --help stocks config cache ownership completions version help"
|
opts="-o -q -v -V -h --output --no-color --quiet --verbose --provider --offline --no-cache --version --help stocks config cache ownership completions version help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -480,6 +489,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -488,7 +501,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__cache)
|
idx__cache)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help info clear help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help info clear help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -502,6 +515,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -510,7 +527,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__cache__clear)
|
idx__cache__clear)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -524,6 +541,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -588,7 +609,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__cache__info)
|
idx__cache__info)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -602,6 +623,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -610,7 +635,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__completions)
|
idx__completions)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help bash zsh fish"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help bash zsh fish"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -624,6 +649,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -632,7 +661,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__config)
|
idx__config)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help init get set path help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help init get set path help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -646,6 +675,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -654,7 +687,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__config__get)
|
idx__config__get)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEY>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -668,6 +701,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -760,7 +797,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__config__init)
|
idx__config__init)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -774,6 +811,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -782,7 +823,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__config__path)
|
idx__config__path)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -796,6 +837,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -804,7 +849,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__config__set)
|
idx__config__set)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEY> <VALUE>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEY> <VALUE>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -818,6 +863,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1218,7 +1267,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__help__stocks)
|
idx__help__stocks)
|
||||||
opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare"
|
opts="quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1371,6 +1420,20 @@ _idx() {
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
idx__help__stocks__intraday)
|
||||||
|
opts=""
|
||||||
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "${prev}" in
|
||||||
|
*)
|
||||||
|
COMPREPLY=()
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
idx__help__stocks__news)
|
idx__help__stocks__news)
|
||||||
opts=""
|
opts=""
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
|
|
@ -1498,7 +1561,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership)
|
idx__ownership)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help discover import sync ticker entity search cross-holders concentration flow changes graph resolve releases help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help discover import sync ticker entity search cross-holders concentration flow changes graph resolve releases help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1512,6 +1575,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1520,7 +1587,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__changes)
|
idx__ownership__changes)
|
||||||
opts="-o -q -v -h --from --to --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --from --to --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1542,6 +1609,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1550,7 +1621,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__concentration)
|
idx__ownership__concentration)
|
||||||
opts="-o -q -v -h --by --top --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --by --top --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1572,6 +1643,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1580,7 +1655,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__cross__holders)
|
idx__ownership__cross__holders)
|
||||||
opts="-o -q -v -h --top --min-tickers --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --top --min-tickers --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1602,6 +1677,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1610,7 +1689,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__discover)
|
idx__ownership__discover)
|
||||||
opts="-o -q -v -h --family --limit --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --family --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1632,6 +1711,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1640,7 +1723,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__entity)
|
idx__ownership__entity)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <NAME>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <NAME>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1654,6 +1737,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1662,7 +1749,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__flow)
|
idx__ownership__flow)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1676,6 +1763,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1684,7 +1775,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__graph)
|
idx__ownership__graph)
|
||||||
opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --offline --no-cache --help <ROOT>"
|
opts="-o -q -v -h --depth --format --output --no-color --quiet --verbose --provider --offline --no-cache --help <ROOT>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1706,6 +1797,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -1966,7 +2061,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__import)
|
idx__ownership__import)
|
||||||
opts="-o -q -v -h --url --file --fetch-bing --force --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --url --file --fetch-bing --force --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -1992,6 +2087,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2000,7 +2099,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__releases)
|
idx__ownership__releases)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2014,6 +2113,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2022,7 +2125,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__resolve)
|
idx__ownership__resolve)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help unresolved map merge help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help unresolved map merge help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2036,6 +2139,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2114,7 +2221,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__resolve__map)
|
idx__ownership__resolve__map)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <ALIAS> <ENTITY>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <ALIAS> <ENTITY>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2128,6 +2235,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2136,7 +2247,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__resolve__merge)
|
idx__ownership__resolve__merge)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <KEEP> <MERGE>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <KEEP> <MERGE>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2150,6 +2261,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2158,7 +2273,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__resolve__unresolved)
|
idx__ownership__resolve__unresolved)
|
||||||
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2176,6 +2291,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2184,7 +2303,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__search)
|
idx__ownership__search)
|
||||||
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help <QUERY>"
|
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <QUERY>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2202,6 +2321,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2210,7 +2333,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__sync)
|
idx__ownership__sync)
|
||||||
opts="-o -q -v -h --manifest --force --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --manifest --force --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2228,6 +2351,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2236,7 +2363,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__ownership__ticker)
|
idx__ownership__ticker)
|
||||||
opts="-o -q -v -h --source --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2254,6 +2381,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2262,7 +2393,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks)
|
idx__stocks)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2276,6 +2407,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2284,7 +2419,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__bandar)
|
idx__stocks__bandar)
|
||||||
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..."
|
opts="-o -q -v -h --date --from --to --investor --board --top --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2322,6 +2457,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2330,7 +2469,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__broker)
|
idx__stocks__broker)
|
||||||
opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --date --from --to --investor --board --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2364,6 +2503,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2372,7 +2515,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__compare)
|
idx__stocks__compare)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..."
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2386,6 +2529,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2394,7 +2541,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__earnings)
|
idx__stocks__earnings)
|
||||||
opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --forecast --history --annual --quarterly --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2408,6 +2555,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2416,7 +2567,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__financials)
|
idx__stocks__financials)
|
||||||
opts="-o -q -v -h --statement --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --statement --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2434,6 +2585,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2442,7 +2597,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__flow)
|
idx__stocks__flow)
|
||||||
opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --days --from --to --source --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2472,6 +2627,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2480,7 +2639,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__fundamental)
|
idx__stocks__fundamental)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2494,6 +2653,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2502,7 +2665,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__growth)
|
idx__stocks__growth)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2516,6 +2679,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2524,7 +2691,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__help)
|
idx__stocks__help)
|
||||||
opts="quote history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
|
opts="quote intraday history technical growth valuation risk fundamental broker bandar flow profile financials earnings sentiment insights news screen compare help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2691,6 +2858,20 @@ _idx() {
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
idx__stocks__help__intraday)
|
||||||
|
opts=""
|
||||||
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "${prev}" in
|
||||||
|
*)
|
||||||
|
COMPREPLY=()
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
idx__stocks__help__news)
|
idx__stocks__help__news)
|
||||||
opts=""
|
opts=""
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 4 ]] ; then
|
||||||
|
|
@ -2804,7 +2985,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__history)
|
idx__stocks__history)
|
||||||
opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --period --interval --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2830,6 +3011,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2838,7 +3023,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__insights)
|
idx__stocks__insights)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2852,6 +3037,36 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
COMPREPLY=()
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
idx__stocks__intraday)
|
||||||
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
case "${prev}" in
|
||||||
|
--output)
|
||||||
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
-o)
|
||||||
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2860,7 +3075,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__news)
|
idx__stocks__news)
|
||||||
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2878,6 +3093,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2886,7 +3105,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__profile)
|
idx__stocks__profile)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2900,6 +3119,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2908,7 +3131,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__quote)
|
idx__stocks__quote)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOLS>..."
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOLS>..."
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2922,6 +3145,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2930,7 +3157,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__risk)
|
idx__stocks__risk)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2944,6 +3171,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2952,7 +3183,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__screen)
|
idx__stocks__screen)
|
||||||
opts="-o -q -v -h --filter --region --limit --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --filter --region --limit --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -2978,6 +3209,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -2986,7 +3221,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__sentiment)
|
idx__stocks__sentiment)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -3000,6 +3235,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -3008,7 +3247,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__technical)
|
idx__stocks__technical)
|
||||||
opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --history-provider --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -3026,6 +3265,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -3034,7 +3277,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__stocks__valuation)
|
idx__stocks__valuation)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help <SYMBOL>"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help <SYMBOL>"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -3048,6 +3291,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
@ -3056,7 +3303,7 @@ _idx() {
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
idx__version)
|
idx__version)
|
||||||
opts="-o -q -v -h --output --no-color --quiet --verbose --offline --no-cache --help"
|
opts="-o -q -v -h --output --no-color --quiet --verbose --provider --offline --no-cache --help"
|
||||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
|
||||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||||
return 0
|
return 0
|
||||||
|
|
@ -3070,6 +3317,10 @@ _idx() {
|
||||||
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
COMPREPLY=($(compgen -W "auto table json jsonl" -- "${cur}"))
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
|
--provider)
|
||||||
|
COMPREPLY=($(compgen -W "yahoo msn stockbit" -- "${cur}"))
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||
|
|
@ -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
|
exit: 2
|
||||||
--- stdout
|
--- stdout
|
||||||
--- stderr
|
--- stderr
|
||||||
{"code":"CONFIGERROR","error":true,"message":"config error: invalid provider 'bogus' (expected yahoo or msn)"}
|
{"code":"CONFIGERROR","error":true,"message":"config error: invalid provider 'bogus' (expected yahoo, msn, or stockbit)"}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
exit: 2
|
exit: 2
|
||||||
--- stdout
|
--- stdout
|
||||||
--- stderr
|
--- stderr
|
||||||
Error: config error: invalid provider 'bogus' (expected yahoo or msn)
|
Error: config error: invalid provider 'bogus' (expected yahoo, msn, or stockbit)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -14,6 +14,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
quote Get real-time stock quotes
|
quote Get real-time stock quotes
|
||||||
|
intraday Today's session price by the minute (Stockbit)
|
||||||
history Get historical OHLC data
|
history Get historical OHLC data
|
||||||
technical Run technical analysis on a stock
|
technical Run technical analysis on a stock
|
||||||
growth Run growth analysis on a stock
|
growth Run growth analysis on a stock
|
||||||
|
|
@ -30,6 +31,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
|
||||||
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
|
//! Fixture-backed provider for tests, enabled by the `testing` feature and
|
||||||
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
|
//! `IDX_USE_MOCK_PROVIDER`. Release builds do not contain it.
|
||||||
|
|
||||||
use super::types::{Bar, Fundamentals, Interval, Period, Quote};
|
use super::types::{Bar, Fundamentals, Interval, Intraday, Period, Quote};
|
||||||
use super::{
|
use super::{
|
||||||
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, QuoteProvider,
|
BrokerSummaryProvider, FundamentalsProvider, HistoryProvider, IntradayProvider, QuoteProvider,
|
||||||
TradingSummaryProvider, idx, ipot, msn, yahoo,
|
TradingSummaryProvider, idx, ipot, msn, stockbit, yahoo,
|
||||||
};
|
};
|
||||||
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
|
use crate::broker::types::{BrokerBoard, BrokerInvestor, BrokerQuery};
|
||||||
use crate::config::ProviderKind;
|
use crate::config::ProviderKind;
|
||||||
|
|
@ -45,6 +45,10 @@ impl MockProvider {
|
||||||
match provider {
|
match provider {
|
||||||
ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose),
|
ProviderKind::Yahoo => Self::from_yahoo_fixtures(history_verbose),
|
||||||
ProviderKind::Msn => Self::from_msn_fixtures(),
|
ProviderKind::Msn => Self::from_msn_fixtures(),
|
||||||
|
// `MockStockbitProvider` serves Stockbit; it has no history.
|
||||||
|
ProviderKind::Stockbit => Self::with_error(IdxError::Unsupported(
|
||||||
|
"Stockbit serves no price history".to_string(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +135,57 @@ impl HistoryProvider for MockProvider {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stockbit symbol pages from `tests/fixtures/stockbit_symbol_<code>.html`;
|
||||||
|
/// a code without a fixture is Stockbit's 404. `IDX_MOCK_STOCKBIT_STATUS`
|
||||||
|
/// (e.g. `403`) answers that HTTP status instead, and `IDX_MOCK_ERROR` fails
|
||||||
|
/// as for the other providers.
|
||||||
|
pub struct MockStockbitProvider;
|
||||||
|
|
||||||
|
impl MockStockbitProvider {
|
||||||
|
fn snapshot(&self, symbol: &str) -> Result<Intraday, IdxError> {
|
||||||
|
if std::env::var("IDX_MOCK_ERROR").is_ok() {
|
||||||
|
return Err(IdxError::ProviderUnavailable);
|
||||||
|
}
|
||||||
|
if let Ok(status) = std::env::var("IDX_MOCK_STOCKBIT_STATUS") {
|
||||||
|
return Err(stockbit::status_error(
|
||||||
|
status.parse().unwrap_or(500),
|
||||||
|
symbol,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let code = stockbit::stockbit_code(symbol)?;
|
||||||
|
let path = format!(
|
||||||
|
"tests/fixtures/stockbit_symbol_{}.html",
|
||||||
|
code.to_ascii_lowercase()
|
||||||
|
);
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(html) => stockbit::parse_symbol_page(symbol, &html),
|
||||||
|
Err(_) => Err(stockbit::status_error(404, symbol)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuoteProvider for MockStockbitProvider {
|
||||||
|
fn quote(&self, symbol: &str) -> Result<Quote, IdxError> {
|
||||||
|
let mut quote = self.snapshot(symbol)?.quote;
|
||||||
|
// Quotes are judged against the clock (behind the session, a week
|
||||||
|
// old); a fixed trade time would make their output age.
|
||||||
|
quote.as_of = None;
|
||||||
|
Ok(quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FundamentalsProvider for MockStockbitProvider {
|
||||||
|
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError> {
|
||||||
|
Err(stockbit::fundamentals_unsupported(symbol))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntradayProvider for MockStockbitProvider {
|
||||||
|
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError> {
|
||||||
|
self.snapshot(symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from>
|
/// Broker summaries from `tests/fixtures/ipot_brokersummary_<code>_<from>
|
||||||
/// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an
|
/// [_<to>][_<investor>_<board>].html`; a query without a fixture reads as an
|
||||||
/// unknown code.
|
/// unknown code.
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ pub mod ipot;
|
||||||
#[cfg(feature = "testing")]
|
#[cfg(feature = "testing")]
|
||||||
pub mod mock;
|
pub mod mock;
|
||||||
pub mod msn;
|
pub mod msn;
|
||||||
|
pub mod stockbit;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod yahoo;
|
pub mod yahoo;
|
||||||
|
|
||||||
|
|
@ -13,7 +14,7 @@ use crate::config::{HistoryProviderKind, ProviderKind};
|
||||||
use crate::error::IdxError;
|
use crate::error::IdxError;
|
||||||
use types::{
|
use types::{
|
||||||
Bar, CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval,
|
Bar, CompanyProfile, EarningsReport, FinancialStatements, Fundamentals, InsightData, Interval,
|
||||||
NewsItem, Period, Quote, SentimentData,
|
Intraday, NewsItem, Period, Quote, SentimentData,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// IDX trades in Western Indonesia Time (WIB, UTC+7, no DST). Providers stamp
|
/// IDX trades in Western Indonesia Time (WIB, UTC+7, no DST). Providers stamp
|
||||||
|
|
@ -64,6 +65,13 @@ pub trait TradingSummaryProvider {
|
||||||
) -> Result<Vec<crate::flow::types::StockDay>, IdxError>;
|
) -> Result<Vec<crate::flow::types::StockDay>, IdxError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Today's session of one IDX symbol: quote and price per minute. Served by
|
||||||
|
/// Stockbit's public page whichever market provider is selected.
|
||||||
|
pub trait IntradayProvider {
|
||||||
|
/// `symbol` is resolved, e.g. `BBCA.JK`.
|
||||||
|
fn intraday(&self, symbol: &str) -> Result<Intraday, IdxError>;
|
||||||
|
}
|
||||||
|
|
||||||
pub trait FundamentalsProvider {
|
pub trait FundamentalsProvider {
|
||||||
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
|
fn fundamentals(&self, symbol: &str) -> Result<Fundamentals, IdxError>;
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +229,10 @@ fn mock_enabled() -> bool {
|
||||||
|
|
||||||
#[cfg(feature = "testing")]
|
#[cfg(feature = "testing")]
|
||||||
fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
|
fn mock_market(provider: ProviderKind) -> Option<Box<dyn MarketDataProvider>> {
|
||||||
Some(Box::new(mock::MockProvider::from_fixtures(provider)))
|
match provider {
|
||||||
|
ProviderKind::Stockbit => Some(Box::new(mock::MockStockbitProvider)),
|
||||||
|
_ => Some(Box::new(mock::MockProvider::from_fixtures(provider))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "testing"))]
|
#[cfg(not(feature = "testing"))]
|
||||||
|
|
@ -274,6 +285,18 @@ fn build_selected_provider(
|
||||||
news: Some(Box::new(msn::MsnProvider::new(verbose))),
|
news: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||||
screener: Some(Box::new(msn::MsnProvider::new(verbose))),
|
screener: Some(Box::new(msn::MsnProvider::new(verbose))),
|
||||||
},
|
},
|
||||||
|
// Quotes only; history falls back to Yahoo (see `history_source`).
|
||||||
|
ProviderKind::Stockbit => SelectedProvider {
|
||||||
|
kind: ProviderKind::Stockbit,
|
||||||
|
market: mock.unwrap_or_else(|| Box::new(stockbit::StockbitProvider::new())),
|
||||||
|
profile: None,
|
||||||
|
financials: None,
|
||||||
|
earnings: None,
|
||||||
|
sentiment: None,
|
||||||
|
insights: None,
|
||||||
|
news: None,
|
||||||
|
screener: None,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -297,6 +320,14 @@ pub fn trading_summary_provider() -> Box<dyn TradingSummaryProvider> {
|
||||||
Box::new(idx::IdxSiteProvider)
|
Box::new(idx::IdxSiteProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn intraday_provider() -> Box<dyn IntradayProvider> {
|
||||||
|
#[cfg(feature = "testing")]
|
||||||
|
if mock::enabled() {
|
||||||
|
return Box::new(mock::MockStockbitProvider);
|
||||||
|
}
|
||||||
|
Box::new(stockbit::StockbitProvider::new())
|
||||||
|
}
|
||||||
|
|
||||||
/// Today in WIB, the date IDX trades on.
|
/// Today in WIB, the date IDX trades on.
|
||||||
pub(crate) fn today_wib() -> chrono::NaiveDate {
|
pub(crate) fn today_wib() -> chrono::NaiveDate {
|
||||||
#[cfg(feature = "testing")]
|
#[cfg(feature = "testing")]
|
||||||
|
|
@ -312,8 +343,7 @@ pub fn history_source(provider: ProviderKind, history_mode: HistoryProviderKind)
|
||||||
HistoryProviderKind::Yahoo => ProviderKind::Yahoo,
|
HistoryProviderKind::Yahoo => ProviderKind::Yahoo,
|
||||||
HistoryProviderKind::Msn => ProviderKind::Msn,
|
HistoryProviderKind::Msn => ProviderKind::Msn,
|
||||||
HistoryProviderKind::Auto => match provider {
|
HistoryProviderKind::Auto => match provider {
|
||||||
ProviderKind::Yahoo => ProviderKind::Yahoo,
|
ProviderKind::Yahoo | ProviderKind::Msn | ProviderKind::Stockbit => ProviderKind::Yahoo,
|
||||||
ProviderKind::Msn => ProviderKind::Yahoo,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +369,9 @@ pub fn history_provider(
|
||||||
match resolved {
|
match resolved {
|
||||||
ProviderKind::Yahoo => Ok((resolved, Box::new(yahoo::YahooProvider::new(verbose)))),
|
ProviderKind::Yahoo => Ok((resolved, Box::new(yahoo::YahooProvider::new(verbose)))),
|
||||||
ProviderKind::Msn => Ok((resolved, Box::new(msn::MsnProvider::new(verbose)))),
|
ProviderKind::Msn => Ok((resolved, Box::new(msn::MsnProvider::new(verbose)))),
|
||||||
|
ProviderKind::Stockbit => Err(IdxError::Unsupported(
|
||||||
|
"Stockbit serves no price history; use --history-provider yahoo or msn".to_string(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ pub(super) fn parse_quote(symbol: &str, quotes: &[MsnQuote]) -> Result<Quote, Id
|
||||||
avg_volume: round_u64(quote.average_volume),
|
avg_volume: round_u64(quote.average_volume),
|
||||||
as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()),
|
as_of: crate::api::parse_as_of(quote.time_last_traded.as_deref()),
|
||||||
behind_session: false,
|
behind_session: false,
|
||||||
|
bid: None,
|
||||||
|
offer: None,
|
||||||
|
market_status: None,
|
||||||
|
session_note: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -720,6 +724,10 @@ pub(super) fn parse_screener_results(raw: &RawScreenerResponse) -> Result<Vec<Qu
|
||||||
avg_volume: round_u64(q.average_volume),
|
avg_volume: round_u64(q.average_volume),
|
||||||
as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()),
|
as_of: crate::api::parse_as_of(q.time_last_traded.as_deref()),
|
||||||
behind_session: false,
|
behind_session: false,
|
||||||
|
bid: None,
|
||||||
|
offer: None,
|
||||||
|
market_status: None,
|
||||||
|
session_note: None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
||||||
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.
|
/// quote is returned, not when it is cached.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub behind_session: bool,
|
pub behind_session: bool,
|
||||||
|
/// Best bid in IDR (whole Rupiah). Stockbit only, and only while its
|
||||||
|
/// market status is `open`: outside trading it is the closing book, not
|
||||||
|
/// a live one. Absent from JSON when unknown.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub bid: Option<i64>,
|
||||||
|
/// Best offer in IDR (whole Rupiah), gated like `bid`.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub offer: Option<i64>,
|
||||||
|
/// The provider's own session state, verbatim (Stockbit
|
||||||
|
/// `market_hour.status`, e.g. `close`). Absent for providers without one.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub market_status: Option<String>,
|
||||||
|
/// What the provider says about this symbol's session: suspended,
|
||||||
|
/// delisted, or no trades. Never inferred.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub session_note: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One price of today's session, minute by minute (Stockbit's chart).
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct IntradayPoint {
|
||||||
|
/// The minute, in WIB.
|
||||||
|
pub time: DateTime<FixedOffset>,
|
||||||
|
/// Last price at that minute in IDR; indices carry two decimals.
|
||||||
|
pub price: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The latest session of one IDX symbol: its quote and the price per minute.
|
||||||
|
/// Stockbit's public page carries no per-minute OHLC or volume.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Intraday {
|
||||||
|
pub quote: Quote,
|
||||||
|
/// Oldest first; empty when the symbol did not trade (suspended,
|
||||||
|
/// delisted, or no session yet), never an error.
|
||||||
|
pub points: Vec<IntradayPoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// OHLC candle data normalized from Yahoo Finance chart indicators.
|
/// OHLC candle data normalized from Yahoo Finance chart indicators.
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,10 @@ pub(super) fn parse_quote(symbol: &str, chart: &ChartResponse) -> Result<Quote,
|
||||||
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
|
.and_then(|ts| chrono::DateTime::from_timestamp(ts, 0))
|
||||||
.map(|t| t.with_timezone(&crate::api::idx_offset())),
|
.map(|t| t.with_timezone(&crate::api::idx_offset())),
|
||||||
behind_session: false,
|
behind_session: false,
|
||||||
|
bid: None,
|
||||||
|
offer: None,
|
||||||
|
market_status: None,
|
||||||
|
session_note: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ impl CachePolicy {
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct Fetched<T> {
|
pub struct Fetched<T> {
|
||||||
pub data: T,
|
pub data: T,
|
||||||
/// Provider that produced the data (`msn` or `yahoo`).
|
/// Provider that produced the data (e.g. `msn`, `yahoo`, `ipot`, `stockbit`).
|
||||||
pub source: &'static str,
|
pub source: &'static str,
|
||||||
pub fetched_at: DateTime<Utc>,
|
pub fetched_at: DateTime<Utc>,
|
||||||
/// True when an expired cache entry was served (offline, or after a
|
/// True when an expired cache entry was served (offline, or after a
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,14 @@ use crate::error::IdxError;
|
||||||
pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"];
|
pub const OUTPUT_VALUES: &[&str] = &["auto", "table", "json", "jsonl"];
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[cfg_attr(feature = "clap", value(rename_all = "lower"))]
|
||||||
pub enum ProviderKind {
|
pub enum ProviderKind {
|
||||||
Yahoo,
|
Yahoo,
|
||||||
Msn,
|
Msn,
|
||||||
|
// Opt-in: quotes from Stockbit's public page (delayed, no login).
|
||||||
|
Stockbit,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
|
||||||
|
|
@ -34,6 +38,7 @@ impl ProviderKind {
|
||||||
match self {
|
match self {
|
||||||
Self::Yahoo => "yahoo",
|
Self::Yahoo => "yahoo",
|
||||||
Self::Msn => "msn",
|
Self::Msn => "msn",
|
||||||
|
Self::Stockbit => "stockbit",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,9 +47,11 @@ impl ProviderKind {
|
||||||
Ok(Self::Yahoo)
|
Ok(Self::Yahoo)
|
||||||
} else if value.eq_ignore_ascii_case("msn") {
|
} else if value.eq_ignore_ascii_case("msn") {
|
||||||
Ok(Self::Msn)
|
Ok(Self::Msn)
|
||||||
|
} else if value.eq_ignore_ascii_case("stockbit") {
|
||||||
|
Ok(Self::Stockbit)
|
||||||
} else {
|
} else {
|
||||||
Err(IdxError::ConfigError(format!(
|
Err(IdxError::ConfigError(format!(
|
||||||
"invalid provider '{value}' (expected yahoo or msn)"
|
"invalid provider '{value}' (expected yahoo, msn, or stockbit)"
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ pub enum IdxError {
|
||||||
InvalidInput(String),
|
InvalidInput(String),
|
||||||
#[error("no market data: {0}")]
|
#[error("no market data: {0}")]
|
||||||
NoMarketData(String),
|
NoMarketData(String),
|
||||||
|
/// The upstream refused this client outright (e.g. a CDN's HTTP 403
|
||||||
|
/// block page). Retrying does not help; the message says what might.
|
||||||
|
#[error("upstream blocked: {0}")]
|
||||||
|
UpstreamBlocked(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||||
|
|
@ -53,6 +57,7 @@ pub enum ErrorCode {
|
||||||
PdfParseError,
|
PdfParseError,
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
NoMarketData,
|
NoMarketData,
|
||||||
|
UpstreamBlocked,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`.
|
/// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`.
|
||||||
|
|
@ -80,6 +85,7 @@ impl IdxError {
|
||||||
Self::PdfParseError(_) => ErrorCode::PdfParseError,
|
Self::PdfParseError(_) => ErrorCode::PdfParseError,
|
||||||
Self::InvalidInput(_) => ErrorCode::InvalidInput,
|
Self::InvalidInput(_) => ErrorCode::InvalidInput,
|
||||||
Self::NoMarketData(_) => ErrorCode::NoMarketData,
|
Self::NoMarketData(_) => ErrorCode::NoMarketData,
|
||||||
|
Self::UpstreamBlocked(_) => ErrorCode::UpstreamBlocked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,7 +99,8 @@ impl IdxError {
|
||||||
| ErrorCode::RateLimited
|
| ErrorCode::RateLimited
|
||||||
| ErrorCode::ProviderUnavailable
|
| ErrorCode::ProviderUnavailable
|
||||||
| ErrorCode::Offline
|
| ErrorCode::Offline
|
||||||
| ErrorCode::AuthError => 4,
|
| ErrorCode::AuthError
|
||||||
|
| ErrorCode::UpstreamBlocked => 4,
|
||||||
ErrorCode::Unsupported => 5,
|
ErrorCode::Unsupported => 5,
|
||||||
ErrorCode::Io
|
ErrorCode::Io
|
||||||
| ErrorCode::ParseError
|
| ErrorCode::ParseError
|
||||||
|
|
@ -119,4 +126,11 @@ mod tests {
|
||||||
let unsupported = IdxError::Unsupported("history unavailable".to_string());
|
let unsupported = IdxError::Unsupported("history unavailable".to_string());
|
||||||
assert_eq!(unsupported.code(), ErrorCode::Unsupported);
|
assert_eq!(unsupported.code(), ErrorCode::Unsupported);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn upstream_blocked_has_its_own_wire_code_and_network_exit() {
|
||||||
|
let blocked = IdxError::UpstreamBlocked("stockbit.com: HTTP 403".to_string());
|
||||||
|
assert_eq!(blocked.code().to_string(), "UPSTREAMBLOCKED");
|
||||||
|
assert_eq!(blocked.exit_code(), 4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday};
|
use chrono::{DateTime, Datelike, Duration, NaiveDate, NaiveTime, Utc, Weekday};
|
||||||
|
|
||||||
use crate::api::types::{Interval, Ohlc, Period, Quote};
|
use crate::api::types::{Interval, Intraday, Ohlc, Period, Quote};
|
||||||
use crate::api::{history_provider, history_source, idx_offset};
|
use crate::api::{history_provider, history_source, idx_offset, intraday_provider, stockbit};
|
||||||
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
|
use crate::client::{CachePolicy, CacheSpec, Fetched, Idx, cached_fetch};
|
||||||
use crate::config::{HistoryProviderKind, ProviderKind};
|
use crate::config::{HistoryProviderKind, ProviderKind};
|
||||||
use crate::error::IdxError;
|
use crate::error::IdxError;
|
||||||
|
|
@ -34,6 +34,10 @@ impl Market<'_> {
|
||||||
) -> Result<Vec<Fetched<Quote>>, IdxError> {
|
) -> Result<Vec<Fetched<Quote>>, IdxError> {
|
||||||
let idx = self.idx;
|
let idx = self.idx;
|
||||||
let bucket = idx.bucket("quote");
|
let bucket = idx.bucket("quote");
|
||||||
|
let ttl_secs = match idx.provider.kind() {
|
||||||
|
ProviderKind::Stockbit => idx.config.quote_ttl.min(stockbit::CACHE_TTL_SECS),
|
||||||
|
_ => idx.config.quote_ttl,
|
||||||
|
};
|
||||||
let session = latest_session(Utc::now());
|
let session = latest_session(Utc::now());
|
||||||
let mut quotes = Vec::with_capacity(symbols.len());
|
let mut quotes = Vec::with_capacity(symbols.len());
|
||||||
for symbol in symbols {
|
for symbol in symbols {
|
||||||
|
|
@ -43,7 +47,7 @@ impl Market<'_> {
|
||||||
CacheSpec {
|
CacheSpec {
|
||||||
bucket: &bucket,
|
bucket: &bucket,
|
||||||
key: &resolved,
|
key: &resolved,
|
||||||
ttl_secs: idx.config.quote_ttl,
|
ttl_secs,
|
||||||
subject: &resolved,
|
subject: &resolved,
|
||||||
source: idx.provider.kind().as_str(),
|
source: idx.provider.kind().as_str(),
|
||||||
},
|
},
|
||||||
|
|
@ -60,6 +64,32 @@ impl Market<'_> {
|
||||||
Ok(quotes)
|
Ok(quotes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Today's session of one IDX symbol from Stockbit's public page,
|
||||||
|
/// whichever market provider is selected: the quote (bid/offer only while
|
||||||
|
/// the market is open) and the price per minute.
|
||||||
|
pub fn intraday(
|
||||||
|
&self,
|
||||||
|
symbol: &str,
|
||||||
|
policy: CachePolicy,
|
||||||
|
) -> Result<Fetched<Intraday>, IdxError> {
|
||||||
|
let idx = self.idx;
|
||||||
|
let resolved = idx.resolve(symbol)?;
|
||||||
|
let provider = intraday_provider();
|
||||||
|
cached_fetch(
|
||||||
|
&idx.cache,
|
||||||
|
CacheSpec {
|
||||||
|
bucket: "stockbit-intraday",
|
||||||
|
key: &resolved,
|
||||||
|
ttl_secs: idx.config.quote_ttl.min(stockbit::CACHE_TTL_SECS),
|
||||||
|
subject: &resolved,
|
||||||
|
source: stockbit::SOURCE,
|
||||||
|
},
|
||||||
|
policy,
|
||||||
|
|| provider.intraday(&resolved),
|
||||||
|
Ok,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolves the history source for an optional `--history-provider`
|
/// Resolves the history source for an optional `--history-provider`
|
||||||
/// override, without fetching.
|
/// override, without fetching.
|
||||||
pub fn history_source(&self, mode: Option<HistoryProviderKind>) -> HistorySource {
|
pub fn history_source(&self, mode: Option<HistoryProviderKind>) -> HistorySource {
|
||||||
|
|
@ -380,6 +410,10 @@ mod tests {
|
||||||
avg_volume: None,
|
avg_volume: None,
|
||||||
as_of: None,
|
as_of: None,
|
||||||
behind_session: false,
|
behind_session: false,
|
||||||
|
bid: None,
|
||||||
|
offer: None,
|
||||||
|
market_status: None,
|
||||||
|
session_note: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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>,
|
pub interval: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
|
pub struct IntradayParams {
|
||||||
|
/// IDX ticker, e.g. "BBCA"; indices as "^JKSE" or "IHSG".
|
||||||
|
pub symbol: String,
|
||||||
|
/// Only the latest N minutes (default: the whole session, at most 400).
|
||||||
|
#[serde(default)]
|
||||||
|
pub tail: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, JsonSchema)]
|
#[derive(Debug, Deserialize, JsonSchema)]
|
||||||
pub struct ScreenParams {
|
pub struct ScreenParams {
|
||||||
/// Screen to run (default "top-performers").
|
/// Screen to run (default "top-performers").
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use crate::params::{
|
use crate::params::{
|
||||||
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
|
AnalysisKind, AnalyzeParams, BandarParams, BrokerParams, ChangesParams, CompanyParams,
|
||||||
CompanySection, EntityParams, FlowParams, HistoryParams, ScreenParams, SearchParams,
|
CompanySection, EntityParams, FlowParams, HistoryParams, IntradayParams, ScreenParams,
|
||||||
ShareholdersParams, SymbolsParams, SyncParams,
|
SearchParams, ShareholdersParams, SymbolsParams, SyncParams,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SYNC_TOOL: &str = "ownership_sync";
|
const SYNC_TOOL: &str = "ownership_sync";
|
||||||
|
|
@ -43,6 +43,8 @@ const MAX_SEARCH_LIMIT: usize = 100;
|
||||||
// Pages above a few hundred rows (~40 KB) overflow some agents' tool output.
|
// Pages above a few hundred rows (~40 KB) overflow some agents' tool output.
|
||||||
const MAX_CHANGES_LIMIT: usize = 300;
|
const MAX_CHANGES_LIMIT: usize = 300;
|
||||||
const CHANGES_TOP_TICKERS: usize = 10;
|
const CHANGES_TOP_TICKERS: usize = 10;
|
||||||
|
// A session has about 340 minutes, so this is the whole day.
|
||||||
|
const MAX_INTRADAY_POINTS: usize = 400;
|
||||||
|
|
||||||
/// Tool calls running at once; the rest wait their turn.
|
/// Tool calls running at once; the rest wait their turn.
|
||||||
static CALL_SLOTS: Semaphore = Semaphore::const_new(4);
|
static CALL_SLOTS: Semaphore = Semaphore::const_new(4);
|
||||||
|
|
@ -117,7 +119,11 @@ impl IdxServer {
|
||||||
description = "Latest quotes (price, change, volume, 52-week range, market cap) \
|
description = "Latest quotes (price, change, volume, 52-week range, market cap) \
|
||||||
for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true \
|
for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true \
|
||||||
when that is before the latest IDX session, so the price is not that session's close: the \
|
when that is before the latest IDX session, so the price is not that session's close: the \
|
||||||
provider lags, the stock did not trade, or the day was an exchange holiday."
|
provider lags, the stock did not trade, or the day was an exchange holiday. When the server \
|
||||||
|
runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. \
|
||||||
|
\"open\", \"close\"), `bid`/`offer` (best price, only while the market is open) and \
|
||||||
|
`session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 \
|
||||||
|
minutes."
|
||||||
)]
|
)]
|
||||||
async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult {
|
async fn get_quotes(&self, params: Parameters<SymbolsParams>) -> ToolResult {
|
||||||
let symbols = match checked_symbols(params.0.symbols) {
|
let symbols = match checked_symbols(params.0.symbols) {
|
||||||
|
|
@ -131,6 +137,27 @@ provider lags, the stock did not trade, or the day was an exchange holiday."
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tool(
|
||||||
|
description = "Today's session of one IDX stock or index from Stockbit's public page \
|
||||||
|
(no login), whichever provider the server uses: `quote` (price, change, volume, \
|
||||||
|
`market_status`, `bid`/`offer` only while the market is open, `session_note`) and \
|
||||||
|
`points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no \
|
||||||
|
per-minute volume. Empty `points` means the stock did not trade this session (see \
|
||||||
|
`session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine \
|
||||||
|
(HTTP 403): do not retry at once."
|
||||||
|
)]
|
||||||
|
async fn get_intraday(&self, params: Parameters<IntradayParams>) -> ToolResult {
|
||||||
|
let IntradayParams { symbol, tail } = params.0;
|
||||||
|
self.live(move |idx| {
|
||||||
|
let tail = bounded_limit("tail", tail, MAX_INTRADAY_POINTS, MAX_INTRADAY_POINTS)?;
|
||||||
|
let mut day = idx.market().intraday(&symbol, CachePolicy::Default)?;
|
||||||
|
let points = &mut day.data.points;
|
||||||
|
points.drain(..points.len().saturating_sub(tail));
|
||||||
|
Ok(day)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[tool(description = "OHLCV price history for one IDX ticker.")]
|
#[tool(description = "OHLCV price history for one IDX ticker.")]
|
||||||
async fn get_price_history(&self, params: Parameters<HistoryParams>) -> ToolResult {
|
async fn get_price_history(&self, params: Parameters<HistoryParams>) -> ToolResult {
|
||||||
let HistoryParams {
|
let HistoryParams {
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,33 @@
|
||||||
},
|
},
|
||||||
"name": "get_foreign_flow"
|
"name": "get_foreign_flow"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Today's session of one IDX stock or index from Stockbit's public page (no login), whichever provider the server uses: `quote` (price, change, volume, `market_status`, `bid`/`offer` only while the market is open, `session_note`) and `points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no per-minute volume. Empty `points` means the stock did not trade this session (see `session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once.",
|
||||||
|
"inputSchema": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"properties": {
|
||||||
|
"symbol": {
|
||||||
|
"description": "IDX ticker, e.g. \"BBCA\"; indices as \"^JKSE\" or \"IHSG\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tail": {
|
||||||
|
"default": null,
|
||||||
|
"description": "Only the latest N minutes (default: the whole session, at most 400).",
|
||||||
|
"format": "uint",
|
||||||
|
"minimum": 0,
|
||||||
|
"type": [
|
||||||
|
"integer",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"symbol"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"name": "get_intraday"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Every IDX holding (above 1%) of one investor, matched by name.",
|
"description": "Every IDX holding (above 1%) of one investor, matched by name.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
|
|
@ -397,7 +424,7 @@
|
||||||
"name": "get_price_history"
|
"name": "get_price_history"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday.",
|
"description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. When the server runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. \"open\", \"close\"), `bid`/`offer` (best price, only while the market is open) and `session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 minutes.",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,14 @@ use idx_core::config::CoreConfig;
|
||||||
use idx_core::investor::{ImportSource, Investor};
|
use idx_core::investor::{ImportSource, Investor};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
const TOOLS: [&str; 14] = [
|
const TOOLS: [&str; 15] = [
|
||||||
"analyze_bandar",
|
"analyze_bandar",
|
||||||
"analyze_stock",
|
"analyze_stock",
|
||||||
"compare_stocks",
|
"compare_stocks",
|
||||||
"get_broker_summary",
|
"get_broker_summary",
|
||||||
"get_company",
|
"get_company",
|
||||||
"get_foreign_flow",
|
"get_foreign_flow",
|
||||||
|
"get_intraday",
|
||||||
"get_investor_holdings",
|
"get_investor_holdings",
|
||||||
"get_ownership_changes",
|
"get_ownership_changes",
|
||||||
"get_price_history",
|
"get_price_history",
|
||||||
|
|
@ -36,6 +37,10 @@ struct Client {
|
||||||
|
|
||||||
impl Client {
|
impl Client {
|
||||||
fn start(args: &[&str], seed: bool) -> Self {
|
fn start(args: &[&str], seed: bool) -> Self {
|
||||||
|
Self::start_with_env(args, seed, &[])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_with_env(args: &[&str], seed: bool, env: &[(&str, &str)]) -> Self {
|
||||||
let home = tempdir::Dir::new();
|
let home = tempdir::Dir::new();
|
||||||
if seed {
|
if seed {
|
||||||
seed_ownership_db(&home.path().join("data/idx/ownership.db"));
|
seed_ownership_db(&home.path().join("data/idx/ownership.db"));
|
||||||
|
|
@ -50,6 +55,7 @@ impl Client {
|
||||||
.env("XDG_CACHE_HOME", home.path().join("cache"))
|
.env("XDG_CACHE_HOME", home.path().join("cache"))
|
||||||
.env("XDG_DATA_HOME", home.path().join("data"))
|
.env("XDG_DATA_HOME", home.path().join("data"))
|
||||||
.env("IDX_USE_MOCK_PROVIDER", "1")
|
.env("IDX_USE_MOCK_PROVIDER", "1")
|
||||||
|
.envs(env.iter().copied())
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::null())
|
.stderr(Stdio::null())
|
||||||
|
|
@ -299,6 +305,63 @@ fn live_tools_wrap_data_with_freshness() {
|
||||||
assert_eq!(compared["failures"], json!([]));
|
assert_eq!(compared["failures"], json!([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn intraday_is_stockbit_whichever_provider_and_empty_sessions_are_data() {
|
||||||
|
let mut client = Client::start(&[], false);
|
||||||
|
|
||||||
|
let day = client.ok("get_intraday", json!({"symbol": "BBCA"}));
|
||||||
|
assert_fetched(&day);
|
||||||
|
assert_eq!(day["source"], "stockbit");
|
||||||
|
let quote = &day["data"]["quote"];
|
||||||
|
assert_eq!(quote["symbol"], "BBCA.JK");
|
||||||
|
assert_eq!(quote["market_status"], "close");
|
||||||
|
// The closing book is not reported as a live one.
|
||||||
|
assert!(
|
||||||
|
quote.get("bid").is_none() && quote.get("offer").is_none(),
|
||||||
|
"{quote}"
|
||||||
|
);
|
||||||
|
assert_eq!(day["data"]["points"].as_array().unwrap().len(), 335);
|
||||||
|
|
||||||
|
let tail = client.ok("get_intraday", json!({"symbol": "BBCA", "tail": 2}));
|
||||||
|
let points = tail["data"]["points"].as_array().unwrap();
|
||||||
|
assert_eq!(points.len(), 2);
|
||||||
|
assert_eq!(points[1]["time"], "2026-10-22T16:14:00+07:00");
|
||||||
|
|
||||||
|
let suspended = client.ok("get_intraday", json!({"symbol": "MABA"}));
|
||||||
|
assert_eq!(suspended["data"]["points"], json!([]));
|
||||||
|
assert_eq!(
|
||||||
|
suspended["data"]["quote"]["session_note"],
|
||||||
|
"Stockbit lists the stock as suspended"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (arguments, code) in [
|
||||||
|
(json!({"symbol": "ZZZZ"}), "SYMBOLNOTFOUND"),
|
||||||
|
(json!({"symbol": "BBCA", "tail": 0}), "INVALIDINPUT"),
|
||||||
|
(json!({"symbol": "BBCA", "tail": 401}), "INVALIDINPUT"),
|
||||||
|
] {
|
||||||
|
let (is_error, body) = client.call("get_intraday", arguments.clone());
|
||||||
|
assert!(is_error, "{arguments}: {body}");
|
||||||
|
assert_eq!(body["code"], code, "{arguments}: {body}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stockbit_provider_quotes_carry_market_status_and_blocks_surface_once() {
|
||||||
|
let mut client = Client::start_with_env(&[], false, &[("IDX_PROVIDER", "stockbit")]);
|
||||||
|
let quotes = client.ok("get_quotes", json!({"symbols": ["BBCA"]}));
|
||||||
|
assert_eq!(quotes["quotes"][0]["source"], "stockbit");
|
||||||
|
assert_eq!(quotes["quotes"][0]["data"]["market_status"], "close");
|
||||||
|
|
||||||
|
let mut blocked = Client::start_with_env(&[], false, &[("IDX_MOCK_STOCKBIT_STATUS", "403")]);
|
||||||
|
let (is_error, body) = blocked.call("get_intraday", json!({"symbol": "BBCA"}));
|
||||||
|
assert!(is_error, "{body}");
|
||||||
|
assert_eq!(body["code"], "UPSTREAMBLOCKED");
|
||||||
|
assert!(
|
||||||
|
body["message"].as_str().unwrap().contains("does not retry"),
|
||||||
|
"{body}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn errors_carry_cli_codes() {
|
fn errors_carry_cli_codes() {
|
||||||
let mut client = Client::start(&[], false);
|
let mut client = Client::start(&[], false);
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ command parses arguments, makes one idx-core call, and renders the result.
|
||||||
(`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries).
|
(`BrokerSummaryProvider`). See [Broker Summaries](#broker-summaries).
|
||||||
- **IDX (idx.co.id)** — the daily trading summary behind `stocks flow
|
- **IDX (idx.co.id)** — the daily trading summary behind `stocks flow
|
||||||
--source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow).
|
--source idx` (`TradingSummaryProvider`). See [Foreign Flow](#foreign-flow).
|
||||||
|
- **Stockbit (public page)** — `stocks intraday` whichever provider is
|
||||||
|
selected (`IntradayProvider`), and quotes under the opt-in
|
||||||
|
`--provider stockbit`. See [Stockbit](#stockbit).
|
||||||
|
|
||||||
### MSN Endpoints
|
### MSN Endpoints
|
||||||
- `https://assets.msn.com/service/`: quotes, charts, equities, earnings,
|
- `https://assets.msn.com/service/`: quotes, charts, equities, earnings,
|
||||||
|
|
@ -151,6 +154,41 @@ WIB as the fallback.
|
||||||
range fails on one. `foreign_net_value` is net shares at the session's
|
range fails on one. `foreign_net_value` is net shares at the session's
|
||||||
average price (value over volume), an estimate.
|
average price (value over volume), an estimate.
|
||||||
|
|
||||||
|
### Stockbit
|
||||||
|
- `GET https://stockbit.com/symbol/{CODE}` needs no login. Next.js
|
||||||
|
server-renders the quote and today's chart into the page's `__NEXT_DATA__`
|
||||||
|
JSON; `api/stockbit/parse.rs` reads it purely. `props.pageProps.company`
|
||||||
|
is the quote (numbers mostly as strings, an index price as a float string),
|
||||||
|
`orderbook.{bid,offer}.price` the best bid and offer, `market_hour.status`
|
||||||
|
the market's state, and `status` `STATUS_ACTIVE|SUSPENDED|DELISTED`.
|
||||||
|
`chart.prices[]` holds one price per minute in WIB with no offset, after a
|
||||||
|
synthetic first row at the previous close; per-minute OHLC and volume are
|
||||||
|
empty. An unknown code is HTTP 404 with the 404 page.
|
||||||
|
- Stockbit's JSON API (`exodus.stockbit.com`: chart, order book) answers 401
|
||||||
|
without a user's bearer token; it is not used.
|
||||||
|
- Bid and offer are kept only while `market_hour.status` is `open`, so a
|
||||||
|
closing book never reads as a live one. Observed 2026-10-23: `open` during
|
||||||
|
trading (bid/offer set), `break` from 11:30 on a Friday, `close` after
|
||||||
|
hours; any other value fails closed. In session the chart gained a point
|
||||||
|
every minute and the last trade was within a minute of the clock, although
|
||||||
|
Stockbit's terms call its data delayed at least 10 minutes. Order-book
|
||||||
|
sizes are dropped: the page does not say whether they are lots or shares.
|
||||||
|
`as_of` comes from `date`/`time`
|
||||||
|
only when the stock traded (a suspended stock carries today's date).
|
||||||
|
`session_note` repeats what the page says (suspended, delisted) or "no
|
||||||
|
trades this session"; it is never inferred.
|
||||||
|
- CloudFront in front of the page answers a static 403 to some user agents
|
||||||
|
(`curl/`, `axios/`, `Go-http-client/`, empty) and does not fingerprint TLS,
|
||||||
|
so a plain ureq request with `idx-cli/<version> (+repo)` passes. A 403 is
|
||||||
|
`UPSTREAMBLOCKED` and is not retried. Requests are at least 1 s apart
|
||||||
|
across the user's idx processes (the last request's time sits in
|
||||||
|
`<cache>/stockbit.pace` under a file lock, so a shell loop of `idx` calls is
|
||||||
|
paced too), and snapshots are cached for 5 s (at most `quote_ttl`), which
|
||||||
|
coalesces an agent's quote-then-intraday burst across MCP calls.
|
||||||
|
- Stockbit's terms call its data delayed at least 10 minutes and for
|
||||||
|
personal use; frontends say so. Fixtures (`tests/fixtures/stockbit_*`) are
|
||||||
|
pages recorded 2026-10-22 after the close, cut to the fields read.
|
||||||
|
|
||||||
### Capability Gating
|
### Capability Gating
|
||||||
```
|
```
|
||||||
MarketDataProvider = QuoteProvider + FundamentalsProvider
|
MarketDataProvider = QuoteProvider + FundamentalsProvider
|
||||||
|
|
@ -161,8 +199,13 @@ Factory functions:
|
||||||
history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)>
|
history_provider(kind, mode, verbose) → Result<(ProviderKind, Box<dyn HistoryProvider>)>
|
||||||
broker_provider() → Box<dyn BrokerSummaryProvider>
|
broker_provider() → Box<dyn BrokerSummaryProvider>
|
||||||
trading_summary_provider() → Box<dyn TradingSummaryProvider>
|
trading_summary_provider() → Box<dyn TradingSummaryProvider>
|
||||||
|
intraday_provider() → Box<dyn IntradayProvider>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ProviderKind::Stockbit` implements quotes only: fundamentals are
|
||||||
|
`UNSUPPORTED`, the MSN-only capabilities say "requires --provider msn", and
|
||||||
|
`auto` history falls back to Yahoo.
|
||||||
|
|
||||||
## Ownership Module (SQLite-backed)
|
## Ownership Module (SQLite-backed)
|
||||||
|
|
||||||
Unlike the `stocks` module (live-fetch), ownership is **import-then-query**:
|
Unlike the `stocks` module (live-fetch), ownership is **import-then-query**:
|
||||||
|
|
@ -239,7 +282,9 @@ CLI flags > environment variables > config file > defaults
|
||||||
bidirectional overrides replaced before it reaches a terminal table.
|
bidirectional overrides replaced before it reaches a terminal table.
|
||||||
- Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage
|
- Exit codes by class: `0` success, `2` `INVALIDINPUT`/`CONFIGERROR` and clap usage
|
||||||
errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/
|
errors, `3` `SYMBOLNOTFOUND`/`NOMARKETDATA`/`CACHEMISS`, `4` `HTTP`/`RATELIMITED`/
|
||||||
`PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`, `5` `UNSUPPORTED`, `1` everything else
|
`PROVIDERUNAVAILABLE`/`OFFLINE`/`AUTHERROR`/`UPSTREAMBLOCKED`, `5` `UNSUPPORTED`,
|
||||||
|
`1` everything else. `UPSTREAMBLOCKED` is an upstream refusing this client
|
||||||
|
outright (a CDN 403 page); retrying at once does not help.
|
||||||
- A totally failed `stocks compare` emits one structured error in JSON mode; per-symbol warnings are retained for partial success, not printed ahead of a total-failure error.
|
- A totally failed `stocks compare` emits one structured error in JSON mode; per-symbol warnings are retained for partial success, not printed ahead of a total-failure error.
|
||||||
|
|
||||||
## CLI Output and Offline Cache
|
## CLI Output and Offline Cache
|
||||||
|
|
|
||||||
|
|
@ -184,7 +184,7 @@ is another 20-30% smaller than pretty JSON.
|
||||||
| 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... |
|
| 1 | other failure | `IO`, `DATABASEERROR`, `PARSEERROR`, ... |
|
||||||
| 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` |
|
| 2 | invalid usage or input | clap errors, `INVALIDINPUT`, `CONFIGERROR` |
|
||||||
| 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` |
|
| 3 | not found or no data | `SYMBOLNOTFOUND`, `NOMARKETDATA`, `CACHEMISS` |
|
||||||
| 4 | upstream unavailable, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR` |
|
| 4 | upstream unavailable or blocked, retry later | `HTTP`, `RATELIMITED`, `PROVIDERUNAVAILABLE`, `OFFLINE`, `AUTHERROR`, `UPSTREAMBLOCKED` |
|
||||||
| 5 | unsupported | `UNSUPPORTED` |
|
| 5 | unsupported | `UNSUPPORTED` |
|
||||||
|
|
||||||
- JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths
|
- JSON keeps today's top-level shape: no `{data, meta}` wrapper, so `jq` paths
|
||||||
|
|
|
||||||
|
|
@ -42,12 +42,12 @@ contents. The native macOS installer job runs separately to exercise BSD tools.
|
||||||
|
|
||||||
## Live drift check
|
## Live drift check
|
||||||
|
|
||||||
Fixtures cannot tell when MSN, Yahoo, or IDX change their responses.
|
Fixtures cannot tell when MSN, Yahoo, IPOT, IDX, or Stockbit change their responses.
|
||||||
`scripts/live-smoke.sh` runs shipped commands against the real upstreams and
|
`scripts/live-smoke.sh` runs shipped commands against the real upstreams and
|
||||||
validates each JSON output shape. It is opt-in and never part of the gate.
|
validates each JSON output shape. It is opt-in and never part of the gate.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix develop --command scripts/live-smoke.sh # stocks + ownership
|
nix develop --command scripts/live-smoke.sh # stocks + stockbit + ownership
|
||||||
nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI
|
nix develop --command scripts/live-smoke.sh --group stocks --symbol BBRI
|
||||||
nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx
|
nix develop --command scripts/live-smoke.sh --bin ./result/bin/idx
|
||||||
nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B against an older build
|
nix develop --command scripts/live-smoke.sh --compare ./old/bin/idx # A/B against an older build
|
||||||
|
|
@ -58,10 +58,16 @@ scripts/live-smoke.sh --dry-run
|
||||||
`flow` and `flow --source idx` on the new binary only, so an older `--compare` build that predates
|
`flow` and `flow --source idx` on the new binary only, so an older `--compare` build that predates
|
||||||
them is fine), plus `compare BUMI,ADRO,AIMS`,
|
them is fine), plus `compare BUMI,ADRO,AIMS`,
|
||||||
whose MSN key ratios contain non-finite values.
|
whose MSN key ratios contain non-finite values.
|
||||||
|
- `stockbit`: `intraday` for the ticker and `^JKSE`, and
|
||||||
|
`--provider stockbit stocks quote`, on the new binary only. A run outside the
|
||||||
|
session sees `market_status: close` and no bid/offer.
|
||||||
- `ownership`: `discover`, then imports the newest supported report into a temp
|
- `ownership`: `discover`, then imports the newest supported report into a temp
|
||||||
DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many
|
DB and lists `releases`. IDX sits behind Cloudflare and returns `403` to many
|
||||||
datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts.
|
datacenter IPs, including GitHub-hosted runners; run it from a network IDX accepts.
|
||||||
|
|
||||||
|
A case whose upstream refuses us (`UPSTREAMBLOCKED`, an HTTP 403) prints
|
||||||
|
`BLOCKED (HTTP 403)` and counts as a failure, never as a pass.
|
||||||
|
|
||||||
`--compare <old-idx>` runs every case with both binaries back to back, each
|
`--compare <old-idx>` runs every case with both binaries back to back, each
|
||||||
with its own config and cache, and fails on a different exit code, any added,
|
with its own config and cache, and fails on a different exit code, any added,
|
||||||
missing, or retyped JSON key, or changed values for `profile`, `financials`,
|
missing, or retyped JSON key, or changed values for `profile`, `financials`,
|
||||||
|
|
|
||||||
|
|
@ -288,7 +288,7 @@
|
||||||
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
echo '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
||||||
sleep 2
|
sleep 2
|
||||||
} | timeout 30 ${mcpPackage}/bin/idx-mcp > out.jsonl
|
} | timeout 30 ${mcpPackage}/bin/idx-mcp > out.jsonl
|
||||||
jq -se 'map(select(.id == 2))[0].result.tools | length == 14' out.jsonl
|
jq -se 'map(select(.id == 2))[0].result.tools | length == 15' out.jsonl
|
||||||
touch "$out"
|
touch "$out"
|
||||||
'';
|
'';
|
||||||
# The website's generated pages match the CLI help and the MCP
|
# The website's generated pages match the CLI help and the MCP
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Live provider drift check: runs shipped commands against real MSN, Yahoo,
|
# Live provider drift check: runs shipped commands against real MSN, Yahoo,
|
||||||
# and IDX endpoints and validates the JSON output shape. Deterministic CLI
|
# IPOT, IDX, and Stockbit endpoints and validates the JSON output shape.
|
||||||
# behavior is covered by tests/cli.rs and tests/golden.rs; this answers "do the
|
# Deterministic CLI behavior is covered by tests/cli.rs and tests/golden.rs;
|
||||||
# upstreams still look the way our parsers expect?"
|
# this answers "do the upstreams still look the way our parsers expect?"
|
||||||
|
# An upstream that refuses us (UPSTREAMBLOCKED, HTTP 403) is reported as
|
||||||
|
# BLOCKED, never as a pass.
|
||||||
#
|
#
|
||||||
# With --compare <old-idx>, every case also runs the old binary back to back
|
# With --compare <old-idx>, every case also runs the old binary back to back
|
||||||
# and fails on any difference in exit code or JSON structure, and on any value
|
# and fails on any difference in exit code or JSON structure, and on any value
|
||||||
|
|
@ -26,7 +28,8 @@ usage() {
|
||||||
Usage: scripts/live-smoke.sh [options]
|
Usage: scripts/live-smoke.sh [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--group <stocks|ownership> Restrict to a group. Repeatable. Default: both
|
--group <name> stocks, stockbit, or ownership. Repeatable.
|
||||||
|
Default: all three
|
||||||
--symbol <ticker> Primary ticker. Default: BBCA
|
--symbol <ticker> Primary ticker. Default: BBCA
|
||||||
--bin <path> Use an existing idx binary (skips cargo build)
|
--bin <path> Use an existing idx binary (skips cargo build)
|
||||||
--compare <old-idx> Also run an older idx binary and diff the results
|
--compare <old-idx> Also run an older idx binary and diff the results
|
||||||
|
|
@ -46,12 +49,12 @@ while [[ $# -gt 0 ]]; do
|
||||||
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
*) echo "unknown argument: $1" >&2; usage >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks ownership)
|
[[ ${#GROUPS_SELECTED[@]} -eq 0 ]] && GROUPS_SELECTED=(stocks stockbit ownership)
|
||||||
|
|
||||||
for group in "${GROUPS_SELECTED[@]}"; do
|
for group in "${GROUPS_SELECTED[@]}"; do
|
||||||
case "$group" in
|
case "$group" in
|
||||||
stocks|ownership) ;;
|
stocks|stockbit|ownership) ;;
|
||||||
*) echo "invalid --group '$group' (expected stocks or ownership)" >&2; exit 2 ;;
|
*) echo "invalid --group '$group' (expected stocks, stockbit, or ownership)" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
@ -99,8 +102,10 @@ def types: [paths(scalars) as $p | select(getpath($p) != null)
|
||||||
# jq filter describing the JSON shape each command must keep.
|
# jq filter describing the JSON shape each command must keep.
|
||||||
contract() {
|
contract() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
"stocks quote "*|"stocks screen"*)
|
"stocks quote "*|"stocks screen"*|"--provider stockbit stocks quote "*)
|
||||||
echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.price | type == "number"))' ;;
|
echo 'type == "array" and length > 0 and all(.[]; (.symbol | type == "string") and (.price | type == "number"))' ;;
|
||||||
|
"stocks intraday "*)
|
||||||
|
echo '(.quote.symbol | type == "string") and (.quote.price | type == "number") and (.quote.market_status | type == "string") and (.points | type == "array") and all(.points[]; (.time | type == "string") and (.price | type == "number"))' ;;
|
||||||
"stocks history "*)
|
"stocks history "*)
|
||||||
echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;;
|
echo 'type == "array" and length > 0 and all(.[]; (.date | type == "string") and (.close | type == "number"))' ;;
|
||||||
"stocks technical "*)
|
"stocks technical "*)
|
||||||
|
|
@ -172,6 +177,11 @@ run_case() {
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if (( status != 0 )); then
|
if (( status != 0 )); then
|
||||||
|
if grep -q 'UPSTREAMBLOCKED' "$log.stderr"; then
|
||||||
|
FAILURES+=("$name: BLOCKED, upstream refused us with HTTP 403 (see $log.stderr)")
|
||||||
|
echo " BLOCKED (HTTP 403)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
FAILURES+=("$name: exit $status (see $log.stderr)")
|
FAILURES+=("$name: exit $status (see $log.stderr)")
|
||||||
echo " FAIL exit"
|
echo " FAIL exit"
|
||||||
return
|
return
|
||||||
|
|
@ -238,6 +248,13 @@ if selected stocks; then
|
||||||
run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx"
|
run_new_case flow-idx "stocks flow $SYMBOL --days 3 --source idx"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if selected stockbit; then
|
||||||
|
# Stockbit's public page (no login): one request per case, 1 s apart.
|
||||||
|
run_new_case intraday "stocks intraday $SYMBOL"
|
||||||
|
run_new_case intraday-index "stocks intraday ^JKSE"
|
||||||
|
run_new_case quote-stockbit "--provider stockbit stocks quote $SYMBOL"
|
||||||
|
fi
|
||||||
|
|
||||||
if selected ownership; then
|
if selected ownership; then
|
||||||
run_case discover "ownership discover --limit 1"
|
run_case discover "ownership discover --limit 1"
|
||||||
if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then
|
if (( ! DRY_RUN )) && [[ -s "$LOG_DIR/$(printf '%02d' "$TOTAL")_discover.stdout" ]]; then
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'CLI',
|
label: 'CLI',
|
||||||
items: ['cli/stocks', 'cli/ownership', 'cli/output', 'cli/reference'],
|
items: ['cli/stocks', 'cli/stockbit', 'cli/ownership', 'cli/output', 'cli/reference'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'MCP server',
|
label: 'MCP server',
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ stdout is not a terminal, and with `--no-color`.
|
||||||
| `0` | success | |
|
| `0` | success | |
|
||||||
| `2` | invalid input or config | fix the command |
|
| `2` | invalid input or config | fix the command |
|
||||||
| `3` | not found, no data, or no cached data offline | check the ticker or name; go online |
|
| `3` | not found, no data, or no cached data offline | check the ticker or name; go online |
|
||||||
| `4` | upstream unavailable | retry later |
|
| `4` | upstream unavailable or blocked | retry later; for `UPSTREAMBLOCKED`, wait or change network |
|
||||||
| `5` | unsupported | use another command or provider |
|
| `5` | unsupported | use another command or provider |
|
||||||
| `1` | anything else | read the message |
|
| `1` | anything else | read the message |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -60,6 +65,7 @@ Usage: idx stocks [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
quote Get real-time stock quotes
|
quote Get real-time stock quotes
|
||||||
|
intraday Today's session price by the minute (Stockbit)
|
||||||
history Get historical OHLC data
|
history Get historical OHLC data
|
||||||
technical Run technical analysis on a stock
|
technical Run technical analysis on a stock
|
||||||
growth Run growth analysis on a stock
|
growth Run growth analysis on a stock
|
||||||
|
|
@ -84,6 +90,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -104,6 +111,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -113,6 +121,53 @@ Examples:
|
||||||
idx stocks quote BBCA,BBRI,BMRI
|
idx stocks quote BBCA,BBRI,BMRI
|
||||||
idx -o json stocks quote BBCA
|
idx -o json stocks quote BBCA
|
||||||
echo BBCA BBRI | idx stocks quote -
|
echo BBCA BBRI | idx stocks quote -
|
||||||
|
idx --provider stockbit stocks quote BBCA # with bid/offer while the market is open
|
||||||
|
```
|
||||||
|
|
||||||
|
### `idx stocks intraday`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Today's session of one IDX stock or index, by the minute, from Stockbit's public page (no login): the quote, the best bid/offer while the market is open, a sparkline, and the last minutes. Delayed at least 10 minutes per Stockbit's terms; prices only, no per-minute volume. A stock that did not trade has no minutes, which is not an error. Indices use Yahoo's tickers (^JKSE, ^JKLQ45) or Stockbit's (IHSG, LQ45).
|
||||||
|
|
||||||
|
Usage: idx stocks intraday [OPTIONS] <SYMBOL>
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
<SYMBOL>
|
||||||
|
Single ticker symbol (e.g. BBCA)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --output <OUTPUT>
|
||||||
|
Output format [default: auto = table on a terminal, json when piped]
|
||||||
|
|
||||||
|
[possible values: auto, table, json, jsonl]
|
||||||
|
|
||||||
|
--no-color
|
||||||
|
Disable colored output
|
||||||
|
|
||||||
|
-q, --quiet
|
||||||
|
Suppress non-essential output
|
||||||
|
|
||||||
|
-v, --verbose...
|
||||||
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
|
--offline
|
||||||
|
Run without network requests
|
||||||
|
|
||||||
|
--no-cache
|
||||||
|
Bypass local cache
|
||||||
|
|
||||||
|
-h, --help
|
||||||
|
Print help (see a summary with '-h')
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
idx stocks intraday BBCA
|
||||||
|
idx stocks intraday ^JKSE
|
||||||
|
idx -o json stocks intraday BBCA | jq '.points[-1]'
|
||||||
```
|
```
|
||||||
|
|
||||||
### `idx stocks history`
|
### `idx stocks history`
|
||||||
|
|
@ -140,6 +195,8 @@ Options:
|
||||||
Suppress non-essential output
|
Suppress non-essential output
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
--no-cache
|
--no-cache
|
||||||
|
|
@ -173,6 +230,8 @@ Options:
|
||||||
Suppress non-essential output
|
Suppress non-essential output
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
--no-cache
|
--no-cache
|
||||||
|
|
@ -200,6 +259,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -224,6 +284,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -248,6 +309,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -272,6 +334,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -337,6 +400,11 @@ Options:
|
||||||
|
|
||||||
[default: all]
|
[default: all]
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -409,17 +477,22 @@ Options:
|
||||||
|
|
||||||
[default: all]
|
[default: all]
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
--no-cache
|
|
||||||
Bypass local cache
|
|
||||||
|
|
||||||
--top <TOP>
|
--top <TOP>
|
||||||
Tier behind the headline signal: 1, 3, 5 or 10 brokers per side
|
Tier behind the headline signal: 1, 3, 5 or 10 brokers per side
|
||||||
|
|
||||||
[default: 5]
|
[default: 5]
|
||||||
|
|
||||||
|
--no-cache
|
||||||
|
Bypass local cache
|
||||||
|
|
||||||
-h, --help
|
-h, --help
|
||||||
Print help (see a summary with '-h')
|
Print help (see a summary with '-h')
|
||||||
|
|
||||||
|
|
@ -474,6 +547,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -506,6 +584,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -527,6 +606,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -556,6 +636,7 @@ Options:
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
--quarterly Only include quarterly periods
|
--quarterly Only include quarterly periods
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -581,6 +662,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -601,6 +683,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -622,6 +705,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -642,6 +726,7 @@ Options:
|
||||||
--limit <LIMIT> [default: 50]
|
--limit <LIMIT> [default: 50]
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -662,6 +747,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -704,6 +790,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -737,6 +828,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -757,6 +849,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -778,6 +871,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -795,6 +889,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -817,6 +912,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -834,6 +930,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -851,6 +948,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -899,6 +997,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -930,6 +1033,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -977,6 +1081,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -1028,6 +1137,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -1061,6 +1175,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1081,6 +1196,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1121,6 +1237,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -1149,6 +1270,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1168,6 +1290,7 @@ Options:
|
||||||
--top <TOP> [default: 20]
|
--top <TOP> [default: 20]
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1188,6 +1311,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1223,6 +1347,11 @@ Options:
|
||||||
-v, --verbose...
|
-v, --verbose...
|
||||||
Increase verbosity (-v, -vv)
|
Increase verbosity (-v, -vv)
|
||||||
|
|
||||||
|
--provider <PROVIDER>
|
||||||
|
Market data provider [default: general.provider, else msn]
|
||||||
|
|
||||||
|
[possible values: yahoo, msn, stockbit]
|
||||||
|
|
||||||
--offline
|
--offline
|
||||||
Run without network requests
|
Run without network requests
|
||||||
|
|
||||||
|
|
@ -1254,6 +1383,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1277,6 +1407,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1295,6 +1426,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1316,6 +1448,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1337,6 +1470,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1354,6 +1488,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1374,6 +1509,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
@ -1391,6 +1527,7 @@ Options:
|
||||||
--no-color Disable colored output
|
--no-color Disable colored output
|
||||||
-q, --quiet Suppress non-essential output
|
-q, --quiet Suppress non-essential output
|
||||||
-v, --verbose... Increase verbosity (-v, -vv)
|
-v, --verbose... Increase verbosity (-v, -vv)
|
||||||
|
--provider <PROVIDER> Market data provider [default: general.provider, else msn] [possible values: yahoo, msn, stockbit]
|
||||||
--offline Run without network requests
|
--offline Run without network requests
|
||||||
--no-cache Bypass local cache
|
--no-cache Bypass local cache
|
||||||
-h, --help Print help
|
-h, --help Print help
|
||||||
|
|
|
||||||
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`
|
provider may still show the previous close. `history` takes `--period` (`1d`
|
||||||
to `5y`) and `--interval` (`1d`, `1wk`, `1mo`).
|
to `5y`) and `--interval` (`1d`, `1wk`, `1mo`).
|
||||||
|
|
||||||
|
## Today by the minute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
idx stocks intraday BBCA
|
||||||
|
idx stocks intraday ^JKSE
|
||||||
|
idx -o json stocks intraday BBCA | jq '.points[-1]'
|
||||||
|
```
|
||||||
|
|
||||||
|
`intraday` shows today's session from Stockbit's public page, without a
|
||||||
|
login: the quote, the best bid and offer while the market is open, a
|
||||||
|
sparkline and the last five minutes. JSON gives every minute in `points`. It
|
||||||
|
is delayed at least 10 minutes. See [Stockbit](/cli/stockbit/) for what it
|
||||||
|
covers and the terms it comes with.
|
||||||
|
|
||||||
## Company data
|
## Company data
|
||||||
|
|
||||||
| Command | What it shows |
|
| Command | What it shows |
|
||||||
|
|
@ -142,6 +156,8 @@ idx stocks screen --filter high-dividend | jq -r '.[].symbol' | idx stocks compa
|
||||||
|
|
||||||
MSN is the default for quotes, company data and screening; Yahoo serves
|
MSN is the default for quotes, company data and screening; Yahoo serves
|
||||||
price history. Indo Premier (IPOT) serves broker summaries and foreign flow
|
price history. Indo Premier (IPOT) serves broker summaries and foreign flow
|
||||||
(IDX's own site with `flow --source idx`), whichever provider is selected. Switch with `IDX_PROVIDER=msn|yahoo` and
|
(IDX's own site with `flow --source idx`), and Stockbit serves `intraday`,
|
||||||
`IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the matching
|
whichever provider is selected. Switch with `--provider msn|yahoo|stockbit`
|
||||||
[config keys](/start/config/).
|
(or `IDX_PROVIDER`) and `IDX_HISTORY_PROVIDER=auto|yahoo|msn`, or the
|
||||||
|
matching [config keys](/start/config/). `--provider stockbit` serves quotes
|
||||||
|
only, with the best bid/offer; see [Stockbit](/cli/stockbit/).
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,9 @@ description: idx-mcp serves idx's stock and ownership data to AI agents over MCP
|
||||||
|
|
||||||
`idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
|
`idx-mcp` is a [Model Context Protocol](https://modelcontextprotocol.io)
|
||||||
server over stdio. It gives an agent the same data as the `idx` CLI, as
|
server over stdio. It gives an agent the same data as the `idx` CLI, as
|
||||||
[14 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, price history, screening, company data,
|
[15 tools](/mcp/tools/) (plus an opt-in `ownership_sync`): quotes, today's
|
||||||
analysis, comparison, broker summaries, bandarmology, foreign flow, and the KSEI
|
session by the minute, price history, screening, company data, analysis,
|
||||||
|
comparison, broker summaries, bandarmology, foreign flow, and the KSEI
|
||||||
ownership queries.
|
ownership queries.
|
||||||
|
|
||||||
It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps),
|
It shares idx's [config, cache and ownership database](/start/config/#files-idx-keeps),
|
||||||
|
|
@ -27,7 +28,7 @@ Tools that fetch live data return:
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`source` is the provider (`msn` or `yahoo`). Results are cached on disk;
|
`source` is the provider (`msn`, `yahoo`, `ipot`, `idx` or `stockbit`). Results are cached on disk;
|
||||||
`stale` is true when an expired cache entry was served because a fresh fetch
|
`stale` is true when an expired cache entry was served because a fresh fetch
|
||||||
failed. Ownership tools return their data directly, from the local
|
failed. Ownership tools return their data directly, from the local
|
||||||
database.
|
database.
|
||||||
|
|
@ -44,6 +45,7 @@ body, so the agent can decide whether to retry:
|
||||||
| Input | Limit |
|
| Input | Limit |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| tickers per call (`get_quotes`, `compare_stocks`) | 20, duplicates removed |
|
| tickers per call (`get_quotes`, `compare_stocks`) | 20, duplicates removed |
|
||||||
|
| `get_intraday` `tail` | 400 minutes (default: the whole session) |
|
||||||
| `screen_stocks` `limit` | 200 |
|
| `screen_stocks` `limit` | 200 |
|
||||||
| `search_investors` `limit` | 100 |
|
| `search_investors` `limit` | 100 |
|
||||||
| `get_ownership_changes` `limit` | 300 per page (default 200); page with `offset` |
|
| `get_ownership_changes` `limit` | 300 per page (default 200); page with `offset` |
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,15 @@ Foreign investors' buying and selling of one IDX ticker, session by session (old
|
||||||
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
|
| `symbol` | string | yes | IDX ticker, e.g. "BBCA". |
|
||||||
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
|
| `to` | string | no | Last session of the range, YYYY-MM-DD (default today). |
|
||||||
|
|
||||||
|
## `get_intraday`
|
||||||
|
|
||||||
|
Today's session of one IDX stock or index from Stockbit's public page (no login), whichever provider the server uses: `quote` (price, change, volume, `market_status`, `bid`/`offer` only while the market is open, `session_note`) and `points`, the price per minute in WIB, oldest first. Delayed at least 10 minutes; no per-minute volume. Empty `points` means the stock did not trade this session (see `session_note`), not an error. UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once.
|
||||||
|
|
||||||
|
| Input | Type | Required | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `symbol` | string | yes | IDX ticker, e.g. "BBCA"; indices as "^JKSE" or "IHSG". |
|
||||||
|
| `tail` | integer | no | Only the latest N minutes (default: the whole session, at most 400). |
|
||||||
|
|
||||||
## `get_investor_holdings`
|
## `get_investor_holdings`
|
||||||
|
|
||||||
Every IDX holding (above 1%) of one investor, matched by name.
|
Every IDX holding (above 1%) of one investor, matched by name.
|
||||||
|
|
@ -107,7 +116,7 @@ OHLCV price history for one IDX ticker.
|
||||||
|
|
||||||
## `get_quotes`
|
## `get_quotes`
|
||||||
|
|
||||||
Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday.
|
Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. When the server runs with the stockbit provider, quotes also carry `market_status` (Stockbit's, e.g. "open", "close"), `bid`/`offer` (best price, only while the market is open) and `session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 minutes.
|
||||||
|
|
||||||
| Input | Type | Required | Description |
|
| Input | Type | Required | Description |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ in scripts. Ask for JSON with `-o json`, `IDX_OUTPUT=json`, or
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[general]
|
[general]
|
||||||
provider = "msn" # msn | yahoo
|
provider = "msn" # msn | yahoo | stockbit
|
||||||
history_provider = "auto" # auto | yahoo | msn
|
history_provider = "auto" # auto | yahoo | msn
|
||||||
exchange = "JK"
|
exchange = "JK"
|
||||||
output = "auto" # auto | table | json | jsonl
|
output = "auto" # auto | table | json | jsonl
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@ TLKM.JK 2,250 <span class="up">+0.45%</span> 87,159,900
|
||||||
<div>
|
<div>
|
||||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="4" y="7" width="16" height="12" rx="3"></rect><path d="M12 3v4M9 13h.01M15 13h.01"></path></svg>
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="4" y="7" width="16" height="12" rx="3"></rect><path d="M12 3v4M9 13h.01M15 13h.01"></path></svg>
|
||||||
<h2>Agents</h2>
|
<h2>Agents</h2>
|
||||||
<p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 11 tools for any MCP client.</p>
|
<p>JSON whenever output is piped, errors as one JSON object, meaningful exit codes. Plus idx-mcp: 15 tools for any MCP client.</p>
|
||||||
<code>npx -y idx-mcp</code>
|
<code>npx -y idx-mcp</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue