Skip to main content

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_of is 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_of there 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

twmarketdata-0.1.0.tar.gz (27.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

twmarketdata-0.1.0-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

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

Hashes for twmarketdata-0.1.0.tar.gz
Algorithm Hash digest
SHA256 e4d9173d42565d7a8a84e8a6fbc6c6f0d7709f17f738e7ca79bef8f8b897275b
MD5 3f709724f694b23576c8ffdc067ecd29
BLAKE2b-256 688ea38c1c6a5b120443996dedff4441075d9a8f136b05be10c93e69e69386cb

See more details on using hashes here.

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

Hashes for twmarketdata-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7f24fe0f2378da2242e31c905c0ebdd6fa5d2c644cb1c1aa81d1943169540147
MD5 a5e5bb9fe89520efc069235b6b06ca9d
BLAKE2b-256 2ac59238a8a3c530676410de9c42a078a1e216edc60c11c96efc8d9c2a0f225b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page