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:
hermes 2026-10-23 04:58:32 +00:00
commit 74e5bb0536
65 changed files with 2320 additions and 381 deletions

View file

@ -34,6 +34,10 @@ pub enum IdxError {
InvalidInput(String),
#[error("no market data: {0}")]
NoMarketData(String),
/// The upstream refused this client outright (e.g. a CDN's HTTP 403
/// block page). Retrying does not help; the message says what might.
#[error("upstream blocked: {0}")]
UpstreamBlocked(String),
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
@ -53,6 +57,7 @@ pub enum ErrorCode {
PdfParseError,
InvalidInput,
NoMarketData,
UpstreamBlocked,
}
/// The stable wire form frontends emit, e.g. `SYMBOLNOTFOUND`.
@ -80,6 +85,7 @@ impl IdxError {
Self::PdfParseError(_) => ErrorCode::PdfParseError,
Self::InvalidInput(_) => ErrorCode::InvalidInput,
Self::NoMarketData(_) => ErrorCode::NoMarketData,
Self::UpstreamBlocked(_) => ErrorCode::UpstreamBlocked,
}
}
@ -93,7 +99,8 @@ impl IdxError {
| ErrorCode::RateLimited
| ErrorCode::ProviderUnavailable
| ErrorCode::Offline
| ErrorCode::AuthError => 4,
| ErrorCode::AuthError
| ErrorCode::UpstreamBlocked => 4,
ErrorCode::Unsupported => 5,
ErrorCode::Io
| ErrorCode::ParseError
@ -119,4 +126,11 @@ mod tests {
let unsupported = IdxError::Unsupported("history unavailable".to_string());
assert_eq!(unsupported.code(), ErrorCode::Unsupported);
}
#[test]
fn upstream_blocked_has_its_own_wire_code_and_network_exit() {
let blocked = IdxError::UpstreamBlocked("stockbit.com: HTTP 403".to_string());
assert_eq!(blocked.code().to_string(), "UPSTREAMBLOCKED");
assert_eq!(blocked.exit_code(), 4);
}
}