mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-11-07 09:45:36 +00:00
feat: login-free Stockbit intraday and quote provider
Read Stockbit's public symbol page (__NEXT_DATA__) with a plain ureq request and an honest user agent: `stocks intraday` (today by the minute, any provider), `--provider stockbit` quotes with the best bid/offer only while the market is open, and MCP `get_intraday`. - UPSTREAMBLOCKED error (exit 4) for a CDN 403; never retried - requests paced 1 s apart across idx processes, 5 s snapshot cache - global --provider flag (yahoo, msn, stockbit) - roadmap: L1 ships from Stockbit; depth stays a login follow-on
This commit is contained in:
parent
018d9ba936
commit
74e5bb0536
65 changed files with 2320 additions and 381 deletions
|
|
@ -9,13 +9,14 @@ use idx_core::config::CoreConfig;
|
|||
use idx_core::investor::{ImportSource, Investor};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
const TOOLS: [&str; 14] = [
|
||||
const TOOLS: [&str; 15] = [
|
||||
"analyze_bandar",
|
||||
"analyze_stock",
|
||||
"compare_stocks",
|
||||
"get_broker_summary",
|
||||
"get_company",
|
||||
"get_foreign_flow",
|
||||
"get_intraday",
|
||||
"get_investor_holdings",
|
||||
"get_ownership_changes",
|
||||
"get_price_history",
|
||||
|
|
@ -36,6 +37,10 @@ struct Client {
|
|||
|
||||
impl Client {
|
||||
fn start(args: &[&str], seed: bool) -> Self {
|
||||
Self::start_with_env(args, seed, &[])
|
||||
}
|
||||
|
||||
fn start_with_env(args: &[&str], seed: bool, env: &[(&str, &str)]) -> Self {
|
||||
let home = tempdir::Dir::new();
|
||||
if seed {
|
||||
seed_ownership_db(&home.path().join("data/idx/ownership.db"));
|
||||
|
|
@ -50,6 +55,7 @@ impl Client {
|
|||
.env("XDG_CACHE_HOME", home.path().join("cache"))
|
||||
.env("XDG_DATA_HOME", home.path().join("data"))
|
||||
.env("IDX_USE_MOCK_PROVIDER", "1")
|
||||
.envs(env.iter().copied())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
|
|
@ -299,6 +305,63 @@ fn live_tools_wrap_data_with_freshness() {
|
|||
assert_eq!(compared["failures"], json!([]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intraday_is_stockbit_whichever_provider_and_empty_sessions_are_data() {
|
||||
let mut client = Client::start(&[], false);
|
||||
|
||||
let day = client.ok("get_intraday", json!({"symbol": "BBCA"}));
|
||||
assert_fetched(&day);
|
||||
assert_eq!(day["source"], "stockbit");
|
||||
let quote = &day["data"]["quote"];
|
||||
assert_eq!(quote["symbol"], "BBCA.JK");
|
||||
assert_eq!(quote["market_status"], "close");
|
||||
// The closing book is not reported as a live one.
|
||||
assert!(
|
||||
quote.get("bid").is_none() && quote.get("offer").is_none(),
|
||||
"{quote}"
|
||||
);
|
||||
assert_eq!(day["data"]["points"].as_array().unwrap().len(), 335);
|
||||
|
||||
let tail = client.ok("get_intraday", json!({"symbol": "BBCA", "tail": 2}));
|
||||
let points = tail["data"]["points"].as_array().unwrap();
|
||||
assert_eq!(points.len(), 2);
|
||||
assert_eq!(points[1]["time"], "2026-10-22T16:14:00+07:00");
|
||||
|
||||
let suspended = client.ok("get_intraday", json!({"symbol": "MABA"}));
|
||||
assert_eq!(suspended["data"]["points"], json!([]));
|
||||
assert_eq!(
|
||||
suspended["data"]["quote"]["session_note"],
|
||||
"Stockbit lists the stock as suspended"
|
||||
);
|
||||
|
||||
for (arguments, code) in [
|
||||
(json!({"symbol": "ZZZZ"}), "SYMBOLNOTFOUND"),
|
||||
(json!({"symbol": "BBCA", "tail": 0}), "INVALIDINPUT"),
|
||||
(json!({"symbol": "BBCA", "tail": 401}), "INVALIDINPUT"),
|
||||
] {
|
||||
let (is_error, body) = client.call("get_intraday", arguments.clone());
|
||||
assert!(is_error, "{arguments}: {body}");
|
||||
assert_eq!(body["code"], code, "{arguments}: {body}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stockbit_provider_quotes_carry_market_status_and_blocks_surface_once() {
|
||||
let mut client = Client::start_with_env(&[], false, &[("IDX_PROVIDER", "stockbit")]);
|
||||
let quotes = client.ok("get_quotes", json!({"symbols": ["BBCA"]}));
|
||||
assert_eq!(quotes["quotes"][0]["source"], "stockbit");
|
||||
assert_eq!(quotes["quotes"][0]["data"]["market_status"], "close");
|
||||
|
||||
let mut blocked = Client::start_with_env(&[], false, &[("IDX_MOCK_STOCKBIT_STATUS", "403")]);
|
||||
let (is_error, body) = blocked.call("get_intraday", json!({"symbol": "BBCA"}));
|
||||
assert!(is_error, "{body}");
|
||||
assert_eq!(body["code"], "UPSTREAMBLOCKED");
|
||||
assert!(
|
||||
body["message"].as_str().unwrap().contains("does not retry"),
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_carry_cli_codes() {
|
||||
let mut client = Client::start(&[], false);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue