marketdata-core
Rust library for Fugle market data streaming. Provides REST API and WebSocket clients for Taiwan stock and futures/options market data.
Features
- REST Client: Synchronous HTTP client for market data queries
- Stock intraday data (quote, ticker, candles, trades, volumes)
- FutOpt (futures/options) intraday data
- WebSocket Client: Async real-time streaming
- Stock channels: trades, candles, books, aggregates, indices
- FutOpt channels: trades, candles, books, aggregates
- Automatic reconnection with exponential backoff
- Health check monitoring
- Authentication: API key, bearer token, or SDK token
- FFI-ready: Error codes and types designed for Python/JavaScript bindings
Quick Start
Installation
Add to your Cargo.toml:
[]
= { = "../marketdata-core" }
= { = "1", = ["rt-multi-thread", "macros"] }
REST API
use ;
WebSocket Streaming
use ;
use Arc;
async
Authentication
Three authentication methods are supported:
use Auth;
// 1. API Key (most common)
let auth = ApiKey;
// 2. Bearer Token
let auth = BearerToken;
// 3. SDK Token
let auth = SdkToken;
For WebSocket:
use AuthRequest;
let auth = with_api_key;
let auth = with_token;
let auth = with_sdk_token;
Configuration
ReconnectionConfig
Control WebSocket automatic reconnection behavior with exponential backoff:
use ReconnectionConfig;
use Duration;
let reconnect = new?;
Parameters:
max_attempts(u32): Maximum reconnection attempts (default: 5, range: 1+)initial_delay(Duration): Initial delay for exponential backoff (default: 1000ms, min: 100ms)max_delay(Duration): Maximum delay cap (default: 60000ms)
Validation:
max_attemptsmust be >= 1initial_delaymust be >= 100ms (prevents connection storms)max_delaymust be >=initial_delay(logical constraint)
HealthCheckConfig
Control connection liveness detection. The SDK declares the connection dead and
triggers reconnect when no inbound frame arrives within heartbeat_timeout.
The SDK uses passive activity detection at the WebSocket read site — no
background task, no atomic timestamps, no protocol-level pings. The dispatch
loop wraps each ws_read.next() in tokio::time::timeout(heartbeat_timeout, ...)
and emits ConnectionEvent::HeartbeatTimeout when the timer fires.
use HealthCheckConfig;
use Duration;
// Recommended: with_timeout validates against the 5s sanity floor.
let health = with_timeout?;
// Default: enabled=true, heartbeat_timeout=35s (Fugle server's 30s heartbeat
// + 5s buffer).
let health = default;
// Opt out (discouraged — stalled connections won't surface until OS times
// out the underlying TCP, typically hours later).
let health = disabled;
Parameters:
enabled(bool): Whether liveness detection is active. Defaulttruein 3.0 (wasfalsein 2.x).heartbeat_timeout(Duration): Maximum allowed gap between inbound frames. Default 35s. Floor 5s — but values below the live server's 30s heartbeat period will cause repeated false disconnects.
Migration from 2.x: interval × max_missed_pongs collapsed into a single
heartbeat_timeout field. If you used HealthCheckConfig::new(enabled, interval, max_missed_pongs), the equivalent is HealthCheckConfig::with_timeout(interval * max_missed_pongs)?.
Config Constants
All configuration constants are exported from lib.rs for use in binding layers:
// Reconnection defaults
pub const DEFAULT_MAX_ATTEMPTS: u32 = 5;
pub const DEFAULT_INITIAL_DELAY_MS: u64 = 1000;
pub const DEFAULT_MAX_DELAY_MS: u64 = 60000;
pub const MIN_INITIAL_DELAY_MS: u64 = 100;
// Health check defaults (3.0)
pub const DEFAULT_HEALTH_CHECK_ENABLED: bool = true;
pub const DEFAULT_HEARTBEAT_TIMEOUT_MS: u64 = 35000;
pub const MIN_HEARTBEAT_TIMEOUT_MS: u64 = 5000;
Which constructor should I use?
The SDK exposes four distinct construction paths. They look similar; this section pins which to reach for in which situation.
1. bon-derived builders — default-filled construction, maybe_* setters for Option<T> fields, no validation. Use for:
SubscribeRequest::builder()RetryPolicy::builder()ReconnectionConfig::builder()
use ;
use Duration;
let reconnect = builder
.max_attempts
.initial_delay
.build;
let retry = builder
.max_attempts
.initial_backoff
.max_backoff
.build;
2. Validating positional constructors — return Result<Self, MarketDataError> and reject bad inputs at construction. Use when validation matters:
ReconnectionConfig::new(max_attempts, initial_delay, max_delay)?
use ReconnectionConfig;
use Duration;
let reconnect = new?; // returns ConfigError on invalid inputs
3. Typestate factory — derives stock + futopt endpoint configurations from one auth credential. Use for full-application setup:
use WebSocketFactory;
use AuthRequest;
let cfg = new
.auth
.stock
.build;
4. Convenience constructors — one-liner for common cases. Use in examples, scripts, and one-shot integration tests:
ConnectionConfig::fugle_stock(auth)ConnectionConfig::fugle_futopt(auth)RetryPolicy::conservative()/RetryPolicy::aggressive()ReconnectionConfig::default()/ReconnectionConfig::disabled()
use ConnectionConfig;
use AuthRequest;
let cfg = fugle_stock;
Rule of thumb: Reach for the bon builder by default. If your inputs come from external configuration (env vars, JSON, a CLI flag) and you want bounds-checking at the boundary, use the positional new(...) constructor. Use the typestate factory when one auth credential drives multiple endpoint configurations.
Independent endpoints: RestClient::base_url(...) and WebSocketFactory::base_url(...) are independent. The two clients can target different hosts (REST on the public API, WebSocket on a separate edge / staging / private host) — see MIGRATION-0.7.md for the dual-host pattern.
Feature flags
| Feature | Default | Adds |
|---|---|---|
tokio-comp |
off | Async client (aio::WebSocketClient), tokio + tokio-tungstenite. |
tracing |
off | Hot-path debug!, lifecycle info!/warn!/error!, cold-path instrument. |
test-utils |
off | core::testing::MockWsServer + aio_pair / aio_pair_n (pulls tokio-comp). |
metrics |
off | Registers fugle_marketdata_ws_messages_dropped_total and fugle_marketdata_ws_events_dropped_total counters on the active metrics recorder. See MIGRATION-0.7.md. |
Error Handling
All operations return Result<T, MarketDataError>:
use MarketDataError;
match client.stock.intraday.quote.symbol.send
Error codes for FFI consumers:
| Code | Error Type |
|---|---|
| 1001 | InvalidSymbol |
| 1002 | DeserializationError |
| 1003 | RuntimeError |
| 1004 | ConfigError |
| 2001 | ConnectionError |
| 2002 | AuthError |
| 2003 | ApiError |
| 2010 | ClientClosed |
| 3001 | TimeoutError |
| 3002 | WebSocketError |
| 9999 | Other |
API Reference
See the API documentation for complete details.
REST Endpoints
Stock Intraday:
client.stock().intraday().quote()- Real-time quoteclient.stock().intraday().ticker()- Symbol informationclient.stock().intraday().candles()- OHLCV candlesclient.stock().intraday().trades()- Trade historyclient.stock().intraday().volumes()- Volume by price
FutOpt Intraday:
client.futopt().intraday().quote()- Real-time quoteclient.futopt().intraday().ticker()- Contract informationclient.futopt().intraday().tickers()- Multiple contractsclient.futopt().intraday().candles()- OHLCV candlesclient.futopt().intraday().trades()- Trade historyclient.futopt().intraday().volumes()- Volume by priceclient.futopt().intraday().products()- Product listing
WebSocket Channels
| Channel | Description |
|---|---|
| Trades | Real-time trade executions |
| Candles | Real-time candlestick updates |
| Books | Order book (5 levels bid/ask) |
| Aggregates | Aggregated market data |
| Indices | Index values (stock only) |
Examples
See the examples/ directory:
rest_basic.rs- REST API usagewebsocket_basic.rs- WebSocket streaming
Run examples:
License
MIT