diff --git a/crates/idx-cli/src/output/table.rs b/crates/idx-cli/src/output/table.rs index 994c454..1663578 100644 --- a/crates/idx-cli/src/output/table.rs +++ b/crates/idx-cli/src/output/table.rs @@ -236,6 +236,9 @@ pub fn print_intraday(day: &Intraday, no_color: bool) -> Result<(), IdxError> { if let Some(note) = &q.session_note { println!("Note: {}", terminal_safe(note)); } + for note in stale_quote_notes(std::slice::from_ref(q), chrono::Utc::now()) { + println!("{}", terminal_safe(¬e)); + } let prices: Vec = day.points.iter().map(|p| p.price).collect(); if let (Some(first), Some(last)) = (day.points.first(), day.points.last()) { diff --git a/crates/idx-cli/tests/cli.rs b/crates/idx-cli/tests/cli.rs index c086316..490e254 100644 --- a/crates/idx-cli/tests/cli.rs +++ b/crates/idx-cli/tests/cli.rs @@ -2197,6 +2197,51 @@ fn stockbit_login_stores_a_private_session_and_logout_removes_it() { assert!(!session.exists()); } +/// A session file Stockbit no longer accepts does not break `auto` history: +/// it falls back to Yahoo with a warning. An explicit Stockbit choice fails. +#[test] +fn auto_history_falls_back_to_yahoo_when_the_session_is_refused() { + for args in [ + &["stocks", "history", "BBCA", "--period", "1mo"][..], + &["stocks", "technical", "BBCA"][..], + ] { + let root = test_env_dir(&format!("stockbit-refused-{}", args[1])); + let output = mock_bin_with_root(&root) + .env("IDX_STOCKBIT_REFRESH_TOKEN", FAKE_STOCKBIT_TOKEN) + .env("IDX_MOCK_STOCKBIT_STATUS", "401") + .args(["-o", "json", "--provider", "stockbit"]) + .args(args) + .output() + .expect("run history"); + assert!(output.status.success(), "{args:?}: {output:?}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("falls back to yahoo"), "{stderr}"); + serde_json::from_slice::(&output.stdout).expect("JSON on stdout"); + let _ = fs::remove_dir_all(root); + } + let root = test_env_dir("stockbit-refused-explicit"); + run_error_json( + mock_bin_with_root(&root) + .env("IDX_STOCKBIT_REFRESH_TOKEN", FAKE_STOCKBIT_TOKEN) + .env("IDX_MOCK_STOCKBIT_STATUS", "401") + .args([ + "-o", + "json", + "--provider", + "stockbit", + "stocks", + "history", + "BBCA", + ]) + .args(["--period", "1mo", "--history-provider", "stockbit"]), + "AUTHERROR", + ); + let _ = fs::remove_dir_all(root); +} + +/// A test session, signed by nobody, that expires in 2100. +const FAKE_STOCKBIT_TOKEN: &str = "eyJhbGciOiJub25lIn0.eyJleHAiOiA0MTAyNDQ0ODAwLCAibm90ZSI6ICJ0ZXN0IHRva2VuLCBzaWduZWQgYnkgbm9ib2R5In0.c2ln"; + /// An agent on a headless box gets told how to log in, not "nothing pasted". #[test] fn stockbit_login_with_empty_stdin_says_how_to_log_in() { diff --git a/crates/idx-cli/tests/golden/stockbit-session-quote.json.golden b/crates/idx-cli/tests/golden/stockbit-session-quote.json.golden index 57399e3..7d27f04 100644 --- a/crates/idx-cli/tests/golden/stockbit-session-quote.json.golden +++ b/crates/idx-cli/tests/golden/stockbit-session-quote.json.golden @@ -1,4 +1,4 @@ exit: 0 --- stdout -[{"symbol":"BBCA.JK","price":5175,"change":-900,"change_pct":-14.81,"volume":1241098200,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":6075,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"STATE_NAME_MARKET_CLOSED"}] +[{"symbol":"BBCA.JK","price":5175,"change":-900,"change_pct":-14.81,"volume":1241098200,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":6075,"avg_volume":null,"as_of":null,"behind_session":false,"market_status":"close"}] --- stderr diff --git a/crates/idx-cli/tests/golden/stockbit-session-quote.table.golden b/crates/idx-cli/tests/golden/stockbit-session-quote.table.golden index 4863e68..70fae5d 100644 --- a/crates/idx-cli/tests/golden/stockbit-session-quote.table.golden +++ b/crates/idx-cli/tests/golden/stockbit-session-quote.table.golden @@ -3,7 +3,7 @@ exit: 0 ┌─────────┬───────┬──────┬─────────┬───────────────┬─────┬───────┬────────┬───────┐ │ SYMBOL ┆ PRICE ┆ CHG ┆ CHG% ┆ VOLUME ┆ BID ┆ OFFER ┆ MARKET ┆ AS OF │ ╞═════════╪═══════╪══════╪═════════╪═══════════════╪═════╪═══════╪════════╪═══════╡ -│ BBCA.JK ┆ 5,175 ┆ -900 ┆ -14.81% ┆ 1,241,098,200 ┆ - ┆ - ┆ closed ┆ - │ +│ BBCA.JK ┆ 5,175 ┆ -900 ┆ -14.81% ┆ 1,241,098,200 ┆ - ┆ - ┆ close ┆ - │ └─────────┴───────┴──────┴─────────┴───────────────┴─────┴───────┴────────┴───────┘ Source: Stockbit, your session when logged in (`idx stockbit status`), else its public page, delayed at least 10 minutes (Stockbit's terms); bid/offer only while the market is open. --- stderr diff --git a/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden b/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden index b284ebc..173b7c5 100644 --- a/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden +++ b/crates/idx-cli/tests/golden/stocks-intraday-index.table.golden @@ -2,6 +2,7 @@ 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 +Note: ^JKSE.JK last traded on 2026-10-22; this price is not current. ▃▄▄▆▆▆▆▆▆▇▇▇▇▇████▇▇▇▇███▇▇▇▇███▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇▇ 08:58 to 16:14 WIB · low 5,192.41 · high 5,419.34 · 335 minutes diff --git a/crates/idx-cli/tests/golden/stocks-intraday.json.golden b/crates/idx-cli/tests/golden/stocks-intraday.json.golden index 54882c9..40d7b72 100644 --- a/crates/idx-cli/tests/golden/stocks-intraday.json.golden +++ b/crates/idx-cli/tests/golden/stocks-intraday.json.golden @@ -1,4 +1,4 @@ exit: 0 --- stdout -{"quote":{"symbol":"BBCA.JK","price":5175,"change":-100,"change_pct":-1.9,"volume":286928500,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":5275,"avg_volume":null,"as_of":"2026-10-22T16:14:00+07:00","behind_session":false,"market_status":"close"},"points":[{"time":"2026-10-22T08:58:00+07:00","price":5125.0},{"time":"2026-10-22T09:00:00+07:00","price":5125.0},{"time":"2026-10-22T09:01:00+07:00","price":5175.0},{"time":"2026-10-22T09:02:00+07:00","price":5150.0},{"time":"2026-10-22T09:03:00+07:00","price":5175.0},{"time":"2026-10-22T09:04:00+07:00","price":5175.0},{"time":"2026-10-22T09:05:00+07:00","price":5175.0},{"time":"2026-10-22T09:06:00+07:00","price":5175.0},{"time":"2026-10-22T09:07:00+07:00","price":5200.0},{"time":"2026-10-22T09:08:00+07:00","price":5175.0},{"time":"2026-10-22T09:09:00+07:00","price":5200.0},{"time":"2026-10-22T09:10:00+07:00","price":5200.0},{"time":"2026-10-22T09:11:00+07:00","price":5200.0},{"time":"2026-10-22T09:12:00+07:00","price":5200.0},{"time":"2026-10-22T09:13:00+07:00","price":5200.0},{"time":"2026-10-22T09:14:00+07:00","price":5225.0},{"time":"2026-10-22T09:15:00+07:00","price":5200.0},{"time":"2026-10-22T09:16:00+07:00","price":5225.0},{"time":"2026-10-22T09:17:00+07:00","price":5225.0},{"time":"2026-10-22T09:18:00+07:00","price":5200.0},{"time":"2026-10-22T09:19:00+07:00","price":5225.0},{"time":"2026-10-22T09:20:00+07:00","price":5250.0},{"time":"2026-10-22T09:21:00+07:00","price":5225.0},{"time":"2026-10-22T09:22:00+07:00","price":5225.0},{"time":"2026-10-22T09:23:00+07:00","price":5225.0},{"time":"2026-10-22T09:24:00+07:00","price":5225.0},{"time":"2026-10-22T09:25:00+07:00","price":5200.0},{"time":"2026-10-22T09:26:00+07:00","price":5200.0},{"time":"2026-10-22T09:27:00+07:00","price":5225.0},{"time":"2026-10-22T09:28:00+07:00","price":5225.0},{"time":"2026-10-22T09:29:00+07:00","price":5250.0},{"time":"2026-10-22T09:30:00+07:00","price":5225.0},{"time":"2026-10-22T09:31:00+07:00","price":5225.0},{"time":"2026-10-22T09:32:00+07:00","price":5200.0},{"time":"2026-10-22T09:33:00+07:00","price":5225.0},{"time":"2026-10-22T09:34:00+07:00","price":5225.0},{"time":"2026-10-22T09:35:00+07:00","price":5225.0},{"time":"2026-10-22T09:36:00+07:00","price":5250.0},{"time":"2026-10-22T09:37:00+07:00","price":5225.0},{"time":"2026-10-22T09:38:00+07:00","price":5225.0},{"time":"2026-10-22T09:39:00+07:00","price":5225.0},{"time":"2026-10-22T09:40:00+07:00","price":5225.0},{"time":"2026-10-22T09:41:00+07:00","price":5225.0},{"time":"2026-10-22T09:42:00+07:00","price":5225.0},{"time":"2026-10-22T09:43:00+07:00","price":5225.0},{"time":"2026-10-22T09:44:00+07:00","price":5225.0},{"time":"2026-10-22T09:45:00+07:00","price":5200.0},{"time":"2026-10-22T09:46:00+07:00","price":5200.0},{"time":"2026-10-22T09:47:00+07:00","price":5225.0},{"time":"2026-10-22T09:48:00+07:00","price":5200.0},{"time":"2026-10-22T09:49:00+07:00","price":5225.0},{"time":"2026-10-22T09:50:00+07:00","price":5225.0},{"time":"2026-10-22T09:51:00+07:00","price":5225.0},{"time":"2026-10-22T09:52:00+07:00","price":5225.0},{"time":"2026-10-22T09:53:00+07:00","price":5225.0},{"time":"2026-10-22T09:54:00+07:00","price":5200.0},{"time":"2026-10-22T09:55:00+07:00","price":5225.0},{"time":"2026-10-22T09:56:00+07:00","price":5225.0},{"time":"2026-10-22T09:57:00+07:00","price":5225.0},{"time":"2026-10-22T09:58:00+07:00","price":5225.0},{"time":"2026-10-22T09:59:00+07:00","price":5225.0},{"time":"2026-10-22T10:00:00+07:00","price":5250.0},{"time":"2026-10-22T10:01:00+07:00","price":5250.0},{"time":"2026-10-22T10:02:00+07:00","price":5250.0},{"time":"2026-10-22T10:03:00+07:00","price":5250.0},{"time":"2026-10-22T10:04:00+07:00","price":5250.0},{"time":"2026-10-22T10:05:00+07:00","price":5225.0},{"time":"2026-10-22T10:06:00+07:00","price":5225.0},{"time":"2026-10-22T10:07:00+07:00","price":5225.0},{"time":"2026-10-22T10:08:00+07:00","price":5225.0},{"time":"2026-10-22T10:09:00+07:00","price":5225.0},{"time":"2026-10-22T10:10:00+07:00","price":5225.0},{"time":"2026-10-22T10:11:00+07:00","price":5225.0},{"time":"2026-10-22T10:12:00+07:00","price":5225.0},{"time":"2026-10-22T10:13:00+07:00","price":5225.0},{"time":"2026-10-22T10:14:00+07:00","price":5225.0},{"time":"2026-10-22T10:15:00+07:00","price":5225.0},{"time":"2026-10-22T10:16:00+07:00","price":5225.0},{"time":"2026-10-22T10:17:00+07:00","price":5225.0},{"time":"2026-10-22T10:18:00+07:00","price":5225.0},{"time":"2026-10-22T10:19:00+07:00","price":5225.0},{"time":"2026-10-22T10:20:00+07:00","price":5225.0},{"time":"2026-10-22T10:21:00+07:00","price":5225.0},{"time":"2026-10-22T10:22:00+07:00","price":5250.0},{"time":"2026-10-22T10:23:00+07:00","price":5250.0},{"time":"2026-10-22T10:24:00+07:00","price":5250.0},{"time":"2026-10-22T10:25:00+07:00","price":5250.0},{"time":"2026-10-22T10:26:00+07:00","price":5250.0},{"time":"2026-10-22T10:27:00+07:00","price":5250.0},{"time":"2026-10-22T10:28:00+07:00","price":5250.0},{"time":"2026-10-22T10:29:00+07:00","price":5250.0},{"time":"2026-10-22T10:30:00+07:00","price":5225.0},{"time":"2026-10-22T10:31:00+07:00","price":5250.0},{"time":"2026-10-22T10:32:00+07:00","price":5250.0},{"time":"2026-10-22T10:33:00+07:00","price":5250.0},{"time":"2026-10-22T10:34:00+07:00","price":5225.0},{"time":"2026-10-22T10:35:00+07:00","price":5250.0},{"time":"2026-10-22T10:36:00+07:00","price":5250.0},{"time":"2026-10-22T10:37:00+07:00","price":5250.0},{"time":"2026-10-22T10:38:00+07:00","price":5225.0},{"time":"2026-10-22T10:39:00+07:00","price":5250.0},{"time":"2026-10-22T10:40:00+07:00","price":5250.0},{"time":"2026-10-22T10:41:00+07:00","price":5250.0},{"time":"2026-10-22T10:42:00+07:00","price":5225.0},{"time":"2026-10-22T10:43:00+07:00","price":5225.0},{"time":"2026-10-22T10:44:00+07:00","price":5200.0},{"time":"2026-10-22T10:45:00+07:00","price":5200.0},{"time":"2026-10-22T10:46:00+07:00","price":5200.0},{"time":"2026-10-22T10:47:00+07:00","price":5200.0},{"time":"2026-10-22T10:48:00+07:00","price":5225.0},{"time":"2026-10-22T10:49:00+07:00","price":5225.0},{"time":"2026-10-22T10:50:00+07:00","price":5225.0},{"time":"2026-10-22T10:51:00+07:00","price":5200.0},{"time":"2026-10-22T10:52:00+07:00","price":5225.0},{"time":"2026-10-22T10:53:00+07:00","price":5225.0},{"time":"2026-10-22T10:54:00+07:00","price":5200.0},{"time":"2026-10-22T10:55:00+07:00","price":5200.0},{"time":"2026-10-22T10:56:00+07:00","price":5225.0},{"time":"2026-10-22T10:57:00+07:00","price":5200.0},{"time":"2026-10-22T10:58:00+07:00","price":5200.0},{"time":"2026-10-22T10:59:00+07:00","price":5225.0},{"time":"2026-10-22T11:00:00+07:00","price":5200.0},{"time":"2026-10-22T11:01:00+07:00","price":5200.0},{"time":"2026-10-22T11:02:00+07:00","price":5225.0},{"time":"2026-10-22T11:03:00+07:00","price":5225.0},{"time":"2026-10-22T11:04:00+07:00","price":5200.0},{"time":"2026-10-22T11:05:00+07:00","price":5225.0},{"time":"2026-10-22T11:06:00+07:00","price":5250.0},{"time":"2026-10-22T11:07:00+07:00","price":5225.0},{"time":"2026-10-22T11:08:00+07:00","price":5225.0},{"time":"2026-10-22T11:09:00+07:00","price":5250.0},{"time":"2026-10-22T11:10:00+07:00","price":5250.0},{"time":"2026-10-22T11:11:00+07:00","price":5225.0},{"time":"2026-10-22T11:12:00+07:00","price":5250.0},{"time":"2026-10-22T11:13:00+07:00","price":5250.0},{"time":"2026-10-22T11:14:00+07:00","price":5250.0},{"time":"2026-10-22T11:15:00+07:00","price":5250.0},{"time":"2026-10-22T11:16:00+07:00","price":5250.0},{"time":"2026-10-22T11:17:00+07:00","price":5250.0},{"time":"2026-10-22T11:18:00+07:00","price":5225.0},{"time":"2026-10-22T11:19:00+07:00","price":5225.0},{"time":"2026-10-22T11:20:00+07:00","price":5225.0},{"time":"2026-10-22T11:21:00+07:00","price":5225.0},{"time":"2026-10-22T11:22:00+07:00","price":5200.0},{"time":"2026-10-22T11:23:00+07:00","price":5225.0},{"time":"2026-10-22T11:24:00+07:00","price":5200.0},{"time":"2026-10-22T11:25:00+07:00","price":5200.0},{"time":"2026-10-22T11:26:00+07:00","price":5225.0},{"time":"2026-10-22T11:27:00+07:00","price":5225.0},{"time":"2026-10-22T11:28:00+07:00","price":5200.0},{"time":"2026-10-22T11:29:00+07:00","price":5225.0},{"time":"2026-10-22T11:30:00+07:00","price":5225.0},{"time":"2026-10-22T11:31:00+07:00","price":5200.0},{"time":"2026-10-22T11:32:00+07:00","price":5200.0},{"time":"2026-10-22T11:33:00+07:00","price":5225.0},{"time":"2026-10-22T11:34:00+07:00","price":5225.0},{"time":"2026-10-22T11:35:00+07:00","price":5200.0},{"time":"2026-10-22T11:36:00+07:00","price":5225.0},{"time":"2026-10-22T11:37:00+07:00","price":5225.0},{"time":"2026-10-22T11:38:00+07:00","price":5225.0},{"time":"2026-10-22T11:39:00+07:00","price":5200.0},{"time":"2026-10-22T11:40:00+07:00","price":5225.0},{"time":"2026-10-22T11:41:00+07:00","price":5225.0},{"time":"2026-10-22T11:42:00+07:00","price":5200.0},{"time":"2026-10-22T11:43:00+07:00","price":5225.0},{"time":"2026-10-22T11:44:00+07:00","price":5200.0},{"time":"2026-10-22T11:45:00+07:00","price":5225.0},{"time":"2026-10-22T11:46:00+07:00","price":5225.0},{"time":"2026-10-22T11:47:00+07:00","price":5225.0},{"time":"2026-10-22T11:48:00+07:00","price":5250.0},{"time":"2026-10-22T11:49:00+07:00","price":5250.0},{"time":"2026-10-22T11:50:00+07:00","price":5225.0},{"time":"2026-10-22T11:51:00+07:00","price":5225.0},{"time":"2026-10-22T11:52:00+07:00","price":5225.0},{"time":"2026-10-22T11:53:00+07:00","price":5225.0},{"time":"2026-10-22T11:54:00+07:00","price":5225.0},{"time":"2026-10-22T11:55:00+07:00","price":5225.0},{"time":"2026-10-22T11:56:00+07:00","price":5225.0},{"time":"2026-10-22T11:57:00+07:00","price":5225.0},{"time":"2026-10-22T11:58:00+07:00","price":5250.0},{"time":"2026-10-22T11:59:00+07:00","price":5250.0},{"time":"2026-10-22T13:30:00+07:00","price":5225.0},{"time":"2026-10-22T13:31:00+07:00","price":5200.0},{"time":"2026-10-22T13:32:00+07:00","price":5200.0},{"time":"2026-10-22T13:33:00+07:00","price":5200.0},{"time":"2026-10-22T13:34:00+07:00","price":5200.0},{"time":"2026-10-22T13:35:00+07:00","price":5225.0},{"time":"2026-10-22T13:36:00+07:00","price":5225.0},{"time":"2026-10-22T13:37:00+07:00","price":5225.0},{"time":"2026-10-22T13:38:00+07:00","price":5200.0},{"time":"2026-10-22T13:39:00+07:00","price":5225.0},{"time":"2026-10-22T13:40:00+07:00","price":5200.0},{"time":"2026-10-22T13:41:00+07:00","price":5225.0},{"time":"2026-10-22T13:42:00+07:00","price":5225.0},{"time":"2026-10-22T13:43:00+07:00","price":5200.0},{"time":"2026-10-22T13:44:00+07:00","price":5200.0},{"time":"2026-10-22T13:45:00+07:00","price":5175.0},{"time":"2026-10-22T13:46:00+07:00","price":5175.0},{"time":"2026-10-22T13:47:00+07:00","price":5175.0},{"time":"2026-10-22T13:48:00+07:00","price":5175.0},{"time":"2026-10-22T13:49:00+07:00","price":5200.0},{"time":"2026-10-22T13:50:00+07:00","price":5200.0},{"time":"2026-10-22T13:51:00+07:00","price":5175.0},{"time":"2026-10-22T13:52:00+07:00","price":5200.0},{"time":"2026-10-22T13:53:00+07:00","price":5200.0},{"time":"2026-10-22T13:54:00+07:00","price":5175.0},{"time":"2026-10-22T13:55:00+07:00","price":5175.0},{"time":"2026-10-22T13:56:00+07:00","price":5175.0},{"time":"2026-10-22T13:57:00+07:00","price":5200.0},{"time":"2026-10-22T13:58:00+07:00","price":5200.0},{"time":"2026-10-22T13:59:00+07:00","price":5200.0},{"time":"2026-10-22T14:00:00+07:00","price":5200.0},{"time":"2026-10-22T14:01:00+07:00","price":5175.0},{"time":"2026-10-22T14:02:00+07:00","price":5200.0},{"time":"2026-10-22T14:03:00+07:00","price":5200.0},{"time":"2026-10-22T14:04:00+07:00","price":5175.0},{"time":"2026-10-22T14:05:00+07:00","price":5175.0},{"time":"2026-10-22T14:06:00+07:00","price":5175.0},{"time":"2026-10-22T14:07:00+07:00","price":5175.0},{"time":"2026-10-22T14:08:00+07:00","price":5175.0},{"time":"2026-10-22T14:09:00+07:00","price":5175.0},{"time":"2026-10-22T14:10:00+07:00","price":5175.0},{"time":"2026-10-22T14:11:00+07:00","price":5200.0},{"time":"2026-10-22T14:12:00+07:00","price":5200.0},{"time":"2026-10-22T14:13:00+07:00","price":5200.0},{"time":"2026-10-22T14:14:00+07:00","price":5175.0},{"time":"2026-10-22T14:15:00+07:00","price":5200.0},{"time":"2026-10-22T14:16:00+07:00","price":5175.0},{"time":"2026-10-22T14:17:00+07:00","price":5175.0},{"time":"2026-10-22T14:18:00+07:00","price":5175.0},{"time":"2026-10-22T14:19:00+07:00","price":5175.0},{"time":"2026-10-22T14:20:00+07:00","price":5200.0},{"time":"2026-10-22T14:21:00+07:00","price":5200.0},{"time":"2026-10-22T14:22:00+07:00","price":5175.0},{"time":"2026-10-22T14:23:00+07:00","price":5175.0},{"time":"2026-10-22T14:24:00+07:00","price":5200.0},{"time":"2026-10-22T14:25:00+07:00","price":5175.0},{"time":"2026-10-22T14:26:00+07:00","price":5200.0},{"time":"2026-10-22T14:27:00+07:00","price":5200.0},{"time":"2026-10-22T14:28:00+07:00","price":5200.0},{"time":"2026-10-22T14:29:00+07:00","price":5200.0},{"time":"2026-10-22T14:30:00+07:00","price":5175.0},{"time":"2026-10-22T14:31:00+07:00","price":5200.0},{"time":"2026-10-22T14:32:00+07:00","price":5175.0},{"time":"2026-10-22T14:33:00+07:00","price":5200.0},{"time":"2026-10-22T14:34:00+07:00","price":5200.0},{"time":"2026-10-22T14:35:00+07:00","price":5200.0},{"time":"2026-10-22T14:36:00+07:00","price":5225.0},{"time":"2026-10-22T14:37:00+07:00","price":5200.0},{"time":"2026-10-22T14:38:00+07:00","price":5200.0},{"time":"2026-10-22T14:39:00+07:00","price":5200.0},{"time":"2026-10-22T14:40:00+07:00","price":5200.0},{"time":"2026-10-22T14:41:00+07:00","price":5200.0},{"time":"2026-10-22T14:42:00+07:00","price":5200.0},{"time":"2026-10-22T14:43:00+07:00","price":5200.0},{"time":"2026-10-22T14:44:00+07:00","price":5200.0},{"time":"2026-10-22T14:45:00+07:00","price":5200.0},{"time":"2026-10-22T14:46:00+07:00","price":5200.0},{"time":"2026-10-22T14:47:00+07:00","price":5175.0},{"time":"2026-10-22T14:48:00+07:00","price":5175.0},{"time":"2026-10-22T14:49:00+07:00","price":5200.0},{"time":"2026-10-22T14:50:00+07:00","price":5200.0},{"time":"2026-10-22T14:51:00+07:00","price":5200.0},{"time":"2026-10-22T14:52:00+07:00","price":5175.0},{"time":"2026-10-22T14:53:00+07:00","price":5200.0},{"time":"2026-10-22T14:54:00+07:00","price":5200.0},{"time":"2026-10-22T14:55:00+07:00","price":5225.0},{"time":"2026-10-22T14:56:00+07:00","price":5225.0},{"time":"2026-10-22T14:57:00+07:00","price":5225.0},{"time":"2026-10-22T14:58:00+07:00","price":5200.0},{"time":"2026-10-22T14:59:00+07:00","price":5200.0},{"time":"2026-10-22T15:00:00+07:00","price":5175.0},{"time":"2026-10-22T15:01:00+07:00","price":5200.0},{"time":"2026-10-22T15:02:00+07:00","price":5200.0},{"time":"2026-10-22T15:03:00+07:00","price":5200.0},{"time":"2026-10-22T15:04:00+07:00","price":5200.0},{"time":"2026-10-22T15:05:00+07:00","price":5200.0},{"time":"2026-10-22T15:06:00+07:00","price":5200.0},{"time":"2026-10-22T15:07:00+07:00","price":5175.0},{"time":"2026-10-22T15:08:00+07:00","price":5200.0},{"time":"2026-10-22T15:09:00+07:00","price":5200.0},{"time":"2026-10-22T15:10:00+07:00","price":5175.0},{"time":"2026-10-22T15:11:00+07:00","price":5175.0},{"time":"2026-10-22T15:12:00+07:00","price":5175.0},{"time":"2026-10-22T15:13:00+07:00","price":5200.0},{"time":"2026-10-22T15:14:00+07:00","price":5175.0},{"time":"2026-10-22T15:15:00+07:00","price":5175.0},{"time":"2026-10-22T15:16:00+07:00","price":5175.0},{"time":"2026-10-22T15:17:00+07:00","price":5200.0},{"time":"2026-10-22T15:18:00+07:00","price":5175.0},{"time":"2026-10-22T15:19:00+07:00","price":5175.0},{"time":"2026-10-22T15:20:00+07:00","price":5150.0},{"time":"2026-10-22T15:21:00+07:00","price":5175.0},{"time":"2026-10-22T15:22:00+07:00","price":5175.0},{"time":"2026-10-22T15:23:00+07:00","price":5175.0},{"time":"2026-10-22T15:24:00+07:00","price":5175.0},{"time":"2026-10-22T15:25:00+07:00","price":5150.0},{"time":"2026-10-22T15:26:00+07:00","price":5175.0},{"time":"2026-10-22T15:27:00+07:00","price":5175.0},{"time":"2026-10-22T15:28:00+07:00","price":5175.0},{"time":"2026-10-22T15:29:00+07:00","price":5175.0},{"time":"2026-10-22T15:30:00+07:00","price":5200.0},{"time":"2026-10-22T15:31:00+07:00","price":5200.0},{"time":"2026-10-22T15:32:00+07:00","price":5200.0},{"time":"2026-10-22T15:33:00+07:00","price":5200.0},{"time":"2026-10-22T15:34:00+07:00","price":5200.0},{"time":"2026-10-22T15:35:00+07:00","price":5175.0},{"time":"2026-10-22T15:36:00+07:00","price":5200.0},{"time":"2026-10-22T15:37:00+07:00","price":5175.0},{"time":"2026-10-22T15:38:00+07:00","price":5175.0},{"time":"2026-10-22T15:39:00+07:00","price":5175.0},{"time":"2026-10-22T15:40:00+07:00","price":5150.0},{"time":"2026-10-22T15:41:00+07:00","price":5150.0},{"time":"2026-10-22T15:42:00+07:00","price":5175.0},{"time":"2026-10-22T15:43:00+07:00","price":5175.0},{"time":"2026-10-22T15:44:00+07:00","price":5200.0},{"time":"2026-10-22T15:45:00+07:00","price":5200.0},{"time":"2026-10-22T15:46:00+07:00","price":5175.0},{"time":"2026-10-22T15:47:00+07:00","price":5200.0},{"time":"2026-10-22T15:48:00+07:00","price":5175.0},{"time":"2026-10-22T15:49:00+07:00","price":5175.0},{"time":"2026-10-22T16:00:00+07:00","price":5175.0},{"time":"2026-10-22T16:02:00+07:00","price":5175.0},{"time":"2026-10-22T16:03:00+07:00","price":5175.0},{"time":"2026-10-22T16:04:00+07:00","price":5175.0},{"time":"2026-10-22T16:05:00+07:00","price":5175.0},{"time":"2026-10-22T16:06:00+07:00","price":5175.0},{"time":"2026-10-22T16:07:00+07:00","price":5175.0},{"time":"2026-10-22T16:08:00+07:00","price":5175.0},{"time":"2026-10-22T16:09:00+07:00","price":5175.0},{"time":"2026-10-22T16:10:00+07:00","price":5175.0},{"time":"2026-10-22T16:11:00+07:00","price":5175.0},{"time":"2026-10-22T16:12:00+07:00","price":5175.0},{"time":"2026-10-22T16:13:00+07:00","price":5175.0},{"time":"2026-10-22T16:14:00+07:00","price":5175.0}]} +{"quote":{"symbol":"BBCA.JK","price":5175,"change":-100,"change_pct":-1.9,"volume":286928500,"market_cap":null,"week52_high":null,"week52_low":null,"week52_position":null,"range_signal":null,"prev_close":5275,"avg_volume":null,"as_of":"2026-10-22T16:14:00+07:00","behind_session":true,"market_status":"close"},"points":[{"time":"2026-10-22T08:58:00+07:00","price":5125.0},{"time":"2026-10-22T09:00:00+07:00","price":5125.0},{"time":"2026-10-22T09:01:00+07:00","price":5175.0},{"time":"2026-10-22T09:02:00+07:00","price":5150.0},{"time":"2026-10-22T09:03:00+07:00","price":5175.0},{"time":"2026-10-22T09:04:00+07:00","price":5175.0},{"time":"2026-10-22T09:05:00+07:00","price":5175.0},{"time":"2026-10-22T09:06:00+07:00","price":5175.0},{"time":"2026-10-22T09:07:00+07:00","price":5200.0},{"time":"2026-10-22T09:08:00+07:00","price":5175.0},{"time":"2026-10-22T09:09:00+07:00","price":5200.0},{"time":"2026-10-22T09:10:00+07:00","price":5200.0},{"time":"2026-10-22T09:11:00+07:00","price":5200.0},{"time":"2026-10-22T09:12:00+07:00","price":5200.0},{"time":"2026-10-22T09:13:00+07:00","price":5200.0},{"time":"2026-10-22T09:14:00+07:00","price":5225.0},{"time":"2026-10-22T09:15:00+07:00","price":5200.0},{"time":"2026-10-22T09:16:00+07:00","price":5225.0},{"time":"2026-10-22T09:17:00+07:00","price":5225.0},{"time":"2026-10-22T09:18:00+07:00","price":5200.0},{"time":"2026-10-22T09:19:00+07:00","price":5225.0},{"time":"2026-10-22T09:20:00+07:00","price":5250.0},{"time":"2026-10-22T09:21:00+07:00","price":5225.0},{"time":"2026-10-22T09:22:00+07:00","price":5225.0},{"time":"2026-10-22T09:23:00+07:00","price":5225.0},{"time":"2026-10-22T09:24:00+07:00","price":5225.0},{"time":"2026-10-22T09:25:00+07:00","price":5200.0},{"time":"2026-10-22T09:26:00+07:00","price":5200.0},{"time":"2026-10-22T09:27:00+07:00","price":5225.0},{"time":"2026-10-22T09:28:00+07:00","price":5225.0},{"time":"2026-10-22T09:29:00+07:00","price":5250.0},{"time":"2026-10-22T09:30:00+07:00","price":5225.0},{"time":"2026-10-22T09:31:00+07:00","price":5225.0},{"time":"2026-10-22T09:32:00+07:00","price":5200.0},{"time":"2026-10-22T09:33:00+07:00","price":5225.0},{"time":"2026-10-22T09:34:00+07:00","price":5225.0},{"time":"2026-10-22T09:35:00+07:00","price":5225.0},{"time":"2026-10-22T09:36:00+07:00","price":5250.0},{"time":"2026-10-22T09:37:00+07:00","price":5225.0},{"time":"2026-10-22T09:38:00+07:00","price":5225.0},{"time":"2026-10-22T09:39:00+07:00","price":5225.0},{"time":"2026-10-22T09:40:00+07:00","price":5225.0},{"time":"2026-10-22T09:41:00+07:00","price":5225.0},{"time":"2026-10-22T09:42:00+07:00","price":5225.0},{"time":"2026-10-22T09:43:00+07:00","price":5225.0},{"time":"2026-10-22T09:44:00+07:00","price":5225.0},{"time":"2026-10-22T09:45:00+07:00","price":5200.0},{"time":"2026-10-22T09:46:00+07:00","price":5200.0},{"time":"2026-10-22T09:47:00+07:00","price":5225.0},{"time":"2026-10-22T09:48:00+07:00","price":5200.0},{"time":"2026-10-22T09:49:00+07:00","price":5225.0},{"time":"2026-10-22T09:50:00+07:00","price":5225.0},{"time":"2026-10-22T09:51:00+07:00","price":5225.0},{"time":"2026-10-22T09:52:00+07:00","price":5225.0},{"time":"2026-10-22T09:53:00+07:00","price":5225.0},{"time":"2026-10-22T09:54:00+07:00","price":5200.0},{"time":"2026-10-22T09:55:00+07:00","price":5225.0},{"time":"2026-10-22T09:56:00+07:00","price":5225.0},{"time":"2026-10-22T09:57:00+07:00","price":5225.0},{"time":"2026-10-22T09:58:00+07:00","price":5225.0},{"time":"2026-10-22T09:59:00+07:00","price":5225.0},{"time":"2026-10-22T10:00:00+07:00","price":5250.0},{"time":"2026-10-22T10:01:00+07:00","price":5250.0},{"time":"2026-10-22T10:02:00+07:00","price":5250.0},{"time":"2026-10-22T10:03:00+07:00","price":5250.0},{"time":"2026-10-22T10:04:00+07:00","price":5250.0},{"time":"2026-10-22T10:05:00+07:00","price":5225.0},{"time":"2026-10-22T10:06:00+07:00","price":5225.0},{"time":"2026-10-22T10:07:00+07:00","price":5225.0},{"time":"2026-10-22T10:08:00+07:00","price":5225.0},{"time":"2026-10-22T10:09:00+07:00","price":5225.0},{"time":"2026-10-22T10:10:00+07:00","price":5225.0},{"time":"2026-10-22T10:11:00+07:00","price":5225.0},{"time":"2026-10-22T10:12:00+07:00","price":5225.0},{"time":"2026-10-22T10:13:00+07:00","price":5225.0},{"time":"2026-10-22T10:14:00+07:00","price":5225.0},{"time":"2026-10-22T10:15:00+07:00","price":5225.0},{"time":"2026-10-22T10:16:00+07:00","price":5225.0},{"time":"2026-10-22T10:17:00+07:00","price":5225.0},{"time":"2026-10-22T10:18:00+07:00","price":5225.0},{"time":"2026-10-22T10:19:00+07:00","price":5225.0},{"time":"2026-10-22T10:20:00+07:00","price":5225.0},{"time":"2026-10-22T10:21:00+07:00","price":5225.0},{"time":"2026-10-22T10:22:00+07:00","price":5250.0},{"time":"2026-10-22T10:23:00+07:00","price":5250.0},{"time":"2026-10-22T10:24:00+07:00","price":5250.0},{"time":"2026-10-22T10:25:00+07:00","price":5250.0},{"time":"2026-10-22T10:26:00+07:00","price":5250.0},{"time":"2026-10-22T10:27:00+07:00","price":5250.0},{"time":"2026-10-22T10:28:00+07:00","price":5250.0},{"time":"2026-10-22T10:29:00+07:00","price":5250.0},{"time":"2026-10-22T10:30:00+07:00","price":5225.0},{"time":"2026-10-22T10:31:00+07:00","price":5250.0},{"time":"2026-10-22T10:32:00+07:00","price":5250.0},{"time":"2026-10-22T10:33:00+07:00","price":5250.0},{"time":"2026-10-22T10:34:00+07:00","price":5225.0},{"time":"2026-10-22T10:35:00+07:00","price":5250.0},{"time":"2026-10-22T10:36:00+07:00","price":5250.0},{"time":"2026-10-22T10:37:00+07:00","price":5250.0},{"time":"2026-10-22T10:38:00+07:00","price":5225.0},{"time":"2026-10-22T10:39:00+07:00","price":5250.0},{"time":"2026-10-22T10:40:00+07:00","price":5250.0},{"time":"2026-10-22T10:41:00+07:00","price":5250.0},{"time":"2026-10-22T10:42:00+07:00","price":5225.0},{"time":"2026-10-22T10:43:00+07:00","price":5225.0},{"time":"2026-10-22T10:44:00+07:00","price":5200.0},{"time":"2026-10-22T10:45:00+07:00","price":5200.0},{"time":"2026-10-22T10:46:00+07:00","price":5200.0},{"time":"2026-10-22T10:47:00+07:00","price":5200.0},{"time":"2026-10-22T10:48:00+07:00","price":5225.0},{"time":"2026-10-22T10:49:00+07:00","price":5225.0},{"time":"2026-10-22T10:50:00+07:00","price":5225.0},{"time":"2026-10-22T10:51:00+07:00","price":5200.0},{"time":"2026-10-22T10:52:00+07:00","price":5225.0},{"time":"2026-10-22T10:53:00+07:00","price":5225.0},{"time":"2026-10-22T10:54:00+07:00","price":5200.0},{"time":"2026-10-22T10:55:00+07:00","price":5200.0},{"time":"2026-10-22T10:56:00+07:00","price":5225.0},{"time":"2026-10-22T10:57:00+07:00","price":5200.0},{"time":"2026-10-22T10:58:00+07:00","price":5200.0},{"time":"2026-10-22T10:59:00+07:00","price":5225.0},{"time":"2026-10-22T11:00:00+07:00","price":5200.0},{"time":"2026-10-22T11:01:00+07:00","price":5200.0},{"time":"2026-10-22T11:02:00+07:00","price":5225.0},{"time":"2026-10-22T11:03:00+07:00","price":5225.0},{"time":"2026-10-22T11:04:00+07:00","price":5200.0},{"time":"2026-10-22T11:05:00+07:00","price":5225.0},{"time":"2026-10-22T11:06:00+07:00","price":5250.0},{"time":"2026-10-22T11:07:00+07:00","price":5225.0},{"time":"2026-10-22T11:08:00+07:00","price":5225.0},{"time":"2026-10-22T11:09:00+07:00","price":5250.0},{"time":"2026-10-22T11:10:00+07:00","price":5250.0},{"time":"2026-10-22T11:11:00+07:00","price":5225.0},{"time":"2026-10-22T11:12:00+07:00","price":5250.0},{"time":"2026-10-22T11:13:00+07:00","price":5250.0},{"time":"2026-10-22T11:14:00+07:00","price":5250.0},{"time":"2026-10-22T11:15:00+07:00","price":5250.0},{"time":"2026-10-22T11:16:00+07:00","price":5250.0},{"time":"2026-10-22T11:17:00+07:00","price":5250.0},{"time":"2026-10-22T11:18:00+07:00","price":5225.0},{"time":"2026-10-22T11:19:00+07:00","price":5225.0},{"time":"2026-10-22T11:20:00+07:00","price":5225.0},{"time":"2026-10-22T11:21:00+07:00","price":5225.0},{"time":"2026-10-22T11:22:00+07:00","price":5200.0},{"time":"2026-10-22T11:23:00+07:00","price":5225.0},{"time":"2026-10-22T11:24:00+07:00","price":5200.0},{"time":"2026-10-22T11:25:00+07:00","price":5200.0},{"time":"2026-10-22T11:26:00+07:00","price":5225.0},{"time":"2026-10-22T11:27:00+07:00","price":5225.0},{"time":"2026-10-22T11:28:00+07:00","price":5200.0},{"time":"2026-10-22T11:29:00+07:00","price":5225.0},{"time":"2026-10-22T11:30:00+07:00","price":5225.0},{"time":"2026-10-22T11:31:00+07:00","price":5200.0},{"time":"2026-10-22T11:32:00+07:00","price":5200.0},{"time":"2026-10-22T11:33:00+07:00","price":5225.0},{"time":"2026-10-22T11:34:00+07:00","price":5225.0},{"time":"2026-10-22T11:35:00+07:00","price":5200.0},{"time":"2026-10-22T11:36:00+07:00","price":5225.0},{"time":"2026-10-22T11:37:00+07:00","price":5225.0},{"time":"2026-10-22T11:38:00+07:00","price":5225.0},{"time":"2026-10-22T11:39:00+07:00","price":5200.0},{"time":"2026-10-22T11:40:00+07:00","price":5225.0},{"time":"2026-10-22T11:41:00+07:00","price":5225.0},{"time":"2026-10-22T11:42:00+07:00","price":5200.0},{"time":"2026-10-22T11:43:00+07:00","price":5225.0},{"time":"2026-10-22T11:44:00+07:00","price":5200.0},{"time":"2026-10-22T11:45:00+07:00","price":5225.0},{"time":"2026-10-22T11:46:00+07:00","price":5225.0},{"time":"2026-10-22T11:47:00+07:00","price":5225.0},{"time":"2026-10-22T11:48:00+07:00","price":5250.0},{"time":"2026-10-22T11:49:00+07:00","price":5250.0},{"time":"2026-10-22T11:50:00+07:00","price":5225.0},{"time":"2026-10-22T11:51:00+07:00","price":5225.0},{"time":"2026-10-22T11:52:00+07:00","price":5225.0},{"time":"2026-10-22T11:53:00+07:00","price":5225.0},{"time":"2026-10-22T11:54:00+07:00","price":5225.0},{"time":"2026-10-22T11:55:00+07:00","price":5225.0},{"time":"2026-10-22T11:56:00+07:00","price":5225.0},{"time":"2026-10-22T11:57:00+07:00","price":5225.0},{"time":"2026-10-22T11:58:00+07:00","price":5250.0},{"time":"2026-10-22T11:59:00+07:00","price":5250.0},{"time":"2026-10-22T13:30:00+07:00","price":5225.0},{"time":"2026-10-22T13:31:00+07:00","price":5200.0},{"time":"2026-10-22T13:32:00+07:00","price":5200.0},{"time":"2026-10-22T13:33:00+07:00","price":5200.0},{"time":"2026-10-22T13:34:00+07:00","price":5200.0},{"time":"2026-10-22T13:35:00+07:00","price":5225.0},{"time":"2026-10-22T13:36:00+07:00","price":5225.0},{"time":"2026-10-22T13:37:00+07:00","price":5225.0},{"time":"2026-10-22T13:38:00+07:00","price":5200.0},{"time":"2026-10-22T13:39:00+07:00","price":5225.0},{"time":"2026-10-22T13:40:00+07:00","price":5200.0},{"time":"2026-10-22T13:41:00+07:00","price":5225.0},{"time":"2026-10-22T13:42:00+07:00","price":5225.0},{"time":"2026-10-22T13:43:00+07:00","price":5200.0},{"time":"2026-10-22T13:44:00+07:00","price":5200.0},{"time":"2026-10-22T13:45:00+07:00","price":5175.0},{"time":"2026-10-22T13:46:00+07:00","price":5175.0},{"time":"2026-10-22T13:47:00+07:00","price":5175.0},{"time":"2026-10-22T13:48:00+07:00","price":5175.0},{"time":"2026-10-22T13:49:00+07:00","price":5200.0},{"time":"2026-10-22T13:50:00+07:00","price":5200.0},{"time":"2026-10-22T13:51:00+07:00","price":5175.0},{"time":"2026-10-22T13:52:00+07:00","price":5200.0},{"time":"2026-10-22T13:53:00+07:00","price":5200.0},{"time":"2026-10-22T13:54:00+07:00","price":5175.0},{"time":"2026-10-22T13:55:00+07:00","price":5175.0},{"time":"2026-10-22T13:56:00+07:00","price":5175.0},{"time":"2026-10-22T13:57:00+07:00","price":5200.0},{"time":"2026-10-22T13:58:00+07:00","price":5200.0},{"time":"2026-10-22T13:59:00+07:00","price":5200.0},{"time":"2026-10-22T14:00:00+07:00","price":5200.0},{"time":"2026-10-22T14:01:00+07:00","price":5175.0},{"time":"2026-10-22T14:02:00+07:00","price":5200.0},{"time":"2026-10-22T14:03:00+07:00","price":5200.0},{"time":"2026-10-22T14:04:00+07:00","price":5175.0},{"time":"2026-10-22T14:05:00+07:00","price":5175.0},{"time":"2026-10-22T14:06:00+07:00","price":5175.0},{"time":"2026-10-22T14:07:00+07:00","price":5175.0},{"time":"2026-10-22T14:08:00+07:00","price":5175.0},{"time":"2026-10-22T14:09:00+07:00","price":5175.0},{"time":"2026-10-22T14:10:00+07:00","price":5175.0},{"time":"2026-10-22T14:11:00+07:00","price":5200.0},{"time":"2026-10-22T14:12:00+07:00","price":5200.0},{"time":"2026-10-22T14:13:00+07:00","price":5200.0},{"time":"2026-10-22T14:14:00+07:00","price":5175.0},{"time":"2026-10-22T14:15:00+07:00","price":5200.0},{"time":"2026-10-22T14:16:00+07:00","price":5175.0},{"time":"2026-10-22T14:17:00+07:00","price":5175.0},{"time":"2026-10-22T14:18:00+07:00","price":5175.0},{"time":"2026-10-22T14:19:00+07:00","price":5175.0},{"time":"2026-10-22T14:20:00+07:00","price":5200.0},{"time":"2026-10-22T14:21:00+07:00","price":5200.0},{"time":"2026-10-22T14:22:00+07:00","price":5175.0},{"time":"2026-10-22T14:23:00+07:00","price":5175.0},{"time":"2026-10-22T14:24:00+07:00","price":5200.0},{"time":"2026-10-22T14:25:00+07:00","price":5175.0},{"time":"2026-10-22T14:26:00+07:00","price":5200.0},{"time":"2026-10-22T14:27:00+07:00","price":5200.0},{"time":"2026-10-22T14:28:00+07:00","price":5200.0},{"time":"2026-10-22T14:29:00+07:00","price":5200.0},{"time":"2026-10-22T14:30:00+07:00","price":5175.0},{"time":"2026-10-22T14:31:00+07:00","price":5200.0},{"time":"2026-10-22T14:32:00+07:00","price":5175.0},{"time":"2026-10-22T14:33:00+07:00","price":5200.0},{"time":"2026-10-22T14:34:00+07:00","price":5200.0},{"time":"2026-10-22T14:35:00+07:00","price":5200.0},{"time":"2026-10-22T14:36:00+07:00","price":5225.0},{"time":"2026-10-22T14:37:00+07:00","price":5200.0},{"time":"2026-10-22T14:38:00+07:00","price":5200.0},{"time":"2026-10-22T14:39:00+07:00","price":5200.0},{"time":"2026-10-22T14:40:00+07:00","price":5200.0},{"time":"2026-10-22T14:41:00+07:00","price":5200.0},{"time":"2026-10-22T14:42:00+07:00","price":5200.0},{"time":"2026-10-22T14:43:00+07:00","price":5200.0},{"time":"2026-10-22T14:44:00+07:00","price":5200.0},{"time":"2026-10-22T14:45:00+07:00","price":5200.0},{"time":"2026-10-22T14:46:00+07:00","price":5200.0},{"time":"2026-10-22T14:47:00+07:00","price":5175.0},{"time":"2026-10-22T14:48:00+07:00","price":5175.0},{"time":"2026-10-22T14:49:00+07:00","price":5200.0},{"time":"2026-10-22T14:50:00+07:00","price":5200.0},{"time":"2026-10-22T14:51:00+07:00","price":5200.0},{"time":"2026-10-22T14:52:00+07:00","price":5175.0},{"time":"2026-10-22T14:53:00+07:00","price":5200.0},{"time":"2026-10-22T14:54:00+07:00","price":5200.0},{"time":"2026-10-22T14:55:00+07:00","price":5225.0},{"time":"2026-10-22T14:56:00+07:00","price":5225.0},{"time":"2026-10-22T14:57:00+07:00","price":5225.0},{"time":"2026-10-22T14:58:00+07:00","price":5200.0},{"time":"2026-10-22T14:59:00+07:00","price":5200.0},{"time":"2026-10-22T15:00:00+07:00","price":5175.0},{"time":"2026-10-22T15:01:00+07:00","price":5200.0},{"time":"2026-10-22T15:02:00+07:00","price":5200.0},{"time":"2026-10-22T15:03:00+07:00","price":5200.0},{"time":"2026-10-22T15:04:00+07:00","price":5200.0},{"time":"2026-10-22T15:05:00+07:00","price":5200.0},{"time":"2026-10-22T15:06:00+07:00","price":5200.0},{"time":"2026-10-22T15:07:00+07:00","price":5175.0},{"time":"2026-10-22T15:08:00+07:00","price":5200.0},{"time":"2026-10-22T15:09:00+07:00","price":5200.0},{"time":"2026-10-22T15:10:00+07:00","price":5175.0},{"time":"2026-10-22T15:11:00+07:00","price":5175.0},{"time":"2026-10-22T15:12:00+07:00","price":5175.0},{"time":"2026-10-22T15:13:00+07:00","price":5200.0},{"time":"2026-10-22T15:14:00+07:00","price":5175.0},{"time":"2026-10-22T15:15:00+07:00","price":5175.0},{"time":"2026-10-22T15:16:00+07:00","price":5175.0},{"time":"2026-10-22T15:17:00+07:00","price":5200.0},{"time":"2026-10-22T15:18:00+07:00","price":5175.0},{"time":"2026-10-22T15:19:00+07:00","price":5175.0},{"time":"2026-10-22T15:20:00+07:00","price":5150.0},{"time":"2026-10-22T15:21:00+07:00","price":5175.0},{"time":"2026-10-22T15:22:00+07:00","price":5175.0},{"time":"2026-10-22T15:23:00+07:00","price":5175.0},{"time":"2026-10-22T15:24:00+07:00","price":5175.0},{"time":"2026-10-22T15:25:00+07:00","price":5150.0},{"time":"2026-10-22T15:26:00+07:00","price":5175.0},{"time":"2026-10-22T15:27:00+07:00","price":5175.0},{"time":"2026-10-22T15:28:00+07:00","price":5175.0},{"time":"2026-10-22T15:29:00+07:00","price":5175.0},{"time":"2026-10-22T15:30:00+07:00","price":5200.0},{"time":"2026-10-22T15:31:00+07:00","price":5200.0},{"time":"2026-10-22T15:32:00+07:00","price":5200.0},{"time":"2026-10-22T15:33:00+07:00","price":5200.0},{"time":"2026-10-22T15:34:00+07:00","price":5200.0},{"time":"2026-10-22T15:35:00+07:00","price":5175.0},{"time":"2026-10-22T15:36:00+07:00","price":5200.0},{"time":"2026-10-22T15:37:00+07:00","price":5175.0},{"time":"2026-10-22T15:38:00+07:00","price":5175.0},{"time":"2026-10-22T15:39:00+07:00","price":5175.0},{"time":"2026-10-22T15:40:00+07:00","price":5150.0},{"time":"2026-10-22T15:41:00+07:00","price":5150.0},{"time":"2026-10-22T15:42:00+07:00","price":5175.0},{"time":"2026-10-22T15:43:00+07:00","price":5175.0},{"time":"2026-10-22T15:44:00+07:00","price":5200.0},{"time":"2026-10-22T15:45:00+07:00","price":5200.0},{"time":"2026-10-22T15:46:00+07:00","price":5175.0},{"time":"2026-10-22T15:47:00+07:00","price":5200.0},{"time":"2026-10-22T15:48:00+07:00","price":5175.0},{"time":"2026-10-22T15:49:00+07:00","price":5175.0},{"time":"2026-10-22T16:00:00+07:00","price":5175.0},{"time":"2026-10-22T16:02:00+07:00","price":5175.0},{"time":"2026-10-22T16:03:00+07:00","price":5175.0},{"time":"2026-10-22T16:04:00+07:00","price":5175.0},{"time":"2026-10-22T16:05:00+07:00","price":5175.0},{"time":"2026-10-22T16:06:00+07:00","price":5175.0},{"time":"2026-10-22T16:07:00+07:00","price":5175.0},{"time":"2026-10-22T16:08:00+07:00","price":5175.0},{"time":"2026-10-22T16:09:00+07:00","price":5175.0},{"time":"2026-10-22T16:10:00+07:00","price":5175.0},{"time":"2026-10-22T16:11:00+07:00","price":5175.0},{"time":"2026-10-22T16:12:00+07:00","price":5175.0},{"time":"2026-10-22T16:13:00+07:00","price":5175.0},{"time":"2026-10-22T16:14:00+07:00","price":5175.0}]} --- stderr diff --git a/crates/idx-cli/tests/golden/stocks-intraday.table.golden b/crates/idx-cli/tests/golden/stocks-intraday.table.golden index 35c6cf8..303612a 100644 --- a/crates/idx-cli/tests/golden/stocks-intraday.table.golden +++ b/crates/idx-cli/tests/golden/stocks-intraday.table.golden @@ -2,6 +2,7 @@ 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 +Note: BBCA.JK last traded on 2026-10-22; this price is not current. ▄▅▇█▅▇▇▇▅▇▇▇▇▇▇██▇▅▇▅▅███▇▅▇▅▇▇▇▅▇▅▅▄▅▄▅▄▄▅▅▅▅▅▇▅▄▄▄▄▄▄▂▄▄▄▄ 08:58 to 16:14 WIB · low 5,125 · high 5,250 · 335 minutes diff --git a/crates/idx-core/src/analysis/mod.rs b/crates/idx-core/src/analysis/mod.rs index c20672c..6b985c0 100644 --- a/crates/idx-core/src/analysis/mod.rs +++ b/crates/idx-core/src/analysis/mod.rs @@ -128,22 +128,24 @@ impl Analysis<'_> { policy: CachePolicy, ) -> Result, IdxError> { let idx = self.idx; - let provider = history_provider(source.provider, idx.config.verbose); - let bucket = format!("{}-technical", source.provider.as_str()); let resolved = idx.resolve(symbol)?; - cached_fetch( - &idx.cache, - CacheSpec { - bucket: &bucket, - key: &resolved, - ttl_secs: idx.config.quote_ttl, - subject: &resolved, - source: source.provider.as_str(), - }, - policy, - || provider.history(&resolved, &Period::OneYear, &Interval::Day), - |history| build_technical_report(&resolved, &history), - ) + idx.market().with_history_fallback(source, |source| { + let provider = history_provider(source.provider, idx.config.verbose); + let bucket = format!("{}-technical", source.provider.as_str()); + cached_fetch( + &idx.cache, + CacheSpec { + bucket: &bucket, + key: &resolved, + ttl_secs: idx.config.quote_ttl, + subject: &resolved, + source: source.provider.as_str(), + }, + policy, + || provider.history(&resolved, &Period::OneYear, &Interval::Day), + |history| build_technical_report(&resolved, &history), + ) + }) } pub fn growth( diff --git a/crates/idx-core/src/api/mock.rs b/crates/idx-core/src/api/mock.rs index 6a76d4d..393359a 100644 --- a/crates/idx-core/src/api/mock.rs +++ b/crates/idx-core/src/api/mock.rs @@ -145,7 +145,8 @@ impl HistoryProvider for MockProvider { /// `stockbit_exodus_chart_candle__.json`, with the market state /// from `stockbit_exodus_market_session.json`. 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. +/// status instead (`401` on the session API: Stockbit refused the session), +/// and `IDX_MOCK_ERROR` fails as for the other providers. pub struct MockStockbitProvider; impl MockStockbitProvider { @@ -180,6 +181,11 @@ impl MockStockbitProvider { if !stockbit::has_session() { return Err(IdxError::AuthError(stockbit::auth::NO_SESSION.to_string())); } + if std::env::var("IDX_MOCK_STOCKBIT_STATUS").as_deref() == Ok("401") { + return Err(IdxError::AuthError( + "Stockbit rejected the session; run `idx stockbit login` again".to_string(), + )); + } if let Some(err) = self.failure(symbol) { return Err(err); } diff --git a/crates/idx-core/src/api/stockbit/browser.rs b/crates/idx-core/src/api/stockbit/browser.rs index 51f7a9c..8db593e 100644 --- a/crates/idx-core/src/api/stockbit/browser.rs +++ b/crates/idx-core/src/api/stockbit/browser.rs @@ -55,6 +55,12 @@ pub const PROFILE_DIR: &str = "stockbit-browser"; const START_TIMEOUT: Duration = Duration::from_secs(20); /// Pause between cookie reads. const POLL: Duration = Duration::from_millis(500); +/// How long a session check that failed for a passing reason (network, rate +/// limit) waits before the same cookie is checked again. +#[cfg(not(test))] +const VERIFY_RETRY: Duration = Duration::from_secs(5); +#[cfg(test)] +const VERIFY_RETRY: Duration = Duration::from_millis(50); /// Socket timeout for one DevTools message. const IO_TIMEOUT: Duration = Duration::from_secs(10); /// How long the browser gets to quit after `Browser.close`. @@ -228,6 +234,8 @@ fn wait_for_session( ) -> Result<(), IdxError> { let deadline = Instant::now() + timeout; let mut rejected: Option = None; + // After a check fails for a passing reason, the next is held back. + let mut retry_at: Option = None; let mut last_error: Option = None; // The window is watched so closing it ends the wait instead of running // out the clock; the first polls may not see a page yet. @@ -235,7 +243,10 @@ fn wait_for_session( let mut misses = 0; loop { match devtools.session_cookie() { - Ok(Some(cookie)) if rejected.as_deref() != Some(cookie.value.as_str()) => { + Ok(Some(cookie)) + if rejected.as_deref() != Some(cookie.value.as_str()) + && retry_at.is_none_or(|at| Instant::now() >= at) => + { match verify(&cookie.value) { Ok(()) => { // The session now lives in idx's own file. Take the @@ -250,12 +261,20 @@ fn wait_for_session( devtools.close(); return Ok(()); } + Err(err) if super::check_is_transient(&err) => { + crate::runtime::warn(format!( + "could not check the session in idx's browser yet ({err}); \ + retrying" + )); + retry_at = Some(Instant::now() + VERIFY_RETRY); + } Err(err) => { crate::runtime::warn(format!( "Stockbit refused the session in idx's browser ({err}); sign in \ again in that window" )); rejected = Some(cookie.value); + retry_at = None; } } } @@ -979,6 +998,33 @@ mod tests { let _ = std::fs::remove_dir_all(&profile); } + /// A check that fails for a passing reason (a timeout, a 429) is not a + /// refusal: the same cookie is checked again and can still sign in. + #[test] + fn a_check_that_fails_in_passing_is_retried() { + let profile = temp_dir("transient"); + let browser = FakeBrowser::start(Fake::default()); + write_endpoint(&profile, browser.port); + let mut devtools = connect(&profile, &|| true).unwrap(); + let checks = std::cell::Cell::new(0); + wait_for_session( + &mut devtools, + Duration::from_secs(5), + &|_| { + checks.set(checks.get() + 1); + match checks.get() { + 1 => Err(IdxError::RateLimited), + _ => Ok(()), + } + }, + &|| true, + ) + .unwrap(); + assert_eq!(checks.get(), 2); + browser.finish(); + let _ = std::fs::remove_dir_all(&profile); + } + /// Closing the window ends the wait at once, not at the timeout. #[test] fn closing_the_window_ends_the_wait() { diff --git a/crates/idx-core/src/api/stockbit/exodus.rs b/crates/idx-core/src/api/stockbit/exodus.rs index ddd0fca..051f4b3 100644 --- a/crates/idx-core/src/api/stockbit/exodus.rs +++ b/crates/idx-core/src/api/stockbit/exodus.rs @@ -355,6 +355,9 @@ pub fn parse_orderbook( } /// A quote from the order book; bid/offer only while the market is open. +/// Its `market_status` is the public page's vocabulary, `open` or `close`, +/// so a quote reads the same with or without a login; the order book keeps +/// Stockbit's state verbatim. pub fn quote_from_book(book: &OrderBook) -> Quote { let open = book.market_status.as_deref().is_some_and(is_trading); let best = |levels: &[BookLevel]| open.then(|| levels.first().map(|l| l.price)).flatten(); @@ -375,7 +378,10 @@ pub fn quote_from_book(book: &OrderBook) -> Quote { behind_session: false, bid: best(&book.bids), offer: best(&book.offers), - market_status: book.market_status.clone(), + market_status: book + .market_status + .as_ref() + .map(|_| if open { "open" } else { "close" }.to_string()), session_note: book.session_note.clone(), } } @@ -518,11 +524,14 @@ mod tests { let open = quote_from_book(&book(trading)); assert_eq!((open.bid, open.offer), (Some(4850), Some(4860))); assert_eq!(open.price, 4860); + assert_eq!(open.market_status.as_deref(), Some("open")); for state in ["market_session_break.json", "market_session.json"] { let shut = quote_from_book(&book(parse_market_status(&fixture(state)))); assert_eq!((shut.bid, shut.offer), (None, None), "{state}"); + assert_eq!(shut.market_status.as_deref(), Some("close"), "{state}"); } - assert_eq!(quote_from_book(&book(None)).bid, None); + let unknown = quote_from_book(&book(None)); + assert_eq!((unknown.bid, unknown.market_status), (None, None)); } #[test] diff --git a/crates/idx-core/src/api/stockbit/loopback.rs b/crates/idx-core/src/api/stockbit/loopback.rs index b154df3..f18e2f8 100644 --- a/crates/idx-core/src/api/stockbit/loopback.rs +++ b/crates/idx-core/src/api/stockbit/loopback.rs @@ -72,13 +72,24 @@ impl LoginPage { /// /// Each connection gets its own thread: browsers open idle preconnects, /// and answering them one at a time used to hold the page back for the - /// read timeout on every one. + /// read timeout on every one. Pastes are checked one at a time, and none + /// after one is accepted: two at once (a double click, the bookmarklet + /// and the clipboard button) would each store a session, and the later + /// could overwrite the one Stockbit had just rotated. pub fn wait(&self, timeout: Duration, verify: F) -> Result<(), IdxError> where F: Fn(&str) -> Result<(), IdxError> + Send + Sync + 'static, { let (done_tx, done_rx) = std::sync::mpsc::channel::<()>(); - let verify = std::sync::Arc::new(verify); + let accepted = std::sync::Mutex::new(false); + let verify = std::sync::Arc::new(move |paste: &str| { + let mut accepted = accepted.lock().unwrap_or_else(|e| e.into_inner()); + if !*accepted { + verify(paste)?; + *accepted = true; + } + Ok(()) + }); let page = std::sync::Arc::new(self.page()); let path = std::sync::Arc::new(format!("/login/{}", self.secret)); let busy = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); @@ -575,6 +586,37 @@ mod tests { (status, body) } + /// Two pastes at once are checked one at a time, and the second is not + /// checked at all once the first is accepted. + #[test] + fn concurrent_pastes_are_checked_once() { + let page = LoginPage::bind(0).unwrap(); + let url = page.url(); + let checks = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let counted = checks.clone(); + let handle = std::thread::spawn(move || { + page.wait(Duration::from_secs(10), move |_| { + counted.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + std::thread::sleep(Duration::from_millis(300)); + Ok(()) + }) + }); + std::thread::sleep(Duration::from_millis(100)); + let clients: Vec<_> = (0..2) + .map(|_| { + let url = url.clone(); + std::thread::spawn(move || request(&url, "POST", "good")) + }) + .collect(); + for client in clients { + let (status, body) = client.join().unwrap(); + assert_eq!(status, 200); + assert!(body.contains("\"ok\":true"), "{body}"); + } + handle.join().unwrap().unwrap(); + assert_eq!(checks.load(std::sync::atomic::Ordering::SeqCst), 1); + } + #[test] fn serves_the_page_only_with_the_secret_and_accepts_a_good_paste() { let page = LoginPage::bind(0).unwrap(); diff --git a/crates/idx-core/src/api/stockbit/mod.rs b/crates/idx-core/src/api/stockbit/mod.rs index a00abf1..f5b4873 100644 --- a/crates/idx-core/src/api/stockbit/mod.rs +++ b/crates/idx-core/src/api/stockbit/mod.rs @@ -265,6 +265,11 @@ pub(crate) fn fundamentals_unsupported(symbol: &str) -> IdxError { /// Stores a pasted session (stockbit.com's `credentialStorage` cookie or a /// refresh token) and checks it with one request, which refreshes it when the /// access token is missing or stale. A session Stockbit refuses is removed. +/// +/// A check that fails another way (network, rate limit) puts the previous +/// session back too, unless Stockbit already rotated the pasted one: the +/// refresh spent the pasted token, so the stored pair is then the only +/// working copy, and a refresh Stockbit answered is proof enough. pub fn login(pasted: &str) -> Result { let session = auth::session_from_paste(pasted).ok_or_else(|| { IdxError::InvalidInput( @@ -277,17 +282,40 @@ pub fn login(pasted: &str) -> Result { // A refused paste must not lose a session that already worked. let previous = std::fs::read(store.path()).ok(); store.save(&session)?; + let saved = std::fs::read(store.path()).ok(); #[cfg(feature = "testing")] let check = !crate::api::mock::enabled(); #[cfg(not(feature = "testing"))] let check = true; if check && let Err(err) = Exodus::new(store.clone()).verify() { - restore(&store, previous); - return Err(err); + let rotated = std::fs::read(store.path()).ok() != saved; + if !keep_after_failed_check(&err, rotated) { + restore(&store, previous); + return Err(err); + } + crate::runtime::warn(format!( + "Stockbit accepted the session and rotated it, but the check after that failed \ + ({err}); the new session is stored" + )); } Ok(store.status(chrono::Utc::now().timestamp())) } +/// Whether `login` keeps the stored session after its check failed with +/// `err`: only when the check rotated it and Stockbit did not then refuse it. +fn keep_after_failed_check(err: &IdxError, rotated: bool) -> bool { + rotated && !matches!(err, IdxError::AuthError(_)) +} + +/// Whether a session check failed for a reason that says nothing about the +/// session itself, so the same session may be checked again later. +pub(crate) fn check_is_transient(err: &IdxError) -> bool { + matches!( + err, + IdxError::Http(_) | IdxError::RateLimited | IdxError::UpstreamBlocked(_) + ) +} + fn restore(store: &SessionStore, previous: Option>) { match previous { Some(bytes) => { @@ -314,7 +342,24 @@ pub fn logout() -> Result<(bool, auth::SessionStatus), IdxError> { #[cfg(test)] mod tests { - use super::USER_AGENT; + use super::{USER_AGENT, check_is_transient, keep_after_failed_check}; + use crate::error::IdxError; + + #[test] + fn login_keeps_a_rotated_session_only_when_stockbit_did_not_refuse_it() { + let network = IdxError::Http("timeout".to_string()); + let refused = IdxError::AuthError("expired".to_string()); + assert!(keep_after_failed_check(&network, true)); + assert!(!keep_after_failed_check(&network, false)); + assert!(!keep_after_failed_check(&refused, true)); + assert!(!keep_after_failed_check(&refused, false)); + assert!(check_is_transient(&IdxError::RateLimited)); + assert!(check_is_transient(&network)); + assert!(!check_is_transient(&refused)); + assert!(!check_is_transient(&IdxError::InvalidInput( + "x".to_string() + ))); + } /// CloudFront answers HTTP 403 to agents containing these (probed /// 2026-10-22); the honest agent must never match one. diff --git a/crates/idx-core/src/api/types.rs b/crates/idx-core/src/api/types.rs index 2d7d1b1..6f4e854 100644 --- a/crates/idx-core/src/api/types.rs +++ b/crates/idx-core/src/api/types.rs @@ -65,8 +65,10 @@ pub struct Quote { /// Best offer in IDR (whole Rupiah), gated like `bid`. #[serde(default, skip_serializing_if = "Option::is_none")] pub offer: Option, - /// The provider's own session state, verbatim (Stockbit - /// `market_hour.status`, e.g. `close`). Absent for providers without one. + /// Stockbit's market state as its public page words it: `open` while + /// the regular market trades, `close` otherwise (also the lunch break + /// and auctions). A logged-in quote maps its session state to the same + /// two words. Absent for other providers, or when the state is unknown. #[serde(default, skip_serializing_if = "Option::is_none")] pub market_status: Option, /// What the provider says about this symbol's session: suspended, diff --git a/crates/idx-core/src/market.rs b/crates/idx-core/src/market.rs index 151ad49..5282919 100644 --- a/crates/idx-core/src/market.rs +++ b/crates/idx-core/src/market.rs @@ -77,7 +77,7 @@ impl Market<'_> { let idx = self.idx; let resolved = idx.resolve(symbol)?; let provider = intraday_provider(); - cached_fetch( + let mut day = cached_fetch( &idx.cache, CacheSpec { bucket: "stockbit-intraday", @@ -89,7 +89,11 @@ impl Market<'_> { policy, || provider.intraday(&resolved), Ok, - ) + )?; + // Stockbit's page keeps the last session's chart up after the close, + // so a cached or lagging day reads as current unless it is marked. + mark_behind_session(&mut day.data.quote, latest_session(Utc::now())); + Ok(day) } /// Resolves the history source for an optional `--history-provider` @@ -123,23 +127,49 @@ impl Market<'_> { policy: CachePolicy, ) -> Result>, IdxError> { let idx = self.idx; - let provider = history_provider(source.provider, idx.config.verbose); - let bucket = format!("{}-history", source.provider.as_str()); let resolved = idx.resolve(symbol)?; let key = format!("{resolved}-{}-{}", period.as_str(), interval.as_str()); - cached_fetch( - &idx.cache, - CacheSpec { - bucket: &bucket, - key: &key, - ttl_secs: idx.config.quote_ttl, - subject: &resolved, - source: source.provider.as_str(), - }, - policy, - || provider.history(&resolved, period, interval), - Ok, - ) + self.with_history_fallback(source, |source| { + let provider = history_provider(source.provider, idx.config.verbose); + let bucket = format!("{}-history", source.provider.as_str()); + cached_fetch( + &idx.cache, + CacheSpec { + bucket: &bucket, + key: &key, + ttl_secs: idx.config.quote_ttl, + subject: &resolved, + source: source.provider.as_str(), + }, + policy, + || provider.history(&resolved, period, interval), + Ok, + ) + }) + } + + /// Runs `fetch` on `source`; when `auto` picked Stockbit because a + /// session file exists and Stockbit then refuses that session, runs it + /// again on Yahoo instead of failing (`has_session` sees only the file). + pub(crate) fn with_history_fallback( + &self, + source: HistorySource, + fetch: impl Fn(HistorySource) -> Result, IdxError>, + ) -> Result, IdxError> { + match fetch(source) { + Err(IdxError::AuthError(reason)) + if source.mode == HistoryProviderKind::Auto + && source.provider == ProviderKind::Stockbit => + { + crate::runtime::warn(format!("{reason}; history falls back to yahoo meanwhile")); + fetch(HistorySource { + provider: ProviderKind::Yahoo, + is_fallback: self.idx.provider.kind() != ProviderKind::Yahoo, + ..source + }) + } + other => other, + } } /// The order book of one IDX stock from the user's Stockbit session, diff --git a/crates/idx-mcp/src/server.rs b/crates/idx-mcp/src/server.rs index bcf375c..23bbffa 100644 --- a/crates/idx-mcp/src/server.rs +++ b/crates/idx-mcp/src/server.rs @@ -125,8 +125,8 @@ impl IdxServer { 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 \ +runs with the stockbit provider, quotes also carry `market_status` (\"open\" while the \ +regular market trades, else \"close\", with or without a login), `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." )] @@ -148,8 +148,9 @@ minutes." `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." +`session_note`), not an error. `quote.behind_session` is true when the chart is from \ +before the latest IDX session (a holiday, or Stockbit lagging), as in get_quotes. \ +UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once." )] async fn get_intraday(&self, params: Parameters) -> ToolResult { let IntradayParams { symbol, tail } = params.0; diff --git a/crates/idx-mcp/tests/golden/tools.json b/crates/idx-mcp/tests/golden/tools.json index b4a8878..bdec564 100644 --- a/crates/idx-mcp/tests/golden/tools.json +++ b/crates/idx-mcp/tests/golden/tools.json @@ -373,7 +373,7 @@ "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.", + "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. `quote.behind_session` is true when the chart is from before the latest IDX session (a holiday, or Stockbit lagging), as in get_quotes. 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": { @@ -536,7 +536,7 @@ "name": "get_price_history" }, { - "description": "Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. 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.", + "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` (\"open\" while the regular market trades, else \"close\", with or without a login), `bid`/`offer` (best price, only while the market is open) and `session_note` (suspended, delisted, no trades); Stockbit data is delayed at least 10 minutes.", "inputSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 0997cd4..f36c6f0 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -82,7 +82,7 @@ WIB as the fallback. `behind_session` when `as_of` is before the latest weekday whose session has ended (16:15 WIB). It is computed on return, not cached; MSN can lag the close by hours, and on an exchange holiday every quote reads as behind. Only `.JK` - quotes and `--region id` screens are marked; other exchanges keep other hours. + quotes, `--region id` screens and `stocks intraday` are marked; other exchanges keep other hours. Table output notes behind quotes, and calls those over seven days old not current. - `change`, `change_pct`, and `volume` are nullable when the provider omits them; diff --git a/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md index 5c54f8f..3c2493b 100644 --- a/web/src/content/docs/cli/stockbit.md +++ b/web/src/content/docs/cli/stockbit.md @@ -197,6 +197,7 @@ What the login adds: | --- | --- | --- | | `--provider stockbit` quotes | public page, delayed | Stockbit's API, live | | Bid and offer in quotes | best price, while open | best price, while open | +| `market_status` in quotes | `open` or `close` | `open` or `close` | | `stocks orderbook` | `AUTHERROR` | every level with lots and queued orders, OHLC, value, trades; foreign buy/sell/net once Stockbit marks them current | | `stocks history` under `--provider stockbit` | Yahoo | Stockbit's daily candles for `5d` to `1y`; Yahoo for other windows | | `stocks intraday` | public page | public page | @@ -209,7 +210,14 @@ the previous session's, so idx leaves them out (`stocks flow` has foreign flow session by session). Sizes are lots in the table and shares in JSON. `--history-provider stockbit` asks for Stockbit's candles explicitly; windows it charts only weekly or hourly (`2y`, `5y`, -`1wk`) answer `UNSUPPORTED`. +`1wk`) answer `UNSUPPORTED`. If Stockbit refuses a stored session, `auto` +history falls back to Yahoo with a warning; an explicit +`--history-provider stockbit` fails with `AUTHERROR`. + +`market_status` in quotes is `open` while the regular market trades and +`close` otherwise, including the lunch break and auctions, with or without a +login. `stocks orderbook` keeps Stockbit's state verbatim +(`STATE_NAME_SESSION_2`, `STATE_NAME_BREAK`, ...). ## Terms diff --git a/web/src/content/docs/cli/stocks.md b/web/src/content/docs/cli/stocks.md index 634dfb6..51c1523 100644 --- a/web/src/content/docs/cli/stocks.md +++ b/web/src/content/docs/cli/stocks.md @@ -30,7 +30,8 @@ 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 +is delayed at least 10 minutes. A chart from before the latest session is +noted under the header and sets `quote.behind_session`, as with quotes. See [Stockbit](/cli/stockbit/) for what it covers and the terms it comes with. ## Order book (Stockbit login) diff --git a/web/src/content/docs/mcp/tools.md b/web/src/content/docs/mcp/tools.md index 19e1260..a904389 100644 --- a/web/src/content/docs/mcp/tools.md +++ b/web/src/content/docs/mcp/tools.md @@ -96,7 +96,7 @@ Foreign investors' buying and selling of one IDX ticker, session by session (old ## `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. +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. `quote.behind_session` is true when the chart is from before the latest IDX session (a holiday, or Stockbit lagging), as in get_quotes. UPSTREAMBLOCKED means stockbit.com refused this machine (HTTP 403): do not retry at once. | Input | Type | Required | Description | | --- | --- | --- | --- | @@ -144,7 +144,7 @@ OHLCV price history for one IDX ticker. ## `get_quotes` -Latest quotes (price, change, volume, 52-week range, market cap) for one or more IDX tickers. `as_of` is the last trade in WIB; `behind_session` is true when that is before the latest IDX session, so the price is not that session's close: the provider lags, the stock did not trade, or the day was an exchange holiday. 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. +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` ("open" while the regular market trades, else "close", with or without a login), `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 | | --- | --- | --- | --- |