From 32f476bae70a0fe75dad888dafa547d6184d2ea6 Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Tue, 3 Nov 2026 14:02:03 +0700 Subject: [PATCH] fix(stockbit): bid/offer while trading; drop stale foreign flow Observed in session on 2026-11-03: - The regular board trades as STATE_NAME_SESSION_2, not the assumed STATE_NAME_MARKET_OPEN, so session quotes hid bid/offer all day. Any numbered STATE_NAME_SESSION_ now counts as trading; the lunch break is STATE_NAME_BREAK. - In session the order book's fbuy/fsell/fnet were the previous session's (equal to IPOT's 2026-11-02 net for BBCA, BBRI and TLKM) and has_foreign_bs was false. Foreign figures are kept only when that flag is true. Fixtures for the break, the trading state and an in-session book; the table names the session ("market open (session 2)"). Claude-Session: https://claude.ai/code/session_01RqPPVeVDrnLoP7NUZmLRxY --- README.md | 2 +- ROADMAP.md | 5 +- crates/idx-cli/src/output/table.rs | 7 +- crates/idx-core/src/api/stockbit/exodus.rs | 96 +++++++++++++++---- crates/idx-core/src/api/types.rs | 6 +- .../stockbit_exodus_market_session_break.json | 1 + ...tockbit_exodus_market_session_trading.json | 1 + ...kbit_exodus_orderbook_bbca_in_session.json | 1 + crates/idx-mcp/src/server.rs | 7 +- crates/idx-mcp/tests/golden/tools.json | 2 +- docs/ARCHITECTURE.md | 12 ++- web/src/content/docs/cli/stockbit.md | 7 +- web/src/content/docs/cli/stocks.md | 3 +- web/src/content/docs/mcp/tools.md | 2 +- 14 files changed, 113 insertions(+), 39 deletions(-) create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_market_session_break.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_market_session_trading.json create mode 100644 crates/idx-core/tests/fixtures/stockbit_exodus_orderbook_bbca_in_session.json diff --git a/README.md b/README.md index f41ab94..5bf505b 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ idx ownership changes --from 2026-07-31 --to 2026-08-31 - Stockbit, opt-in: 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. With your own account - (`idx stockbit login`): `stocks orderbook` depth and foreign flow, live + (`idx stockbit login`): `stocks orderbook` depth, live quotes and Stockbit daily history. 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 diff --git a/ROADMAP.md b/ROADMAP.md index dc20fd7..05d1fd3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -15,8 +15,9 @@ history lives in git. - Order book depth without a login: no free source exists (IDX sells delayed depth to vendors); `stocks orderbook` needs the user's Stockbit login. - Stockbit login follow-ups: intraday OHLCV from the session's `1d` chart; - observing the session API's open-market state name (assumed - `STATE_NAME_MARKET_OPEN`; `STATE_NAME_MARKET_CLOSED` seen 2026-10-29); an + confirming session 1 is `STATE_NAME_SESSION_1` (session 2, `BREAK` and + `MARKET_CLOSED` were recorded on 2026-11-03), and when `has_foreign_bs` + turns on with the day's foreign figures; an opt-in daily refresh so a session outlives the refresh token's week, once it is known whether Stockbit caps a session's total life; and the sign-in response (`/login/v6/*`, read over DevTools' `Fetch`) as a second signal diff --git a/crates/idx-cli/src/output/table.rs b/crates/idx-cli/src/output/table.rs index ffd99b3..9773fc8 100644 --- a/crates/idx-cli/src/output/table.rs +++ b/crates/idx-cli/src/output/table.rs @@ -272,10 +272,11 @@ pub fn print_intraday(day: &Intraday, no_color: bool) -> Result<(), IdxError> { } /// Stockbit's market state for people: `STATE_NAME_MARKET_CLOSED` reads -/// `closed`. +/// `closed`, `STATE_NAME_BREAK` `break`, `STATE_NAME_SESSION_2` `session 2`. fn market_words(state: &str) -> String { state .strip_prefix("STATE_NAME_MARKET_") + .or_else(|| state.strip_prefix("STATE_NAME_")) .unwrap_or(state) .replace('_', " ") .to_lowercase() @@ -326,7 +327,9 @@ pub fn print_orderbook(book: &OrderBook, no_color: bool) -> Result<(), IdxError> ); } match book.market_status.as_deref() { - Some(idx_core::api::stockbit::exodus::MARKET_OPEN) => println!("market open"), + Some(state) if idx_core::api::stockbit::exodus::is_trading(state) => { + println!("market open ({})", terminal_safe(&market_words(state))) + } Some(state) => println!( "market {}: the book as the market left it", terminal_safe(&market_words(state)) diff --git a/crates/idx-core/src/api/stockbit/exodus.rs b/crates/idx-core/src/api/stockbit/exodus.rs index 1bd3163..c2030ff 100644 --- a/crates/idx-core/src/api/stockbit/exodus.rs +++ b/crates/idx-core/src/api/stockbit/exodus.rs @@ -2,10 +2,13 @@ //! user's session (see `auth`). Opt-in: used only when a session is stored. //! //! - `GET /company-price-feed/v2/orderbook/companies/{CODE}`: last price, -//! OHLC, volume (shares), value, frequency, foreign buy/sell/net (IDR) and -//! the depth ladder `bid[]`/`offer[]` of `{price, que_num, volume}` (shares). +//! OHLC, volume (shares), value, frequency, foreign buy/sell/net (IDR, used +//! only when `has_foreign_bs` is true) and the depth ladder +//! `bid[]`/`offer[]` of `{price, que_num, volume}` (shares). //! - `GET /company-price-feed/market-time/session`: the market's state, -//! `data.detail.regular.state_name` (e.g. `STATE_NAME_MARKET_CLOSED`). +//! `data.detail.regular.state_name`: `STATE_NAME_SESSION_2` while trading, +//! `STATE_NAME_BREAK`, `STATE_NAME_MARKET_CLOSED` (recorded). The `fca` +//! (full call auction) board beside it keeps its own state and is not read. //! - `GET /charts/{CODE}/daily?timeframe=1m|3m|1y&chart_type=PRICE_CHART_TYPE_CANDLE`: //! daily candles, `formatted_date` `YYYY-MM-DD`, string OHLC, `value` the //! close and `volume` in lots. @@ -29,10 +32,19 @@ use crate::error::IdxError; const BASE_URL: &str = "https://exodus.stockbit.com"; const REFRESH_PATH: &str = "/login/refresh"; -/// The state in which the book is live. Assumed from Stockbit's naming -/// (only `STATE_NAME_MARKET_CLOSED` has been recorded); any other state hides -/// bid/offer in quotes, as on the public page. -pub const MARKET_OPEN: &str = "STATE_NAME_MARKET_OPEN"; +/// The prefix of the states in which the regular market trades: +/// `STATE_NAME_SESSION_2` was recorded on 2026-11-03 from 13:30; session 1 is +/// assumed to follow the same naming. Every other state (`STATE_NAME_BREAK`, +/// `STATE_NAME_MARKET_CLOSED`, auctions) hides bid/offer in quotes, as on the +/// public page. +const TRADING_PREFIX: &str = "STATE_NAME_SESSION_"; + +/// Whether Stockbit's market state is a trading session. +pub fn is_trading(state: &str) -> bool { + state + .strip_prefix(TRADING_PREFIX) + .is_some_and(|n| !n.is_empty() && n.bytes().all(|b| b.is_ascii_digit())) +} /// The market's state changes a few times a day; one read serves this long. const MARKET_STATE_TTL: Duration = Duration::from_secs(30); @@ -293,6 +305,12 @@ pub fn parse_orderbook( } else { None }; + // Stockbit flags whether `fbuy`/`fsell`/`fnet` are current. While the + // flag was off (2026-11-03, in session) they were the previous session's + // figures (equal to IPOT's for 2026-11-02 for BBCA, BBRI and TLKM), so + // they are dropped rather than shown beside today's prices. + let foreign_current = d.get("has_foreign_bs").and_then(Value::as_bool) == Some(true); + let foreign = |key: &str| foreign_current.then(|| whole(d.get(key))).flatten(); Ok(OrderBook { symbol: resolved.to_string(), name: d.get("name").and_then(Value::as_str).map(str::to_string), @@ -306,9 +324,9 @@ pub fn parse_orderbook( volume, value: count(d.get("value")), frequency: count(d.get("frequency")), - foreign_buy: whole(d.get("fbuy")), - foreign_sell: whole(d.get("fsell")), - foreign_net: whole(d.get("fnet")), + foreign_buy: foreign("fbuy"), + foreign_sell: foreign("fsell"), + foreign_net: foreign("fnet"), bids: levels("bid"), offers: levels("offer"), market_status, @@ -318,7 +336,7 @@ pub fn parse_orderbook( /// A quote from the order book; bid/offer only while the market is open. pub fn quote_from_book(book: &OrderBook) -> Quote { - let open = book.market_status.as_deref() == Some(MARKET_OPEN); + 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(); Quote { symbol: book.symbol.clone(), @@ -473,21 +491,59 @@ mod tests { #[test] fn quote_shows_the_best_levels_only_while_the_market_is_open() { - let mut book = parse_orderbook("BBCA.JK", &fixture("orderbook_bbca.json"), None).unwrap(); - let closed = quote_from_book(&book); - assert_eq!((closed.bid, closed.offer), (None, None)); - book.market_status = Some(MARKET_OPEN.to_string()); - let open = quote_from_book(&book); - assert_eq!((open.bid, open.offer), (None, Some(5175))); - assert_eq!(open.price, 5175); + let trading = parse_market_status(&fixture("market_session_trading.json")); + let book = |state: Option| { + parse_orderbook("BBCA.JK", &fixture("orderbook_bbca_in_session.json"), state).unwrap() + }; + let open = quote_from_book(&book(trading)); + assert_eq!((open.bid, open.offer), (Some(4850), Some(4860))); + assert_eq!(open.price, 4860); + 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!(quote_from_book(&book(None)).bid, None); } #[test] fn market_status_is_read_verbatim() { + for (name, state) in [ + ("market_session.json", "STATE_NAME_MARKET_CLOSED"), + ("market_session_break.json", "STATE_NAME_BREAK"), + ("market_session_trading.json", "STATE_NAME_SESSION_2"), + ] { + assert_eq!(parse_market_status(&fixture(name)).as_deref(), Some(state)); + } + } + + #[test] + fn only_numbered_sessions_are_trading() { + assert!(is_trading("STATE_NAME_SESSION_1")); + assert!(is_trading("STATE_NAME_SESSION_2")); + for state in [ + "STATE_NAME_BREAK", + "STATE_NAME_MARKET_CLOSED", + "STATE_NAME_MARKET_OPEN", + "STATE_NAME_SESSION_", + "STATE_NAME_SESSION_PRE_OPENING", + "", + ] { + assert!(!is_trading(state), "{state}"); + } + } + + #[test] + fn foreign_flow_only_when_stockbit_marks_it_current() { + // In session (has_foreign_bs false) the figures were 2026-11-02's. + let live = + parse_orderbook("BBCA.JK", &fixture("orderbook_bbca_in_session.json"), None).unwrap(); assert_eq!( - parse_market_status(&fixture("market_session.json")).as_deref(), - Some("STATE_NAME_MARKET_CLOSED") + (live.foreign_buy, live.foreign_sell, live.foreign_net), + (None, None, None) ); + assert_eq!(live.volume, Some(134_744_100)); + let done = parse_orderbook("BBCA.JK", &fixture("orderbook_bbca.json"), None).unwrap(); + assert_eq!(done.foreign_buy, Some(1_616_461_977_500)); } #[test] diff --git a/crates/idx-core/src/api/types.rs b/crates/idx-core/src/api/types.rs index 72d5463..2d7d1b1 100644 --- a/crates/idx-core/src/api/types.rs +++ b/crates/idx-core/src/api/types.rs @@ -126,7 +126,8 @@ pub struct OrderBook { pub value: Option, /// Trades in the session. pub frequency: Option, - /// Foreign buy, sell and net value over the session, IDR. + /// Foreign buy, sell and net value over the session, IDR; `None` while + /// Stockbit marks them not current (in session they lag a day). pub foreign_buy: Option, pub foreign_sell: Option, pub foreign_net: Option, @@ -134,7 +135,8 @@ pub struct OrderBook { pub bids: Vec, /// Best price first. pub offers: Vec, - /// Stockbit's session state, verbatim (e.g. `STATE_NAME_MARKET_CLOSED`). + /// Stockbit's session state, verbatim (e.g. `STATE_NAME_SESSION_2`, + /// `STATE_NAME_BREAK`, `STATE_NAME_MARKET_CLOSED`). pub market_status: Option, /// Stockbit's listing status when it is not active (e.g. suspended). pub session_note: Option, diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_break.json b/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_break.json new file mode 100644 index 0000000..b2447f6 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_break.json @@ -0,0 +1 @@ +{"message":"Successfully get session info data","data":{"datetime":"2026-11-03 12:35:59","detail":{"fca":{"session":3,"state_name":"STATE_NAME_BREAK","is_last_session":false,"is_end_of_day":false,"state_start_time":"12:00:00","state_end_time":"13:59:59","time_left":{"raw":5040,"formatted":"1 jam 24 menit 0 detik"},"suspend_info":""},"regular":{"session":1,"state_name":"STATE_NAME_BREAK","is_last_session":false,"is_end_of_day":false,"state_start_time":"12:00:00","state_end_time":"13:29:59","time_left":{"raw":3240,"formatted":"0 jam 54 menit 0 detik"},"suspend_info":""}}}} \ No newline at end of file diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_trading.json b/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_trading.json new file mode 100644 index 0000000..2b33457 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_market_session_trading.json @@ -0,0 +1 @@ +{"message":"Successfully get session info data","data":{"datetime":"2026-11-03 13:30:03","detail":{"fca":{"session":3,"state_name":"STATE_NAME_BREAK","is_last_session":false,"is_end_of_day":false,"state_start_time":"12:00:00","state_end_time":"13:59:59","time_left":{"raw":1796,"formatted":"0 jam 29 menit 56 detik"},"suspend_info":""},"regular":{"session":2,"state_name":"STATE_NAME_SESSION_2","is_last_session":true,"is_end_of_day":false,"state_start_time":"13:30:00","state_end_time":"15:49:59","time_left":{"raw":9896,"formatted":"2 jam 44 menit 56 detik"},"suspend_info":""}}}} \ No newline at end of file diff --git a/crates/idx-core/tests/fixtures/stockbit_exodus_orderbook_bbca_in_session.json b/crates/idx-core/tests/fixtures/stockbit_exodus_orderbook_bbca_in_session.json new file mode 100644 index 0000000..8c89d42 --- /dev/null +++ b/crates/idx-core/tests/fixtures/stockbit_exodus_orderbook_bbca_in_session.json @@ -0,0 +1 @@ +{"data":{"average":4950,"bid":[{"price":"4850","que_num":"1649","volume":"7255100","change_percentage":""},{"price":"4840","que_num":"273","volume":"1417100","change_percentage":""},{"price":"4830","que_num":"255","volume":"1642200","change_percentage":""},{"price":"4820","que_num":"370","volume":"2501200","change_percentage":""},{"price":"4810","que_num":"343","volume":"1469800","change_percentage":""},{"price":"4800","que_num":"1586","volume":"11375100","change_percentage":""},{"price":"4790","que_num":"101","volume":"271500","change_percentage":""},{"price":"4780","que_num":"106","volume":"268500","change_percentage":""},{"price":"4770","que_num":"71","volume":"105100","change_percentage":""},{"price":"4760","que_num":"96","volume":"252000","change_percentage":""},{"price":"4750","que_num":"439","volume":"1606500","change_percentage":""},{"price":"4740","que_num":"171","volume":"519700","change_percentage":""},{"price":"4730","que_num":"47","volume":"198600","change_percentage":""},{"price":"4720","que_num":"62","volume":"102800","change_percentage":""},{"price":"4710","que_num":"61","volume":"124400","change_percentage":""},{"price":"4700","que_num":"455","volume":"1743900","change_percentage":""},{"price":"4690","que_num":"46","volume":"68500","change_percentage":""},{"price":"4680","que_num":"70","volume":"262900","change_percentage":""},{"price":"4670","que_num":"34","volume":"127100","change_percentage":""},{"price":"4660","que_num":"62","volume":"281200","change_percentage":""},{"price":"4650","que_num":"275","volume":"1441900","change_percentage":""},{"price":"4640","que_num":"548","volume":"3830900","change_percentage":""}],"change":-120,"close":4860,"country":"ID","domestic":"39.93","down":"110","exchange":"IDX","fbuy":534507825000,"fnet":-116143206500,"foreign":"60.07","frequency":30935,"fsell":650651031500,"high":5050,"id":"BBCA-0","lastprice":4860,"low":4850,"offer":[{"price":"4860","que_num":"1","volume":"86800","change_percentage":""},{"price":"4870","que_num":"9","volume":"282500","change_percentage":""},{"price":"4880","que_num":"20","volume":"499700","change_percentage":""},{"price":"4890","que_num":"17","volume":"681500","change_percentage":""},{"price":"4900","que_num":"39","volume":"328200","change_percentage":""},{"price":"4910","que_num":"10","volume":"1255800","change_percentage":""},{"price":"4920","que_num":"33","volume":"175200","change_percentage":""},{"price":"4930","que_num":"18","volume":"73800","change_percentage":""},{"price":"4940","que_num":"57","volume":"645800","change_percentage":""},{"price":"4950","que_num":"128","volume":"1703400","change_percentage":""},{"price":"4960","que_num":"46","volume":"230800","change_percentage":""},{"price":"4970","que_num":"60","volume":"384400","change_percentage":""},{"price":"4980","que_num":"67","volume":"784700","change_percentage":""},{"price":"4990","que_num":"75","volume":"800000","change_percentage":""},{"price":"5000","que_num":"297","volume":"1980500","change_percentage":""},{"price":"5025","que_num":"518","volume":"2942300","change_percentage":""},{"price":"5050","que_num":"781","volume":"5752100","change_percentage":""},{"price":"5075","que_num":"465","volume":"3892600","change_percentage":""},{"price":"5100","que_num":"1014","volume":"7926500","change_percentage":""},{"price":"5125","que_num":"328","volume":"3467400","change_percentage":""},{"price":"5150","que_num":"419","volume":"3256100","change_percentage":""},{"price":"5175","que_num":"238","volume":"1402900","change_percentage":""},{"price":"5200","que_num":"701","volume":"4462900","change_percentage":""},{"price":"5225","que_num":"224","volume":"1621200","change_percentage":""},{"price":"5250","que_num":"404","volume":"3036500","change_percentage":""},{"price":"5275","que_num":"216","volume":"1837900","change_percentage":""},{"price":"5300","que_num":"610","volume":"4584500","change_percentage":""},{"price":"5325","que_num":"208","volume":"1329500","change_percentage":""},{"price":"5350","que_num":"273","volume":"1823400","change_percentage":""},{"price":"5375","que_num":"101","volume":"632600","change_percentage":""},{"price":"5400","que_num":"238","volume":"1122800","change_percentage":""},{"price":"5425","que_num":"61","volume":"303200","change_percentage":""},{"price":"5450","que_num":"89","volume":"284800","change_percentage":""},{"price":"5475","que_num":"74","volume":"387200","change_percentage":""},{"price":"5500","que_num":"274","volume":"2019900","change_percentage":""},{"price":"5525","que_num":"54","volume":"109200","change_percentage":""},{"price":"5550","que_num":"55","volume":"272400","change_percentage":""},{"price":"5575","que_num":"35","volume":"96500","change_percentage":""},{"price":"5600","que_num":"97","volume":"748300","change_percentage":""},{"price":"5625","que_num":"22","volume":"178400","change_percentage":""},{"price":"5650","que_num":"28","volume":"67300","change_percentage":""},{"price":"5675","que_num":"21","volume":"60000","change_percentage":""},{"price":"5700","que_num":"56","volume":"239800","change_percentage":""},{"price":"5725","que_num":"17","volume":"26500","change_percentage":""},{"price":"5750","que_num":"36","volume":"37400","change_percentage":""},{"price":"5775","que_num":"28","volume":"130000","change_percentage":""},{"price":"5800","que_num":"71","volume":"1256700","change_percentage":""},{"price":"5825","que_num":"25","volume":"79900","change_percentage":""},{"price":"5850","que_num":"29","volume":"177900","change_percentage":""},{"price":"5875","que_num":"19","volume":"255000","change_percentage":""},{"price":"5900","que_num":"46","volume":"266200","change_percentage":""},{"price":"5925","que_num":"19","volume":"82600","change_percentage":""},{"price":"5950","que_num":"40","volume":"104200","change_percentage":""},{"price":"5975","que_num":"38","volume":"149700","change_percentage":""},{"price":"6000","que_num":"227","volume":"1724500","change_percentage":""},{"price":"6025","que_num":"30","volume":"84700","change_percentage":""},{"price":"6050","que_num":"22","volume":"70800","change_percentage":""},{"price":"6075","que_num":"15","volume":"488900","change_percentage":""},{"price":"6100","que_num":"68","volume":"159000","change_percentage":""},{"price":"6125","que_num":"34","volume":"83100","change_percentage":""},{"price":"6150","que_num":"46","volume":"80700","change_percentage":""},{"price":"6175","que_num":"54","volume":"277400","change_percentage":""},{"price":"6200","que_num":"132","volume":"823900","change_percentage":""},{"price":"6225","que_num":"123","volume":"436800","change_percentage":""}],"open":5050,"percentage_change":-2.41,"previous":4980,"status":"Active","symbol":"BBCA","symbol_2":"BBCA","symbol_3":"BBCA","tradable":true,"unchanged":"1118","up":"265","value":666978580500,"volume":134744100,"corp_action":{"active":false,"icon":"https://assets.stockbit.com/images/corp_action_event_icon.svg","text":"Perusahaan Memiliki Corporate Action"},"notation":[],"uma":false,"has_foreign_bs":false,"iepiev":{"symbol":"","status":"STATUS_UNSPECIFIED","iep":{"raw":0,"formatted":""},"iev":{"raw":0,"formatted":""},"time_left_seconds":0,"best_bid_offer":null,"title":"","iep_changes":{"price":{"raw":0,"formatted":""},"percentage":{"raw":0,"formatted":""}}},"market_data":[{"label":"All Market","frequency":{"raw":"30935","formatted":"30.9 K"},"volume":{"raw":"134744100","formatted":"135 M"},"value":{"raw":"666978580500","formatted":"667 B"}},{"label":"Regular","frequency":{"raw":"30935","formatted":"30.9 K"},"volume":{"raw":"134744100","formatted":"135 M"},"value":{"raw":"666978580500","formatted":"667 B"}},{"label":"Nego","frequency":{"raw":"0","formatted":"0"},"volume":{"raw":"0","formatted":"0"},"value":{"raw":"0","formatted":"0"}},{"label":"Cash","frequency":{"raw":"0","formatted":"0"},"volume":{"raw":"0","formatted":"0"},"value":{"raw":"0","formatted":"0"}}],"name":"Bank Central Asia Tbk.","icon_url":"https://assets.stockbit.com/logos/companies/BBCA.png","ara":{"value":"6,225","visible":true},"arb":{"value":"4,640","visible":true},"company_type":"Saham","total_bid_offer":{"bid":{"freq":"7,120","lot":"36,866,000","raw_lot":"36866000","raw_freq":"7120"},"offer":{"freq":"9,600","lot":"70,567,200","raw_lot":"70567200","raw_freq":"9600"},"bid_percent":34.3},"next_ara":{"value":"6,225","visible":true},"next_arb":{"value":"4,640","visible":true},"autoreject_time_left_in_sec":0,"auto_reject_estimation":[{"value":489675,"change_to_prev":{"value":81600,"percentage":20},"change_to_base":{"value":484695,"percentage":9732.83},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":408075,"change_to_prev":{"value":68000,"percentage":20},"change_to_base":{"value":403095,"percentage":8094.28},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":340075,"change_to_prev":{"value":56675,"percentage":20},"change_to_base":{"value":335095,"percentage":6728.82},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":283400,"change_to_prev":{"value":47225,"percentage":20},"change_to_base":{"value":278420,"percentage":5590.76},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":236175,"change_to_prev":{"value":39350,"percentage":19.99},"change_to_base":{"value":231195,"percentage":4642.47},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":196825,"change_to_prev":{"value":32800,"percentage":20},"change_to_base":{"value":191845,"percentage":3852.31},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":164025,"change_to_prev":{"value":27325,"percentage":19.99},"change_to_base":{"value":159045,"percentage":3193.67},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":136700,"change_to_prev":{"value":22775,"percentage":19.99},"change_to_base":{"value":131720,"percentage":2644.98},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":113925,"change_to_prev":{"value":18975,"percentage":19.98},"change_to_base":{"value":108945,"percentage":2187.65},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":94950,"change_to_prev":{"value":15825,"percentage":20},"change_to_base":{"value":89970,"percentage":1806.63},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":79125,"change_to_prev":{"value":13175,"percentage":19.98},"change_to_base":{"value":74145,"percentage":1488.86},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":65950,"change_to_prev":{"value":10975,"percentage":19.96},"change_to_base":{"value":60970,"percentage":1224.3},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":54975,"change_to_prev":{"value":9150,"percentage":19.97},"change_to_base":{"value":49995,"percentage":1003.92},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":45825,"change_to_prev":{"value":7625,"percentage":19.96},"change_to_base":{"value":40845,"percentage":820.18},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":38200,"change_to_prev":{"value":6350,"percentage":19.94},"change_to_base":{"value":33220,"percentage":667.07},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":31850,"change_to_prev":{"value":5300,"percentage":19.96},"change_to_base":{"value":26870,"percentage":539.56},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":26550,"change_to_prev":{"value":4425,"percentage":20},"change_to_base":{"value":21570,"percentage":433.13},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":22125,"change_to_prev":{"value":3675,"percentage":19.92},"change_to_base":{"value":17145,"percentage":344.28},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":18450,"change_to_prev":{"value":3075,"percentage":20},"change_to_base":{"value":13470,"percentage":270.48},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":15375,"change_to_prev":{"value":2550,"percentage":19.88},"change_to_base":{"value":10395,"percentage":208.73},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":12825,"change_to_prev":{"value":2125,"percentage":19.86},"change_to_base":{"value":7845,"percentage":157.53},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":10700,"change_to_prev":{"value":1775,"percentage":19.89},"change_to_base":{"value":5720,"percentage":114.86},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":8925,"change_to_prev":{"value":1475,"percentage":19.8},"change_to_base":{"value":3945,"percentage":79.22},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":7450,"change_to_prev":{"value":1225,"percentage":19.68},"change_to_base":{"value":2470,"percentage":49.6},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":6225,"change_to_prev":{"value":1245,"percentage":25},"change_to_base":{"value":1245,"percentage":25},"type":"AUTO_REJECT_TYPE_POSITIVE"},{"value":4980,"change_to_prev":{"value":0,"percentage":0},"change_to_base":{"value":0,"percentage":0},"type":"AUTO_REJECT_TYPE_BASE"},{"value":4640,"change_to_prev":{"value":-340,"percentage":-6.83},"change_to_base":{"value":-340,"percentage":-6.83},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":4320,"change_to_prev":{"value":-320,"percentage":-6.9},"change_to_base":{"value":-660,"percentage":-13.25},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":4020,"change_to_prev":{"value":-300,"percentage":-6.94},"change_to_base":{"value":-960,"percentage":-19.28},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":3740,"change_to_prev":{"value":-280,"percentage":-6.97},"change_to_base":{"value":-1240,"percentage":-24.9},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":3480,"change_to_prev":{"value":-260,"percentage":-6.95},"change_to_base":{"value":-1500,"percentage":-30.12},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":3240,"change_to_prev":{"value":-240,"percentage":-6.9},"change_to_base":{"value":-1740,"percentage":-34.94},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":3020,"change_to_prev":{"value":-220,"percentage":-6.79},"change_to_base":{"value":-1960,"percentage":-39.36},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":2810,"change_to_prev":{"value":-210,"percentage":-6.95},"change_to_base":{"value":-2170,"percentage":-43.57},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":2620,"change_to_prev":{"value":-190,"percentage":-6.76},"change_to_base":{"value":-2360,"percentage":-47.39},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":2440,"change_to_prev":{"value":-180,"percentage":-6.87},"change_to_base":{"value":-2540,"percentage":-51},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":2270,"change_to_prev":{"value":-170,"percentage":-6.97},"change_to_base":{"value":-2710,"percentage":-54.42},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":2120,"change_to_prev":{"value":-150,"percentage":-6.61},"change_to_base":{"value":-2860,"percentage":-57.43},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1975,"change_to_prev":{"value":-145,"percentage":-6.84},"change_to_base":{"value":-3005,"percentage":-60.34},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1840,"change_to_prev":{"value":-135,"percentage":-6.84},"change_to_base":{"value":-3140,"percentage":-63.05},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1715,"change_to_prev":{"value":-125,"percentage":-6.79},"change_to_base":{"value":-3265,"percentage":-65.56},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1595,"change_to_prev":{"value":-120,"percentage":-7},"change_to_base":{"value":-3385,"percentage":-67.97},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1485,"change_to_prev":{"value":-110,"percentage":-6.9},"change_to_base":{"value":-3495,"percentage":-70.18},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1385,"change_to_prev":{"value":-100,"percentage":-6.73},"change_to_base":{"value":-3595,"percentage":-72.19},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1290,"change_to_prev":{"value":-95,"percentage":-6.86},"change_to_base":{"value":-3690,"percentage":-74.1},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1200,"change_to_prev":{"value":-90,"percentage":-6.98},"change_to_base":{"value":-3780,"percentage":-75.9},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1120,"change_to_prev":{"value":-80,"percentage":-6.67},"change_to_base":{"value":-3860,"percentage":-77.51},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":1045,"change_to_prev":{"value":-75,"percentage":-6.7},"change_to_base":{"value":-3935,"percentage":-79.02},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":975,"change_to_prev":{"value":-70,"percentage":-6.7},"change_to_base":{"value":-4005,"percentage":-80.42},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":910,"change_to_prev":{"value":-65,"percentage":-6.67},"change_to_base":{"value":-4070,"percentage":-81.73},"type":"AUTO_REJECT_TYPE_NEGATIVE"},{"value":850,"change_to_prev":{"value":-60,"percentage":-6.59},"change_to_base":{"value":-4130,"percentage":-82.93},"type":"AUTO_REJECT_TYPE_NEGATIVE"}],"orderbook_active_feature_mobile":"ORDER_BOOK_FEATURE_UNSPECIFIED"},"message":"Successfully get company orderbook"} \ No newline at end of file diff --git a/crates/idx-mcp/src/server.rs b/crates/idx-mcp/src/server.rs index fbe5c6f..3b9b4a0 100644 --- a/crates/idx-mcp/src/server.rs +++ b/crates/idx-mcp/src/server.rs @@ -165,9 +165,10 @@ per-minute volume. Empty `points` means the stock did not trade this session (se #[tool( description = "Order book of one IDX stock from the user's own Stockbit session: \ `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, \ -volume (shares), value and frequency, and foreign buy/sell/net in IDR. `market_status` is \ -Stockbit's state, verbatim; outside STATE_NAME_MARKET_OPEN the book is as the market left \ -it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL \ +volume (shares), value and frequency, and foreign buy/sell/net in IDR (null while Stockbit \ +marks them not current, as during trading; get_foreign_flow has them per session). \ +`market_status` is Stockbit's state, verbatim: STATE_NAME_SESSION_1 or _2 is trading; in any \ +other state the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL \ the user can open." )] async fn get_orderbook(&self, params: Parameters) -> ToolResult { diff --git a/crates/idx-mcp/tests/golden/tools.json b/crates/idx-mcp/tests/golden/tools.json index 2c8acbf..51ce573 100644 --- a/crates/idx-mcp/tests/golden/tools.json +++ b/crates/idx-mcp/tests/golden/tools.json @@ -332,7 +332,7 @@ "name": "get_investor_holdings" }, { - "description": "Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR. `market_status` is Stockbit's state, verbatim; outside STATE_NAME_MARKET_OPEN the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL the user can open.", + "description": "Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR (null while Stockbit marks them not current, as during trading; get_foreign_flow has them per session). `market_status` is Stockbit's state, verbatim: STATE_NAME_SESSION_1 or _2 is trading; in any other state the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL the user can open.", "inputSchema": { "$schema": "https://json-schema.org/draft/2020-12/schema", "properties": { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d532a90..158163d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -269,13 +269,17 @@ WIB as the fallback. its recordings are `tests/fixtures/stockbit_exodus_*`): `/company-price-feed/v2/orderbook/companies/{CODE}` gives the depth ladder (`volume` in shares, `que_num` orders), OHLC, value, frequency and foreign - buy/sell/net; `/company-price-feed/market-time/session` the market state - (`STATE_NAME_MARKET_CLOSED`, cached 30 s per process); `/charts/{CODE}/daily` + buy/sell/net (kept only when `has_foreign_bs` is true: on 2026-11-03 in + session it was false and the figures were the previous session's); + `/company-price-feed/market-time/session` the market state of the `regular` + board (`STATE_NAME_SESSION_2` while trading, `STATE_NAME_BREAK`, + `STATE_NAME_MARKET_CLOSED`; cached 30 s per process; the `fca` board's own + state is not read); `/charts/{CODE}/daily` daily candles for `1m`/`3m`/`1y` windows (volume in lots, returned in shares). Requests share the public page's cross-process pacing. - With a session, `--provider stockbit` quotes come from the order book (bid - and offer only in `STATE_NAME_MARKET_OPEN`, a name assumed until observed - in session), `stocks orderbook` works, and `auto` history under the stockbit + and offer only in a `STATE_NAME_SESSION_` state; session 2 was observed + on 2026-11-03, session 1 is assumed to match), `stocks orderbook` works, and `auto` history under the stockbit provider uses Stockbit's daily windows (`5d` to `1y`, trimmed from `1m`, `3m` or `1y`), Yahoo otherwise. Without one, quotes use the public page and the order book is `AUTHERROR`. diff --git a/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md index 25c38c7..4ab02a0 100644 --- a/web/src/content/docs/cli/stockbit.md +++ b/web/src/content/docs/cli/stockbit.md @@ -195,12 +195,15 @@ 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 | -| `stocks orderbook` | `AUTHERROR` | every level with lots and queued orders, OHLC, value, trades, foreign buy/sell/net | +| `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 | The order book is what Stockbit's own app shows. Outside trading it is the -book as the market left it, and the output says so. Sizes are lots in the +book as the market left it, and the output says so. Foreign buy, sell and net +appear only when Stockbit marks them current: during trading its figures are +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`. diff --git a/web/src/content/docs/cli/stocks.md b/web/src/content/docs/cli/stocks.md index a64e013..3a9ecb5 100644 --- a/web/src/content/docs/cli/stocks.md +++ b/web/src/content/docs/cli/stocks.md @@ -42,7 +42,8 @@ idx stocks orderbook BBRI --depth 5 With your own Stockbit session (`idx stockbit login`), `orderbook` shows every bid and offer level with lots and queued orders, the session's OHLC, -value and trades, and foreign buy, sell and net. Without one it answers +value and trades, and foreign buy, sell and net once Stockbit marks them +current (not during trading, when they lag a day). Without one it answers `AUTHERROR`. See [Stockbit](/cli/stockbit/#with-your-stockbit-login). ## Company data diff --git a/web/src/content/docs/mcp/tools.md b/web/src/content/docs/mcp/tools.md index 7fd4798..ca4c98a 100644 --- a/web/src/content/docs/mcp/tools.md +++ b/web/src/content/docs/mcp/tools.md @@ -94,7 +94,7 @@ Every IDX holding (above 1%) of one investor, matched by name. ## `get_orderbook` -Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR. `market_status` is Stockbit's state, verbatim; outside STATE_NAME_MARKET_OPEN the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL the user can open. +Order book of one IDX stock from the user's own Stockbit session: `bids` and `offers` (best price first) of {price, shares, orders}, the session's OHLC, volume (shares), value and frequency, and foreign buy/sell/net in IDR (null while Stockbit marks them not current, as during trading; get_foreign_flow has them per session). `market_status` is Stockbit's state, verbatim: STATE_NAME_SESSION_1 or _2 is trading; in any other state the book is as the market left it. AUTHERROR means there is no session: call stockbit_status, then stockbit_login for a URL the user can open. | Input | Type | Required | Description | | --- | --- | --- | --- |