Data-access client for the TW Market Data API. Retrieval only, for research and education.
Project description
twmd
Python data-access client for the TW Market Data API.
twmd retrieves published datasets over HTTP and returns them as pandas
DataFrames. It is a transport layer for data retrieval, intended for research
and educational use. It fetches records as the API publishes them and performs
no analysis, scoring, ranking, or interpretation of any kind. Deciding what the
data means, and what if anything to do about it, is entirely the caller's own
work and responsibility.
Responses carry the API's own lineage.not_investment_advice flag; this client
preserves it unmodified.
Install
pip install twmarketdata
The distribution is named twmarketdata; the import package is twmd:
import twmd
Pure Python. Dependencies are httpx and pandas — no compiled extensions, no
system libraries, installable in a restricted sandbox.
Requires Python 3.9 or newer. The floors are deliberately low (pandas>=1.5,
httpx>=0.27) so this installs into existing environments without forcing an
upgrade, and CI runs the full suite pinned to exactly those floors so they stay
honest rather than aspirational.
One caveat that is not ours to fix: pandas 1.5 wheels are built against the
numpy 1.x C ABI and fail at import under numpy 2 with numpy.dtype size changed. If you are pinned to pandas 1.x, pin numpy<2 alongside it. Anything
from pandas 2.2.2 onward works with numpy 2 unconstrained.
Quick start — no API key needed
Five sample tickers are served without credentials on selected datasets, so this runs with zero configuration:
from twmd import Client
client = Client()
df = client.get_dataset("twse-daily-price", symbol="2330", limit=5)
print(df[["date", "open", "high", "low", "close", "volume_shares"]])
print(df.attrs["data_as_of"]) # data freshness date
print(df.attrs["lineage"]) # provider, source endpoints, source table
Authentication
Set your key in the environment; it is never written to disk or logged:
export TWMD_API_KEY="sk_live_..."
Client() picks it up automatically. You may also pass Client(api_key=...)
explicitly. With no key set, the client runs in key-free mode and reaches the
datasets listed below.
Key-free access matrix
Key-free access is scoped per dataset, not globally per ticker. The same ticker that is served on one dataset may require a key on another. Measured against the live API on 2026-07-21:
| Tier | Datasets | Tickers served without a key |
|---|---|---|
| Open | security-master, market-index |
any |
| Sample | twse-daily-price, tpex-daily-price, monthly-revenue |
2330, 2317, 2454, 0050, 2603 only |
| Key required | everything else, including institutional-flow, market-prices, financial-metrics, income-statement, balance-sheet |
none |
Check before requesting:
from twmd import is_key_free
is_key_free("twse-daily-price", "2330") # True
is_key_free("twse-daily-price", "1101") # False — outside the sample set
is_key_free("security-master", "1101") # True — open dataset
is_key_free("institutional-flow", "2330") # False — key required
Datasets not in the table are treated as key-required, which is the safe default. The client never blocks a request on this basis — it only uses the matrix to explain a 401 after the fact.
DataFrames and metadata
Records become rows. Everything else in the response envelope is preserved on
df.attrs.
The API uses two envelope shapes, and twmd handles both transparently:
# rows / count — twse-daily-price, tpex-daily-price, monthly-revenue
df.attrs["dataset"] # "twse_daily_price"
df.attrs["count"] # record count
df.attrs["data_as_of"] # data freshness date
df.attrs["source_role"] # e.g. "official_twse"
df.attrs["lineage"] # provider, official_source, source_endpoints, table
df.attrs["meta"] # last_trading_day, market_status
# items / row_count — security-master, market-index
df.attrs["dataset_id"] # "security-master"
df.attrs["row_count"] # record count
df.attrs["as_of_date"] # snapshot date
df.attrs["survivorship_bias_warning"] # integrity caveat raised by the API
Envelope contents vary by dataset — monthly-revenue sends only dataset,
rows and count — so read attrs defensively.
Records in the items variant contain nested objects (security_identity,
market_identity, index_level). These stay as dict-valued columns rather than
being flattened, so a DataFrame reflects what the API actually sent. Expand one
when you want to:
import pandas as pd
identity = pd.json_normalize(df["security_identity"])
Note that security-master carries a survivorship_bias_warning stating the
current master is not point-in-time complete. It is surfaced on attrs
unmodified; check it before using that dataset for historical work.
Errors
from twmd import Client, TwmdAuthError, TwmdPaymentRequired
try:
df = client.get_dataset("institutional-flow", symbol="2330")
except TwmdAuthError as exc:
print(exc.error_code) # "missing_api_key" or "invalid_api_key"
print(exc.body) # decoded response body, verbatim
Every TwmdAPIError subclass — every row in the table below except the last —
exposes .status_code, .body (decoded body, unmodified), .text and
.error_code. TwmdTransportError and TwmdConfigError derive from TwmdError
directly and carry none of those, since no response was received.
| Status | Exception | Retried |
|---|---|---|
| 401 | TwmdAuthError |
no |
| 402 | TwmdPaymentRequired |
no |
| 404 | TwmdNotFoundError |
no |
| 422 | TwmdValidationError |
no |
| 429 | TwmdRateLimitError |
yes |
| 5xx | TwmdServerError |
yes |
| network failure | TwmdTransportError |
yes |
Retries use exponential backoff with jitter. A Retry-After header — in either
RFC 7231 form, delta-seconds or HTTP-date — overrides that and is honoured
exactly, with no jitter and no shortening, since retrying sooner than the server
permitted is worse than waiting too long. If it asks for more than
RETRY_AFTER_MAX (120s) the client stops and raises the server's error rather
than blocking for minutes.
401 messages are annotated with the key-free status of the dataset and ticker you asked for, so "I forgot my key" is distinguishable from "that ticker is not in the sample set".
On 402
No 402 response has been observed from this API. The probe on 2026-07-21
found zero endpoints declaring 402 in the published OpenAPI document, and
unauthenticated requests to key-gated datasets return 401 missing_api_key.
TwmdPaymentRequired exists as a documented extension point: if the API does
return 402 — the untested path is a valid but unentitled key, which requires
live credentials to exercise — the full decoded body is preserved verbatim on
.body and reaches your code intact. The client deliberately does not model or
require any field layout inside it, since no 402 body exists to model.
except TwmdPaymentRequired as exc:
handle_payment_required(exc.body) # whole body, nothing dropped
Source attribution
Pass source to mark every request with the integration that produced it, so
the publisher can attribute traffic:
client = Client(source="ecosys/tradingagents")
It is attached as a source query parameter on every request. A per-call
source= on get_dataset overrides the client-wide value. It is an ordinary
query parameter — it does not change the response, does not enter a data
response's request_context.filters, and carries nothing about the user. Leave
it unset to send nothing.
Pagination
df = client.get_all("twse-daily-price", symbol="2330", limit=1000)
for page in client.iter_pages("twse-daily-price", symbol="2330", limit=500):
process(page)
The API publishes no cursor: no cursor or next_cursor field appears in any
response or in the OpenAPI document. Paging is limit/offset, and offset
was measured to be ignored on the key-free endpoints — offset=3 returned
the same records as offset=0.
So pagination here is defensive. It advances offset as documented, then stops
when either a page comes back shorter than limit, or a page repeats the
previous page's first record — the signature of a silently ignored offset.
Against endpoints that ignore offset this yields exactly one page, which is
the correct outcome rather than a failure. max_pages caps the loop.
as_of
get_dataset() accepts an as_of argument and forwards it as a query
parameter. Its scope is narrow. As measured on 2026-07-21:
as_ofis declared on exactly four endpoints —income-statement,cash-flow-statement,balance-sheet,financials— all of which require an API key.- On every other endpoint it is not a declared parameter. The API silently
ignores unknown query parameters, returning 200 rather than 422, so passing
as_ofthere has no observable effect and raises no error. - Its behaviour on the four declared endpoints is unverified by this project, which has no credentials to exercise them.
Treat as_of as forwarded-but-unconfirmed rather than as a general
point-in-time facility. The data_as_of field in responses is a data-freshness
date and is a different thing.
Development
pip install -e ".[test]"
pytest -m "not live" # offline, no network
pytest -m live # hits the real API, key-free paths only
The live suite asserts a sample of the key-free matrix — seven dataset/ticker
pairs — still matches what the API serves, so drift in those surfaces as a test
failure. It is a tripwire, not full coverage: the five sample tickers are
verified only against twse-daily-price, and three datasets listed as
key-required are assumed rather than probed (see
access.PRESUMED_KEY_REQUIRED_DATASETS, and access.provenance() to tell the
two apart).
Scope
This package retrieves data. It does not generate recommendations, forecasts, signals, valuations, or opinions about any security, and nothing it returns should be read as such. The data is provided for research and educational purposes; verify it against the original sources before relying on it. Use of the underlying API is governed by TW Market Data's own terms.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file twmarketdata-0.1.0.tar.gz.
File metadata
- Download URL: twmarketdata-0.1.0.tar.gz
- Upload date:
- Size: 27.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e4d9173d42565d7a8a84e8a6fbc6c6f0d7709f17f738e7ca79bef8f8b897275b
|
|
| MD5 |
3f709724f694b23576c8ffdc067ecd29
|
|
| BLAKE2b-256 |
688ea38c1c6a5b120443996dedff4441075d9a8f136b05be10c93e69e69386cb
|
File details
Details for the file twmarketdata-0.1.0-py3-none-any.whl.
File metadata
- Download URL: twmarketdata-0.1.0-py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f24fe0f2378da2242e31c905c0ebdd6fa5d2c644cb1c1aa81d1943169540147
|
|
| MD5 |
a5e5bb9fe89520efc069235b6b06ca9d
|
|
| BLAKE2b-256 |
2ac59238a8a3c530676410de9c42a078a1e216edc60c11c96efc8d9c2a0f225b
|