Skip to main content

Python client and macOS service manager for a local tdx-api market data service.

Project description

pytdxfeed

PyPI Python CI License

English | 简体中文

pytdxfeed is a Python client and macOS service manager for a local tdx-api market-data service. It gives Python applications one strict HTTP client contract and, on macOS, installs a pinned native service without requiring Docker or a local Go toolchain.

The service is always local: the host is fixed to 127.0.0.1, the default port is 8080, and callers may select another explicit port.

Installation

Python 3.11-3.13 is supported.

uv add pytdxfeed

Or install it into the current uv environment:

uv pip install pytdxfeed

Quick Start

On macOS, ensure_service() installs or upgrades the bundled service, registers its per-user LaunchAgent, waits for strict health, and returns its runtime details. On other operating systems it only verifies an already running local service.

from pytdxfeed import TdxApiClient, ensure_service

service = ensure_service()
print(service.base_url)  # http://127.0.0.1:8080

with TdxApiClient(timeout_sec=10.0) as client:
    print(client.health())

    raw_daily = client.kline_all(
        "sh600000",
        source="tdx",
        interval="day",
        limit=20,
    )
    qfq_daily = client.kline_all(
        "sh600000",
        source="ths",
        interval="day",
        limit=20,
    )
    index_daily = client.index_all("sh000300", interval="day", limit=20)
    quotes = client.batch_quote(["sh600000", "sz000001"])
    recent_trades = client.trade("sh600000")

Use the same explicit port for service management and client requests:

from pytdxfeed import TdxApiClient, ensure_service

service = ensure_service(port=18080)
with TdxApiClient(port=service.port) as client:
    client.health()

The port must be an integer from 1 to 65535. The host and full base URL are not configurable. The runtime never scans for a free port.

Near-tick Polling

batch_quote() provides the current quote and five-level order-book snapshot, while trade() returns a rolling window of recent transaction records. Polling both endpoints can provide near-tick behavior for a small symbol set:

from time import monotonic, sleep

from pytdxfeed import TdxApiClient, TdxApiError

CODES = ("sh600000", "sz000001")
POLL_INTERVAL_SEC = 1.0
TRADE_KEY_FIELDS = ("Time", "Price", "Volume", "Status", "Number")


def trade_rows(payload: object) -> list[dict]:
    if not isinstance(payload, dict):
        return []
    data = payload.get("data")
    if not isinstance(data, dict):
        return []
    rows = data.get("List")
    return [row for row in rows if isinstance(row, dict)] if isinstance(rows, list) else []


def trade_key(row: dict) -> tuple:
    return tuple(row.get(field) for field in TRADE_KEY_FIELDS)


previous_windows: dict[str, set[tuple]] = {}

with TdxApiClient(timeout_sec=3.0) as client:
    while True:
        started = monotonic()
        try:
            quote_snapshot = client.batch_quote(CODES)
            new_trades: dict[str, list[dict]] = {}

            for code in CODES:
                rows = trade_rows(client.trade(code))
                current_keys = {trade_key(row) for row in rows}
                previous_keys = previous_windows.get(code)
                new_trades[code] = (
                    []
                    if previous_keys is None
                    else [row for row in rows if trade_key(row) not in previous_keys]
                )
                previous_windows[code] = current_keys

            # Replace this with a queue, callback, or strategy handler.
            print({"quotes": quote_snapshot, "new_trades": new_trades})
        except TdxApiError as exc:
            print(f"poll failed: {exc}")

        elapsed = monotonic() - started
        sleep(max(0.0, POLL_INTERVAL_SEC - elapsed))

The first response establishes a baseline instead of replaying up to 1,800 old records. Later responses emit rows not present in the previous rolling window. This is best-effort polling: if more records arrive between polls than the endpoint retains, intermediate trades can still be missed. Reduce request load or increase the interval as the symbol count grows.

Capabilities

