This release is a pre-release and may not be stable for production use.
fugle-marketdata
Fugle market data REST and WebSocket client for Python, powered by a Rust core through PyO3.
Installation
pip install --pre fugle-marketdata # 3.x pre-release
Wheels are published for CPython 3.8+ (abi3) on Linux glibc and musl
(x86_64, aarch64), macOS (x86_64, arm64) and Windows x64. There is no source
distribution: on other platforms pip installs the pure-Python 2.x SDK instead.
See Unsupported platforms for how
to pin fugle-marketdata<3 or build 3.x from source.
Upgrading from 2.x
Most 2.x code runs unchanged, but a few things behave differently. Read the migration guide before upgrading; the changes most 2.x code runs into:
- The WebSocket
messagecallback receives a parseddict, not a JSON string (§3). Code that callsjson.loads(message)keeps working on theraw_messageevent, which delivers the string. HealthCheckConfigkeeps its name but not its fields:ping_intervalandmax_missed_pongsare ignored, with aFugleHealthCheckWarning(drop-in table).- Auto-reconnect is on by default; reconnect code of your own that calls
disconnect()thenconnect()loops with it — keep one or the other (§5). connect()raisesAuthErrorwhen the credentials are rejected (§9).- A callback that blocks (for example
time.sleep()in adisconnectcallback) holds up all events, and messages are dropped once the queue is full (§15). str(e)of an exception is its message, without 2.x's[Fugle API Error]prefix andURL:/Status:lines (§6).- A
subscribedmessage'sdatacan be a list (§16).
Development Build
# Create virtual environment
python -m venv .venv
source .venv/bin/activate
# Install maturin
pip install maturin
# Build and install in development mode
cd py
maturin develop
Release Build From Source
maturin build --release
pip install target/wheels/fugle_marketdata-*.whl
Quick Start
REST API
from fugle_marketdata import RestClient, MarketDataError
# Create client with API key
client = RestClient(api_key="your-api-key")
# Get stock quote
quote = client.stock.intraday.quote("2330")
print(f"TSMC Price: {quote['closePrice']}")
print(f"Change: {quote['change']}")
print(f"Volume: {quote['total']['tradeVolume']}")
# Get stock ticker info
ticker = client.stock.intraday.ticker("2330")
print(f"Name: {ticker['name']}")
# Get intraday candles (5-minute)
candles = client.stock.intraday.candles("2330", timeframe="5")
for candle in candles['data'][:3]:
print(f" {candle['date']}: O={candle['open']} H={candle['high']} L={candle['low']} C={candle['close']}")
# Get recent trades
trades = client.stock.intraday.trades("2330")
for trade in trades['data'][:5]:
print(f" Price: {trade['price']}, Size: {trade['size']}")
# FutOpt (futures/options) data: contract symbols expire, so look up the
# TAIEX futures (TXF) contracts listed today and take the nearest month
txf = client.futopt.intraday.tickers(type="FUTURE", product="TXF", is_spread=False)
front = min(txf['data'], key=lambda t: t['settlementDate'])['symbol']
futopt_quote = client.futopt.intraday.quote(front)
print(f"{front} Price: {futopt_quote['closePrice']}")
WebSocket Streaming
from fugle_marketdata import WebSocketClient
import time
# Create WebSocket client
ws = WebSocketClient(api_key="your-api-key")
# --- Callback Mode ---
def on_message(msg):
"""Handle incoming messages"""
if msg.get('event') == 'data':
channel = msg.get('channel')
data = msg.get('data', {})
symbol = data.get('symbol')
print(f"[{channel}] {symbol}: {data}")
def on_connect():
print("Connected!")
def on_disconnect(code, reason):
info = ws.stock.last_disconnect # who closed it, and whether a reconnect follows
print(f"Disconnected: {code} - {reason} ({info.intent}, will_reconnect={info.will_reconnect})")
def on_error(err): # a WebSocketError
print(f"Error [{err.code}]: {err}")
# Register callbacks
stock = ws.stock
stock.on("message", on_message)
stock.on("connect", on_connect)
stock.on("disconnect", on_disconnect)
stock.on("error", on_error)
# Connect and subscribe
stock.connect()
stock.subscribe("trades", "2330")
stock.subscribe("books", "2330")
# Keep running for 10 seconds
time.sleep(10)
# Disconnect
stock.disconnect()
# --- Iterator Mode ---
ws2 = WebSocketClient(api_key="your-api-key")
stock2 = ws2.stock
stock2.connect()
stock2.subscribe("trades", "2330")
# Iterate over messages
for msg in stock2.messages():
print(msg)
# Add break condition as needed
Authentication
Three authentication methods are supported:
from fugle_marketdata import RestClient
# 1. API Key (most common)
client = RestClient(api_key="your-api-key")
# 2. Bearer Token
client = RestClient(bearer_token="your-bearer-token")
# 3. SDK Token
client = RestClient(sdk_token="your-sdk-token")
Configuration
Reconnection Config
Auto-reconnect is on by default: after an unexpected drop the client
reconnects with exponential backoff, without an attempt limit (waits capped
at max_delay_ms), and subscribes again. A normal closure by the server
(close code 1000) and rejected credentials end the connection for good and
are not reconnected; every other close is. Pass reconnect to tune it:
from fugle_marketdata import WebSocketClient, ReconnectConfig
# Create custom reconnect configuration
reconnect = ReconnectConfig(
max_attempts=10,
initial_delay_ms=2000,
max_delay_ms=120000
)
ws = WebSocketClient(api_key="your-key", reconnect=reconnect)
# Turn auto-reconnect off
ws = WebSocketClient(api_key="your-key", reconnect=ReconnectConfig.disabled())
ReconnectConfig Options:
enabled(bool): Whether auto-reconnect is enabled (default: True)max_attempts(int): Maximum reconnection attempts; 0 means unlimited (default: 0). With a limit, theerrorcallback reports code 3005 once the last attempt fails; whatever the limit, it also does so when a reconnect attempt's credentials are rejected (afterunauthenticated)initial_delay_ms(int): Initial delay for exponential backoff (default: 1000ms, min: 100ms)max_delay_ms(int): Maximum delay cap (default: 60000ms)
connect() / connect_async() during an automatic reconnect opens no
connection of its own: it waits for the reconnect and returns once the
connection is back and the subscriptions have been re-sent (#230). So code
that calls connect() and subscribes again from a disconnect callback
keeps working; the repeated subscriptions are harmless
(migration guide §5).
Called from a callback, it holds up that client's callbacks until the
reconnect ends. It raises if the reconnect does not come back:
WebSocketError with code 2010 on disconnect(), code 3005 when the
attempts run out, and AuthError when the credentials are rejected.
Reconnect code that first calls disconnect() — typically a background
thread that runs disconnect() then connect() after the disconnect
callback set a flag — does not mix with auto-reconnect: it closes the
connection the SDK has just restored, which fires disconnect again, and
the two loop forever (#226). Remove that code or turn auto-reconnect off.
When connect() follows, within 30 seconds, a disconnect() that closed a
connection auto-reconnect restored less than 30 seconds earlier, the SDK
issues a RuntimeWarning (once per client) pointing at this. A
disconnect() with no connect() after it is not warned about.
To tell whether the SDK will reconnect after a disconnect, read
ws.stock.last_disconnect.will_reconnect in the callback rather than the
close code (#293): True means a reconnect follows, False that the
connection is over. A reconnect that gives up ends with an error of code
3005, not another disconnect.
Health Check Config
Liveness detection is on by default: when no inbound frame (data, heartbeat or
pong) arrives within heartbeat_timeout_ms, the connection is declared dead
and auto-reconnect takes over. The server sends a heartbeat every 30 seconds.
from fugle_marketdata import WebSocketClient, HealthCheckConfig
# Longer timeout
health_check = HealthCheckConfig(heartbeat_timeout_ms=60000)
ws = WebSocketClient(api_key="your-key", health_check=health_check)
# Turn liveness detection off
ws = WebSocketClient(api_key="your-key", health_check=HealthCheckConfig(enabled=False))
# Confirm with a ping before disconnecting; know within 10 seconds
ws = WebSocketClient(api_key="your-key",
health_check=HealthCheckConfig(probe_enabled=True,
idle_probe_after_ms=5000,
probe_timeout_ms=5000))
# Round trip on demand, in milliseconds (default timeout 5000), once connected
ws.stock.connect()
latency = ws.stock.measure_latency()
With probe_enabled, a silent connection is asked before it is declared dead:
after idle_probe_after_ms of silence one ping is sent, and only if nothing
arrives within probe_timeout_ms is the connection declared dead. The defaults
keep detection at 35 seconds and send no ping while the server's heartbeat is on
time, so turning on probe_enabled alone only removes false disconnects caused
by a late heartbeat.
HealthCheckConfig Options:
enabled(bool): Whether health check is enabled (default: True)heartbeat_timeout_ms(int): Maximum gap between inbound frames before the connection is declared dead (default: 35000ms, min: 5000ms). Does not apply whenprobe_enabledis True.probe_enabled(bool): Confirm with a ping before declaring the connection dead (default: False). Detection isidle_probe_after_ms + probe_timeout_ms.idle_probe_after_ms(int): Silence before the ping (default: 30000ms, the server's heartbeat period; min: 5000ms). Below 30000 a ping is sent in every gap between heartbeats while no data flows.probe_timeout_ms(int): Wait for any inbound frame after the ping (default: 5000ms, min: 1000ms)
Probing does not detect a half-open connection (the server still sends, our writes no longer arrive). See docs/configuration.md for the trade-offs and the server cost of short probe intervals.
Auth Timeout
ws = WebSocketClient(api_key="your-key", auth_timeout_ms=15000) # default 10000
Once the WebSocket is open the client sends its auth frame and waits for the
server's verdict for auth_timeout_ms (default 10 s). It applies to the
first connect and to every reconnect; elapsing it fails the attempt with
TimeoutError (code 3001). Raise it on a slow route to the server; the
server itself allows 60 s. It must be greater than 0 (ConfigError, code
1004, otherwise).
Combined Configuration
from fugle_marketdata import WebSocketClient, ReconnectConfig, HealthCheckConfig
reconnect = ReconnectConfig(max_attempts=10, initial_delay_ms=2000)
health_check = HealthCheckConfig(heartbeat_timeout_ms=60000)
ws = WebSocketClient(
api_key="your-key",
reconnect=reconnect,
health_check=health_check,
auth_timeout_ms=15000,
)
API Reference
RestClient
Every method below blocks and returns the response as a dict. Each has an
_async sibling that returns a coroutine instead, for asyncio code:
quote_async, candles_async, ... (await client.stock.intraday.quote_async("2330")).
Stock
client.stock.intraday.quote(symbol) # Real-time quote
client.stock.intraday.ticker(symbol) # Symbol information
client.stock.intraday.tickers(type) # Symbol list ("EQUITY", "INDEX", ...)
client.stock.intraday.candles(symbol, timeframe="1") # OHLCV candles
client.stock.intraday.trades(symbol) # Trade history
client.stock.intraday.volumes(symbol) # Volume by price
client.stock.historical.candles(symbol, from_date=..., to_date=..., timeframe="D")
client.stock.historical.stats(symbol) # 52-week statistics
client.stock.snapshot.quotes(market) # Quotes of a whole market ("TSE", "OTC", ...)
client.stock.snapshot.movers(market, direction, change) # Top gainers / losers
client.stock.snapshot.actives(market, trade) # Most active by "volume" / "value"
client.stock.technical.sma(symbol, period) # also rsi(symbol, period), bb(symbol, period),
client.stock.technical.kdj(symbol, r_period, k_period, d_period)
client.stock.technical.macd(symbol, fast, slow, signal)
client.stock.ownership.etf_holdings(symbol)
client.stock.ownership.institutional_trades(symbol)
client.stock.ownership.director_holdings(symbol)
client.stock.ownership.tdcc_distribution(symbol)
client.stock.corporate_actions.capital_changes() # all keyword arguments
client.stock.corporate_actions.dividends()
client.stock.corporate_actions.listing_applicants()
FutOpt
client.futopt.intraday.quote(symbol) # Real-time quote
client.futopt.intraday.ticker(symbol) # Contract information
client.futopt.intraday.tickers(type) # Contract list ("FUTURE" or "OPTION")
client.futopt.intraday.candles(symbol, timeframe="1") # OHLCV candles
client.futopt.intraday.trades(symbol) # Trade history
client.futopt.intraday.volumes(symbol) # Volume by price
client.futopt.intraday.products(type) # Product listing ("FUTURE" or "OPTION")
client.futopt.historical.candles(product, contract_month="1!") # "TXF"; also product=...
client.futopt.historical.daily(product, date=...) # one day, a row per contract
Client
RestClient(api_key=...) # or bearer_token= / sdk_token=, plus base_url=
RestClient.with_bearer_token(token) # same as RestClient(bearer_token=token)
RestClient.with_sdk_token(sdk_token) # same as RestClient(sdk_token=sdk_token)
client.base_url # https://api.fugle.tw/marketdata/v1.0
client.stock.base_url # .../v1.0/stock (client.futopt.base_url: .../v1.0/futopt)
Query parameters
Every REST method takes the endpoint's query parameters as keyword arguments,
under the 3.x snake_case names, the API's own names as documented on
developer.fugle.tw (the spelling the 2.x SDK used), or the 2.x from_ alias
for the reserved word:
client.stock.intraday.trades("2330", limit=5, sort="asc", is_trial=True)
client.stock.intraday.trades("2330", limit=5, sort="asc", isTrial=True) # same call
client.stock.intraday.ticker("2330", odd_lot=True)
client.stock.intraday.ticker("2330", type="oddlot") # same call
client.stock.technical.sma("2330", from_date="2026-08-01", to_date="2026-09-10", period=5)
client.stock.technical.sma("2330", from_="2026-08-01", to="2026-09-10", period=5) # same call
Only the path parameter (symbol, market, type) and the query
parameters the server requires (movers(market, direction, change),
actives(market, trade), the technical periods — sma(symbol, period),
kdj(symbol, r_period, k_period, d_period), macd(symbol, fast, slow, signal)) may be passed positionally; every optional one is keyword-only,
so candles("2330", "5") is a TypeError rather than a slot to get wrong.
A keyword the endpoint does not take raises TypeError naming the accepted
ones, and so does one parameter given under two spellings. Values are sent as
given and the server reports a bad value, except the two switches behind a
boolean keyword: type must be "oddlot" and session "afterhours" /
"regular", or ValueError.
Type checkers (mypy, pyright) only know the snake_case keywords: the stubs
list them and no **kwargs, so isTrial= or from_= is flagged even though
it works at runtime. Use the snake_case names in code you type-check; the
other spellings are a runtime compatibility layer for 2.x call sites.
WebSocketClient
Properties
ws.stock # Access StockWebSocketClient
ws.futopt # Access FutOptWebSocketClient
StockWebSocketClient / FutOptWebSocketClient Methods
client.connect() # Connect to server
client.disconnect() # Disconnect from server
client.is_connected() # Check connection status
client.is_closed() # Check if client is closed
client.url # Resolved endpoint, e.g. wss://api.fugle.tw/marketdata/v1.0/stock/streaming
client.subscribe(channel, symbol) # Subscribe to channel
client.subscribe(channel, symbols=[...]) # ... for several symbols in one frame
client.unsubscribe(subscription_id) # Unsubscribe by server ID (or ids=[...])
client.unsubscribe(channel=channel, symbol=symbol) # Unsubscribe by subscribe() arguments
client.subscriptions() # Ask the server for its list: returns None, the reply
# arrives as a message ({"event": "subscriptions", ...})
client.local_subscriptions() # The SDK's own list of active subscriptions, returned now
client.on(event, callback) # Register event callback (not async def)
client.off(event, listener=None) # Unregister one callback (==), or all for the event
client.messages() # Get message iterator
client.messages(raw=True) # ... yielding each message as the str the server sent
client.ping(state=None) # Send a ping frame
client.measure_latency(timeout_ms=None) # Round trip to the server, in milliseconds
Both clients also have async versions, which do not block the event loop:
await client.connect_async()
await client.disconnect_async()
await client.subscribe_async(channel, symbol) # odd_lot= (stock) / after_hours= (futopt)
await client.measure_latency_async(timeout_ms=None)
async with ws.futopt as client: # connect_async() on entry, disconnect_async() on exit
await client.subscribe_async("trades", front) # a contract symbol, e.g. from Quick Start's REST example
async for msg in client.messages():
print(msg)
Event Types
| Event | Callback Signature | Description |
|---|---|---|
message |
fn(msg: dict) |
Incoming data message |
raw_message |
fn(raw: str) |
The same message as the text the server sent, not turned into a dict |
connect |
fn() |
Connection established |
authenticated |
fn(frame: dict) |
Credentials accepted; the server's frame as in 2.x, {"event": "authenticated", "data": {...}} |
unauthenticated |
fn(frame: dict) |
Credentials rejected; the server's frame as in 2.x, {"event": "error", "code": 1000, "data": {"message": ...}} |
disconnect |
fn(code: int, reason: str) |
Connection closed |
reconnect |
fn(attempt: int) |
An automatic reconnect attempt starts (1, 2, ...) |
error |
fn(err: WebSocketError) |
Error occurred (err.args == (message, code), str(err) is the message) |
messages_dropped |
fn(dropped: int, total: int) |
Messages dropped because you fell behind (at most once per second, and before disconnect) |
These are the events on() accepts; any other name raises ValueError.
Names are matched ignoring case, and data, connected,
disconnected / close / closed and reconnecting are accepted as
aliases of message, connect, disconnect and reconnect.
disconnect's code is None when the connection ended without one. The
rest of the story is ws.stock.last_disconnect / ws.futopt.last_disconnect
(#293), a DisconnectInfo or None before the first disconnect:
| Attribute | Value |
|---|---|
code |
int or None, as passed to disconnect |
reason |
str, as passed to disconnect |
intent |
"client" (your disconnect()), "server" (the server's Close frame, any code) or "network" (transport error, EOF without a Close frame, heartbeat timeout) |
will_reconnect |
True if a reconnect follows (unless you call disconnect() first), False if the connection is over |
It is set before the disconnect callbacks run, so a callback reads the
disconnect it is handling, and it is never cleared: it is a record of the last
disconnect, not the connection state (ask is_connected() for that). After a
messages() iterator ends, it tells you why. A reconnect that gives up
(error 3005) leaves it at the drop that started the reconnect.
If you call disconnect() in a callback and connect() right after, the old
connection's last disconnect can be written after the new connection's, as it
is handed to the callbacks after it; for the connection state use
is_connected().
Raw Messages
stock.on("raw_message", lambda raw: sink.write(raw)) # raw is a str
for raw in stock.messages(raw=True): # or: async for
sink.write(raw)
raw_message hands you each message as the JSON text the server sent;
json.loads(raw) equals the dict message gets for it. Use it to forward or
store messages, to filter before parsing, or to parse with your own JSON
library. With only raw_message callbacks registered the SDK builds no dict.
With both, each message goes to raw_message first, then to message.
messages(raw=True) is the iterator counterpart; raw is keyword-only and
belongs to that iterator, so messages() on the same client still yields
dicts. A raw and a non-raw iterator open on one client read the same queue:
each message goes to only one of them.
Callback Exceptions
An exception raised in a callback does not stop the connection or later
callbacks. It is passed to the error callbacks as a WebSocketError with
code 3004, err.event (the event whose callback raised, e.g. "message"),
err.count and the original exception as err.__cause__:
def on_error(err):
if err.code == 3004:
print(f"{err.event} callback failed {err.count}x:", repr(err.__cause__))
The first failure is reported at once, later ones at most once per second,
each report counting the failures since the previous one; failures after the
last report are not reported on their own. Without an error callback, or
when it raises too, the failure goes to sys.unraisablehook (printed with its
traceback by default). KeyboardInterrupt, SystemExit and other
BaseExceptions that are not Exceptions are only printed that way.
Callbacks run on the SDK's thread and must be regular functions: on()
raises TypeError for an async def callback, and a callback that returns a
coroutine is reported as a failure. Use async for msg in stock.messages()
in asyncio code.
All of a client's callbacks run one at a time on that one thread, so a
callback that blocks (a time.sleep() or retry loop in disconnect, say)
holds up every later event, and messages past message_buffer are dropped
meanwhile (see Message Queue below and
migration guide §15).
Hand slow work to your own thread.
disconnect() returns once the stream readers of the connection it closes,
of a connect it aborts, and of earlier connections left for it have ended,
so their disconnect callbacks have fired. A connection that another
thread's connect() opens meanwhile is not waited for.
Called from a callback of any client — this one or another, such as a
ws.stock callback calling ws.futopt.disconnect() — it waits for no stream
reader, since two callbacks waiting for each other's would never return. No
disconnect callback is guaranteed to have fired when it returns; those
readers are left to the next disconnect() called outside a callback. Do not
wait for disconnect_async() synchronously from a callback (with
asyncio.run(...), say): it waits for the callback's own reader and never
completes.
Message Queue
ws = WebSocketClient(
api_key="key",
message_overflow="drop_newest", # default; or "unbounded"
message_buffer=4096, # unread messages held (default 4096)
)
ws.stock.on("messages_dropped", lambda dropped, total: print(dropped, total))
ws.stock.messages_dropped_total() # this connection's drops; still readable after disconnect()
With "drop_newest", while message_buffer messages are unread (your
message callback or iterator is behind), new messages are dropped, counted
and reported through messages_dropped. "unbounded" never drops; memory
grows for as long as you lag.
Channels
| Channel | Description |
|---|---|
trades |
Real-time trade executions |
candles |
Candlestick updates |
books |
Order book (5 levels) |
aggregates |
Aggregated market data |
indices |
Index values (stock only) |
MessageIterator
# Get iterator from connected client
messages = stock.messages()
# Iterate (blocking): yields messages only, stops once the connection is gone
for msg in messages:
print(msg)
# Async iteration
async for msg in stock.messages():
print(msg)
# Manual iteration
msg = next(messages) # Blocking until a message arrives
msg = messages.try_recv() # Non-blocking, returns None if no message
msg = messages.recv_timeout(5000) # Waits up to 5000 ms, returns None on timeout
Iteration never yields None and does not end while no data arrives; it
raises StopIteration / StopAsyncIteration once the connection is gone. A
blocked for loop or recv_timeout still reacts to Ctrl+C, and cancelling
an async for step that is still waiting takes no message (steps that are
already done are described below). messages(timeout_ms=...) is deprecated
and ignored. For periodic
work while no data arrives, use message callbacks or async for alongside
other tasks.
While messages are queued, async for reads them without waiting on the
event loop, and lets other tasks on the loop run once every 32 messages. It
does so because messages().__anext__() then returns an awaitable that is
already done and holds the message. async for and a plain await of it
lose no message; code that handles the awaitable itself must not discard a
done one:
step = asyncio.ensure_future(messages.__anext__())
done, _ = await asyncio.wait({step, stop}, return_when=asyncio.FIRST_COMPLETED)
if step.done(): # check this first, whatever else finished
handle(step.result())
else:
step.cancel() # pending: takes no message
step.cancel()returns False for a done awaitable; the message isstep.result().- Cancelling "the rest" after
asyncio.waitbecausestopfinished drops the message a donestepholds. - A cancelled
asyncio.gather(messages.__anext__(), other)drops the message its done step holds. - Awaitables of one iterator held at the same time may not get the messages
in the order they were created. A single
async forgets them in order.
asyncio.wait_for(messages.__anext__(), timeout):
- loses no message when
timeoutis longer than one turn of the event loop, in practice about 1 ms or more.timeout=0returns the message when one is queued instead of always timing out. - with a shorter
timeout, can lose a queued message whose delivery was left to the loop (one in 32): the awaitable is resolved one turn before the waiting task resumes, and a timeout that expires within that turn cancels the task with the message already in the awaitable. Seen on Python 3.12 withtimeout=1e-6. - up to Python 3.11, spends a turn of the loop even on a done awaitable. A
task cancelled in that turn gets the message back instead of
CancelledError(3.8.6 and later; run on 3.8.10 only), sotask.cancel()on awhile True: await wait_for(...)loop may only take effect at a step with nothing queued. Python 3.8.0 to 3.8.5 raiseCancelledErrorthere and drop that message.
Messages go to message and raw_message callbacks when any are registered
as they arrive, otherwise to the iterator. The iterator holds at most 4096
unread messages; while it does, lifecycle callbacks (disconnect,
reconnect, …) that follow those messages wait until you read or call
disconnect(). Every wait releases the GIL.
Error Handling
All API errors raise MarketDataError (or a subclass):
from fugle_marketdata import RestClient, MarketDataError
client = RestClient(api_key="invalid-key")
try:
quote = client.stock.intraday.quote("2330")
except MarketDataError as e:
print(f"Error [{e.code}] {e.source_kind}: {e.message}")
if e.status is not None:
print(e.status, e.body, e.headers.get("retry-after"))
Every exception carries the same fields as the other languages:
| Attribute | Type | |
|---|---|---|
code |
int |
Error code (table below); also args[1] |
source_kind |
str |
"network", "protocol", "auth", "rate_limit" or "client" |
message |
str |
Human-readable message; also args[0] and str(e) (2.x's str(e) also had the URL, status and response: see §6) |
status |
int | None |
HTTP status |
body |
str | None |
Raw HTTP response body (REST) |
request_id |
str | None |
x-request-id response header |
headers |
dict[str, str] |
HTTP response headers, lowercase names (REST; else empty) |
status_code and response_text remain as aliases of status and body
for code written against the 2.4.1 FugleAPIError. The WebSocket error
callback receives a WebSocketError with these fields, whose args stay
(message, code). See the
error reference for all languages.
Every exception class is a MarketDataError: ApiError (with
RateLimitError under it), AuthError, ConfigError, ConnectionError,
TimeoutError and WebSocketError. None of them is a built-in ValueError.
An invalid ReconnectConfig or HealthCheckConfig value and the credential
rule raise ConfigError (code 1004); the other constructor keyword checks
(base_url, TLS options, message_buffer, ...) and the REST argument checks
keep their built-in TypeError / ValueError.
Error Codes
| Code | Error Type | Description |
|---|---|---|
| 1001 | InvalidSymbol | Invalid or unsupported symbol |
| 1002 | DeserializationError | JSON parsing failed |
| 1003 | RuntimeError | Internal runtime error |
| 1004 | ConfigError | Invalid configuration: not exactly one credential, or a ReconnectConfig / HealthCheckConfig value below its floor |
| 1005 | InvalidParameter | Invalid or missing parameter (including an unknown WebSocket channel) |
| 2001 | ConnectionError | A REST request cannot reach the server (DNS, connection refused, TLS), a WebSocket command is sent while the connection is down (not before connect() or after disconnect(): that is a RuntimeError), or the WebSocket auth handshake fails for a reason other than rejected credentials |
| 2002 | AuthError | Authentication failed |
| 2003 | ApiError | API returned error response |
| 2010 | ClientClosed, ConnectionAborted | Client has been closed, or connect() / connect_async() was given up because disconnect() was called before the connection was established or while it waited on an automatic reconnect (raised as WebSocketError, message Connection aborted: …) |
| 2011 | AlreadyConnected | connect() / connect_async() called while connected or while another connect is in progress; during an automatic reconnect it waits instead (raised as WebSocketError) |
| 2012 | ConnectionLimit | The server refused the WebSocket connection because the account is at its connection limit (Close 1013, or error 1003 / Close 1001 with the message Maximum number of connections reached). connect() / connect_async() raise ConnectionError; the error callback receives WebSocketError; both with code 2012. Automatic reconnect keeps retrying (see configuration) |
| 3001 | TimeoutError | Operation timed out |
| 3002 | WebSocketError | WebSocket connect, read or write failed |
| 3003 | HeartbeatTimeout | No inbound WebSocket frame within the heartbeat window. Not reported as an error: disconnect fires with code None and the reason Heartbeat timeout after …, and last_disconnect.intent is "network" |
| 3004 | CallbackFailed | A WebSocket callback raised an exception (error callback only) |
| 3005 | ReconnectFailed | Reconnection gave up: after the last attempt, or because an attempt's credentials were rejected (error callback); a connect() / connect_async() waiting on the reconnect raises it (WebSocketError) after the last attempt |
| 9999 | Other | Unexpected error |
| -1 | ThreadPanic | A WebSocket worker thread panicked (error callback only) |
Examples
Full REST Example
from fugle_marketdata import RestClient, MarketDataError
import os
def main():
api_key = os.environ.get("FUGLE_API_KEY")
if not api_key:
print("Set FUGLE_API_KEY environment variable")
return
client = RestClient(api_key=api_key)
try:
# Stock data
print("=== Stock Market Data ===")
quote = client.stock.intraday.quote("2330")
print(f"TSMC Quote: {quote['closePrice']}")
ticker = client.stock.intraday.ticker("2330")
print(f"Ticker: {ticker['name']}")
candles = client.stock.intraday.candles("2330", timeframe="5")
print(f"Candles: {len(candles['data'])} entries")
# FutOpt data
print("\n=== FutOpt Market Data ===")
products = client.futopt.intraday.products("FUTURE")
print(f"Futures products: {len(products['data'])}")
except MarketDataError as e:
print(f"Error [{e.code}]: {e}")
if __name__ == "__main__":
main()
Full WebSocket Example
from fugle_marketdata import WebSocketClient, MarketDataError
import os
import time
def main():
api_key = os.environ.get("FUGLE_API_KEY")
if not api_key:
print("Set FUGLE_API_KEY environment variable")
return
ws = WebSocketClient(api_key=api_key)
stock = ws.stock
message_count = 0
def on_message(msg):
nonlocal message_count
message_count += 1
if msg.get('event') == 'data':
print(f"[{message_count}] {msg.get('channel')}: {msg['data'].get('symbol')}")
def on_connect():
print("Connected!")
def on_error(err): # a WebSocketError
print(f"Error [{err.code}]: {err}")
stock.on("message", on_message)
stock.on("connect", on_connect)
stock.on("error", on_error)
try:
stock.connect()
stock.subscribe("trades", "2330")
stock.subscribe("books", "2330")
print("Listening for 10 seconds...")
time.sleep(10)
print(f"\nReceived {message_count} messages")
print(f"Subscriptions: {stock.local_subscriptions()}")
except MarketDataError as e:
print(f"Error [{e.code}]: {e}")
finally:
if stock.is_connected():
stock.disconnect()
print("Done")
if __name__ == "__main__":
main()
Requirements
- Python 3.8+
- Rust toolchain (for building from source)
- maturin (for development builds)
License
Licensed under either of MIT or Apache-2.0, at your option.
Metadata
Release files for fugle-marketdata 3.0.0rc11
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distributions (wheels)
Total release size: 24.1 MB
Release files / fugle_marketdata-3.0.0rc11-cp38-abi3-win_amd64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-win_amd64.whl |
|---|---|
| Size | 3.0 MB |
| Tags | CPython 3.8 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1dce62a79c8616438a3b4808a8e2e9a9e8cb3615a490a0c2ba691a2a050c59a6
|
|
BLAKE2b-256 checksum How to use checksums |
06635ccdbcdfd957437a43a4bd38f49a0d164f6b9351e3c429cf994eeff2e0f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-musllinux_1_2_x86_64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 3.8 MB |
| Tags | CPython 3.8 Linux musl 1.2+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
4b15f95bcc5686a628d0e4265cbfe3cf62152e17026ba9365d94149530868aad
|
|
BLAKE2b-256 checksum How to use checksums |
068eb804ebd948b920626ca8f44cab01769fde83b579d4786a1ee57c37d97164
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-musllinux_1_2_aarch64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 3.8 MB |
| Tags | CPython 3.8 Linux musl 1.2+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
a47ac0bf28b8aa74ea640cf4a290aa053c0fc42d6962cf845b54495e45efef7c
|
|
BLAKE2b-256 checksum How to use checksums |
f6ddea481b1dcc50dae4e4e393f992b268f6c039e70496a2c5c864976c66f187
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 3.4 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
c73157fd547c7060fff106da0116d9326a51f0f50dce4c29309a31932e5b787a
|
|
BLAKE2b-256 checksum How to use checksums |
d8abfdb8b221b9ef49aca253d765e6916f257911c534fc2968c5a7b7c26df14c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 3.6 MB |
| Tags | CPython 3.8 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
048be48b9551e873ca5e8e38c7f5c6fbc89a127489c0cffe4edc83b7a852baae
|
|
BLAKE2b-256 checksum How to use checksums |
729ab8a410c3f7172a4df4a6e1f3686522c5881d9f8ccfcb87bc3bee76ad536b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-macosx_11_0_arm64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | CPython 3.8 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
cc90a04c132ad624239e74d6ad8768140c7eee73716a7472d34e6c90a5b099ed
|
|
BLAKE2b-256 checksum How to use checksums |
fe4439723a168bbf7fd752be16e4686fc797d320ad357ea607ad825f73bc7a38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logRelease files / fugle_marketdata-3.0.0rc11-cp38-abi3-macosx_10_12_x86_64.whl
| Download URL | fugle_marketdata-3.0.0rc11-cp38-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 3.2 MB |
| Tags | CPython 3.8 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
ed48227fffcddb6e9dbca60f3e7def24196a267dcd3f09c84e5a97e7db5d7318
|
|
BLAKE2b-256 checksum How to use checksums |
abcfb1231146ded8a6a1bf16b2d259eeb52b39f4fe2e863007b71e0c3dd65e1c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logSecurity for fugle-marketdata 3.0.0rc11
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 7 files in this release have been uploaded with provenance metadata.
Attestation for fugle_marketdata-3.0.0rc11-cp38-abi3-win_amd64.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.