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-core/src/api/stockbit/exodus.rs b/crates/idx-core/src/api/stockbit/exodus.rs index c2030ff..d8d5d01 100644 --- a/crates/idx-core/src/api/stockbit/exodus.rs +++ b/crates/idx-core/src/api/stockbit/exodus.rs @@ -335,6 +335,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(); @@ -355,7 +358,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(), } } @@ -498,11 +504,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/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-mcp/src/server.rs b/crates/idx-mcp/src/server.rs index 3dfe1fa..00a6883 100644 --- a/crates/idx-mcp/src/server.rs +++ b/crates/idx-mcp/src/server.rs @@ -124,8 +124,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." )] diff --git a/crates/idx-mcp/tests/golden/tools.json b/crates/idx-mcp/tests/golden/tools.json index f3ca8d3..b20cd5d 100644 --- a/crates/idx-mcp/tests/golden/tools.json +++ b/crates/idx-mcp/tests/golden/tools.json @@ -451,7 +451,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/web/src/content/docs/cli/stockbit.md b/web/src/content/docs/cli/stockbit.md index 4ab02a0..722bd06 100644 --- a/web/src/content/docs/cli/stockbit.md +++ b/web/src/content/docs/cli/stockbit.md @@ -195,6 +195,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 | @@ -206,7 +207,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/mcp/tools.md b/web/src/content/docs/mcp/tools.md index df9f31d..326f476 100644 --- a/web/src/content/docs/mcp/tools.md +++ b/web/src/content/docs/mcp/tools.md @@ -125,7 +125,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 | | --- | --- | --- | --- |