Capability Public API Scope
Strict health check health() Requires the exact healthy response from the pinned service
Stock K-lines kline_all() Raw TDX or THS-adjusted stock data
Index K-lines index_all() Full index history for one interval
Quote snapshots batch_quote() 1-50 explicit symbols per request
Recent trades trade() Polling snapshot of up to 1,800 recent transaction records
Near-tick polling batch_quote() + trade() Caller-controlled polling with incremental trade detection
macOS lifecycle ensure_service() Installs, upgrades, starts, migrates ports, and checks the LaunchAgent
Other platforms ensure_service() Health-checks an existing service; it does not install one
Push tick stream Not available No WebSocket, server-side subscription, or exchange event stream
Storage API Not available The package returns upstream JSON and does not write user market data

pytdxfeed therefore supports near-tick polling and transaction-level records, but does not claim exchange-native push delivery or lossless tick capture.

Public Methods

Method Important arguments Upstream endpoint
ensure_service() port=8080, timeout_sec=2.0, startup_timeout_sec=120.0 Service lifecycle and /api/health
TdxApiClient() timeout_sec=10.0, port=8080 Creates thread-local HTTP sessions
health() None GET /api/health
kline_all() code, source, interval, optional limit and end_date `GET /api/kline-all/{tdx
index_all() code, interval, optional limit GET /api/index/all
batch_quote() Iterable of 1-50 codes POST /api/batch-quote
trade() One code GET /api/trade

Codes use the upstream lower-case exchange prefix, for example sh600000, sz000001, or bj920002. Responses are the upstream JSON objects; pytdxfeed does not rename fields, rescale prices, or convert results to pandas objects.

end_date is an explicit historical as-of boundary (YYYY-MM-DD). For THS qfq requests it prevents a newer, still-publishing tail from invalidating an older completed date; it does not relax the same-date raw/qfq validation.

K-line Coverage

Source Intervals Adjustment Typical coverage and limits
tdx stock minute1, minute5, minute15, minute30, hour Raw About 24,000 bars per interval; the calendar span grows with the interval
tdx stock day, week, month, quarter, year Raw Usually listing-to-present; the service joins underlying 800-bar batches
ths stock day, week, month Forward-adjusted Listing-to-latest data available from the THS endpoint
Index Same interval names as TDX Index points Coverage depends on the selected index and upstream servers

For THS forward-adjusted daily data, the bundled service treats all.js as the history source. From 15:10 Asia/Shanghai onward it can complete a missing closed tail from today.js, but only after the date and integer-milliyuan OHLC match the same TDX raw daily bar exactly. TDX supplies that tail's volume and amount. Incomplete or mismatched responses fail explicitly; last.js is not used.

The minute history is bar-count limited, not year limited. In a check on sh600000 dated 2026-07-20, minute1 returned 23,520 bars from 2026-02-26 and minute5 returned 23,952 bars from 2024-06-28. Daily TDX data returned 6,349 bars from the 1999-11-10 listing date. Different symbols and upstream servers can produce different ranges.

Calls fetch the latest data available from the selected endpoint. pytdxfeed has no scheduler and makes no freshness guarantee beyond the successful upstream response.

Failure Semantics

The client does not retry requests or switch sources. This keeps the selected market-data source observable.

from pytdxfeed import (
    TdxApiClient,
    TdxApiDeploymentError,
    TdxApiResponseError,
    TdxApiTransportError,
)

try:
    with TdxApiClient(timeout_sec=10.0) as client:
        payload = client.kline_all(
            "sh600000",
            source="tdx",
            interval="minute1",
        )
except TdxApiTransportError as exc:
    print(f"HTTP connection failed: {exc}")
except TdxApiResponseError as exc:
    print(f"tdx-api rejected the request: {exc}")
except TdxApiDeploymentError as exc:
    print(f"the managed service could not start: {exc}")

If the selected port is occupied by another strictly healthy tdx-api, it is reused as an external service. If an unknown process returns an unhealthy or incompatible response, startup fails without terminating that process.

Managed Runtime

Setting Value
Host 127.0.0.1
Default port 8080
Managed platforms macOS arm64 and x86_64
Runtime root ~/.pytdxfeed
Persistent service data ~/.pytdxfeed/data/database
LaunchAgent label com.hermanzhaozzzz.pytdxfeed.tdx-api
Logs ~/.pytdxfeed/logs

Changing the configured port rebuilds the managed plist and restarts the owned service while preserving its data directory. Newer compatible artifact generations are never downgraded.

Bundled tdx-api

Item v1.0.3 value
Source repository hermanzhaozzzz/tdx-api
Source commit e6778c6b8ec4cbf0705dc1fe4c3c413a6b6518ff
Upstream repository oficcejo/tdx-api
Upstream base ea07dccb67aeb92ebde851ac31503b4bf457a318
Source commit date 2026-07-23
Service API major 1
Artifact generation 5
Bundled architectures darwin-arm64, darwin-x86_64
Reviewed fork changes Strict TDX_API_PORT; THS response validation; verified qfq close tail; historical qfq end_date isolation

The source fork tracks the upstream project and preserves its general-purpose code only. The upstream project declares the MIT License in its README. pytdxfeed's own source and release metadata are provided under the MIT License.

Performance

The following local measurements used an Apple M2 Mac mini (8 cores, 16 GB), macOS 26.5.2, sh600000, a warm service on loopback, one warm-up request, and five measured requests on 2026-07-20.

Operation Returned data p50 latency
Health 41-byte response 0.5 ms
Batch quote 2 symbols 44.7 ms
Recent trades 1,800 records 50.2 ms
Full daily history 6,349 bars / about 1.0 MiB 381.5 ms
Full 1-minute history 23,520 bars / about 3.5 MiB 1.406 s

These measurements are examples, not a service-level guarantee. Latency varies with symbol history, response size, upstream server selection, network quality, and market hours.

Run the reproducible benchmark against your local service:

uv run python benchmarks/benchmark_client.py \
  --symbol sh600000 \
  --second-symbol sz000001 \
  --port 8080 \
  --iterations 5

The script reports rows, response size, minimum, p50, p95, and maximum latency as a Markdown table.

Distribution

Channel Purpose
PyPI Primary installation channel
GitHub Releases Release notes plus matching wheel and source archive
GitHub Packages Not used because GitHub does not provide a Python/PyPI package registry

Development

git clone https://github.com/hermanzhaozzzz/pytdxfeed.git
cd pytdxfeed
uv sync --locked --all-groups
uv run ruff check .
uv run ruff format --check .
uv run pytest
uv run python scripts/check_release_audit.py
uv build
uvx --from twine twine check dist/*

Regenerate the deterministic macOS assets when the locked fork commit changes:

uv run python scripts/build_assets.py

The build checks out the exact fork commit into clean build and test clones, runs both Go modules' tests, builds both macOS architectures without CGO, and records source/upstream provenance plus SHA-256 hashes in the asset manifest. Before a release, update its machine-readable upstream audit and run the audit checker with --check-remotes.

License

MIT. See LICENSE.

Star History

Star History Chart

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

pytdxfeed-1.0.3.tar.gz (12.8 MB view details)

Uploaded Source

Built Distribution

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

pytdxfeed-1.0.3-py3-none-any.whl (12.8 MB view details)

Uploaded Python 3

File details

Details for the file pytdxfeed-1.0.3.tar.gz.

File metadata

  • Download URL: pytdxfeed-1.0.3.tar.gz
  • Upload date:
  • Size: 12.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.30 {"installer":{"name":"uv","version":"0.11.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pytdxfeed-1.0.3.tar.gz
Algorithm Hash digest
SHA256 0dbe8c68b00dbf8f20a49fab3392f6950e1d589bbbeb5eb74fe9e06747d0ec1e
MD5 39a97cab5f9083e983db19bdfe99de00
BLAKE2b-256 453bc67fb7414fc9ccb8445f07689227af6907d48d2836c1a6197b69a49134f5

See more details on using hashes here.

File details

Details for the file pytdxfeed-1.0.3-py3-none-any.whl.

File metadata

  • Download URL: pytdxfeed-1.0.3-py3-none-any.whl
  • Upload date:
  • Size: 12.8 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.30 {"installer":{"name":"uv","version":"0.11.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pytdxfeed-1.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 92c6f2b7603cfb9531cbc53ba473c87ddad01fbbe1791a9d3cbb0af46f38df3e
MD5 148ab3d5f9a1816f64be1f8d8eac190e
BLAKE2b-256 343b0d3902f3af994c54f5b51ad8cbd17f6c840775552178393d8d8f06e06b39

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