Skip to main content

tcbs-api

Python client library for the TCBS Open API.

Thin, dependency-light wrapper over the TCBS REST read endpoints: authentication, account information, stock order and trade lookups, holdings, cash, and cash-market data. Responses are parsed into typed pydantic models, so you get autocompletion instead of hand-written dict poking.

The nine endpoints that place, amend or cancel an order — and the three that move cash — are deliberately not wrapped, and neither is any derivative endpoint; see Known limitations.

A few payloads stay partly raw — the profile blocks no real payload pinned down — see Known limitations.

Requirements

Installation

With uv:

uv add tcbs-api

With pip:

pip install tcbs-api

From a source checkout (either tool):

uv add git+https://github.com/nghialuffy/tcbs-api
pip install git+https://github.com/nghialuffy/tcbs-api

For development, install the lint/type-check extras:

uv sync    # or: pip install -e ".[dev]" — both bring in ruff, mypy and pytest

Quick start

from tcbs_api.service import account as account_service, token as token_service

API_KEY = "your-api-key"
OTP = "your-otp"

# 1.1. Exchange the API key for a JWT token.
# NOTE: this endpoint is rate limited to 10 requests/day — persist the token
# and reuse it instead of calling this on every request.
token = token_service.get_token(API_KEY, OTP).token

# 2.1. Account information. The `fields` parameter decides which blocks come back;
#       a block you leave out stays None, and `block:field` narrows one further.
custody_code = "0001201435"
info = account_service.get_subaccount_info(
    custody_code,
    "basicInfo,personalInfo:fullName,bankSubAccounts",
    token,
)
print(info.bankSubAccounts[0].accountNo, info.basicInfo.tcbsId, info.personalInfo.fullName)

Reading holdings and cash

from tcbs_api.service import stock as stock_service

# 4.14. Every stock the sub-account holds, with what is sellable today.
holdings = stock_service.get_asset_stock_by_sub_account("0001F77149", token)
for holding in holdings.stock:
    print(holding.symbol, holding.availableTrading, holding.currentPrice, holding.totalQtty)

# 4.15. The same account's cash: balance, buying power and what is blocked.
cash = stock_service.get_cash_investment("0001F77149", token)
print(cash.data[0].balance, cash.data[0].pp0, cash.data[0].blockAmountInfo.blockAmountTotal)

Reading market data

from tcbs_api.service import market as market_service

# Symbol and price board: a basket, or an explicit list of symbols (the two are
# mutually exclusive, and are keyword-only because `token` comes first here).
board = market_service.get_symbol_and_price(token, index=1)
print(board.tradingDate, board.data[0].matchPrice)

# Supply and demand, filtered by investor class — here `token` stays a positional
# argument, since the symbol is required.
flow = market_service.get_supply_demand_daily("FPT", token, investor_type="shark")
print(flow.data[-1].bsr)

API reference

The public API mirrors the numbering used in the TCBS documentation. Each function carries a docstring naming the operation it implements and linking that page — so help(tcbs_api.service.stock_normal.normal) and IDE hover text both tell you where to look.

Where every argument other than token is an optional filter, token comes first and the filters are keyword-only (None values are dropped from the query); everywhere else token stays the last positional argument.

Module Covers
tcbs_api.service.auth Exchange an API key for a JWT token (1.1)
tcbs_api.service.account Account information (2.1)
tcbs_api.service.stock_normal Stock orders, trades, purchasing power, holdings, cash balance and cash statement (4.x)
tcbs_api.service.market Cash-market price board, foreign room, put-through, intraday and supply-and-demand data (5.x)

Request and response models live under tcbs_api.dto, grouped by the same domains.

The endpoints that write — placing, amending and cancelling orders, and moving cash — are not wrapped, so no function here changes anything at TCBS. get_token is the only call that issues a POST, and it only exchanges the API key for a token. The derivatives endpoints (6.x and 7.1) are not wrapped either.

Error handling

Non-2xx responses call requests.Response.raise_for_status(), so HTTP failures surface as requests.HTTPError:

import requests

from tcbs_api.service import stock as stock_service

try:
    response = stock_service.get_orders(account_no="0001201435", token=token)
except requests.HTTPError as exc:
    print(exc.response.status_code, exc.response.json())

Development

git clone https://github.com/nghialuffy/tcbs-api
cd tcbs-api

uv sync
uv run ruff check .
uv run ruff format --check .
uv run mypy tcbs_api
uv build

The DTOs are checked against live responses by hand: paste a JWT into ACCESS_TOKEN at the top of tests/integration_test.py, then either uv run pytest tests/integration_test.py (it skips while the token is empty) or uv run python tests/integration_test.py. It calls every wrapped endpoint once and reports, per endpoint, anything it could not decode, any key no model declares, and the declared fields the payload left out.

Releasing

Publishing is automated by .github/workflows/publish.yml using PyPI trusted publishing (OIDC), so no API token is stored in this repository.

One-time setup

On PyPI, go to Account → Publishing → Add a pending publisher and fill in:

Field Value
PyPI project name tcbs-api
Owner nghialuffy
Repository name tcbs-api
Workflow name publish.yml
Environment name pypi

The environment name must match the environment: key in the publish job.

Cutting a release

tcbs_api/__init__.py is the single source of truth for the version — pyproject.toml reads it dynamically, so there is nothing to keep in sync.

  1. Bump __version__ in tcbs_api/__init__.py and add a matching CHANGELOG.md entry.
  2. Commit, then tag and push:
    git commit -am "release: vX.Y.Z"
    git tag -a vX.Y.Z -m "vX.Y.Z"
    git push origin main --follow-tags
    
  3. Publish a GitHub Release for that tag. The workflow builds the sdist and wheel, runs twine check --strict, verifies the built version matches the tag, and uploads to PyPI.

Because tcbs-api does not exist on PyPI yet, the first release needs a pending publisher as shown above. Once the project exists, the same entry is managed from the project's own settings instead.

Rehearsing on TestPyPI

Run the same commands against TestPyPI before a real release:

uv build
uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-<testpypi-token>

Notes

  • A published version can never be re-uploaded, so bump __version__ for every attempt — including failed ones. The tag/version guard in the workflow catches the common slip of tagging a version you forgot to bump.
  • Keep the tag (with a v prefix) in sync with __version__ (no v); the workflow strips the prefix before comparing.

Known limitations

The write endpoints are not wrapped

Placing, amending and cancelling an order (4.1–4.3 and 6.4/6.5/6.8/6.9/6.10/6.11) and moving cash between accounts or into and out of margin (3.1–3.3) are not part of this library, so every function it exposes only reads. get_token is the sole POST, and it only exchanges the API key for a token. Call those endpoints directly with requests if you need them.

The derivatives endpoints are not wrapped at all — none of section 6 or 7.1.

Seven further read operations were never wrapped, so they are missing too: 4.10 margin quota, 4.11 risk and margin ratios, 4.12 supplementary loan package, 4.13 loan list, 4.17 debt lookup, 4.18 margin pricing policy, and the REST /api/v1/derivatives/contracts price board. That leaves 19 of the 44 operations covered.

The models follow live responses where the document is wrong

Every DTO field starts from openapi-v1.0.0.json for the operation that returns it, but the document is wrong, thin or silent often enough that fifteen of the nineteen operations were corrected against real payloads — tests/integration_test.py is what keeps this honest:

Op The document says The endpoint actually sends
2.1 basicInfo, bankSubAccounts plus personalInfo (with identityCard), personalBasicInfo, accountStatus, bankAccounts, systemUserInfo, rmRefInfo
4.4 rows under orders rows under data, beside an object/totalCount/pageSize/pageIndex header
4.6 page fields the same, plus object
4.7 purchasingPower, maxQuantity the 16-field ppse object
4.8, 4.9 pp0, maxQtty the same 16-field ppse object, so all three share PurchasingPowerResponse
4.14 assets records of 4 fields stock records of 32 fields
4.15 5 fields the whole 32-field record, with fullName as the string "null"
4.16 query parameter accountno, full page header acctno, and a page of just {"data": []} when nothing matches
5.1 prices and totals always present adds nextCeilPrice/nextFloorPrice/nextRefPrice, omits most fields outside trading hours, so only symbol is required
5.3 13 row fields and a tradingDate 28 keys, no tradingDate, every number quoted, and buyForeignQtty/change/room/bidPrice03 absent on some symbols
5.4 match rows of five fields match rows also carry color
5.5 8 row fields 15 keys, including rcp/pcp, the trading date d and five nulls that stay Any
5.7 ticker, data plus an undocumented d
5.11 no response at all a page of content rows, each with a 36-field securitiesInfo block, and pageable/sort

Where a payload showed a field can be absent, that field is optional rather than required, and fields whose JSON type no live value pinned down stay AnyavatarData, personalBasicInfo, an rmRefInfo item, 5.5's pcps/hc/ac/pcpc and as_, 5.3's sellForeignQtty, 5.4's color and 5.7's d. 5.5's as is a Python keyword, so that field is as_ — see the alias note below. 5.11's models describe the full fields=all projection, so a narrower fields raises a ValidationError naming what is missing.

Two things follow:

  • Unknown keys are ignored, omitted ones default. A field TCBS leaves out falls back to its default, so orders, data, stock, response and friends are None when the API omits them — as long as the model declares them optional.
  • Annotations say what a value means, and pydantic coerces in lax mode. "30500" lands in a float field as 30500.0, which is how 5.3's quoted prices and 5.11's newPrice read as numbers; anything that may be fractional is a float rather than an int for the same reason.

Every field is described

All 437 fields on these models carry a Field(description=...), so model_json_schema() — and any documentation generated from it — explains the payload. The text comes from the OpenAPI document where the document has it; from 5.11's own documentation page, whose response table is hand-written precisely because the document declares none; and from the captured payloads otherwise. A field nothing explains says so instead of guessing: 5.5's rcp/pcp, 5.4's color, 5.7's d, the five Any fields 5.5 sends as null, and the 4.14/4.15 counters whose meaning only TCBS knows.

When a payload does not fit

request_api.decode is a thin model_validate, so a response that does not match its model — a required field missing, a value that cannot be coerced — raises pydantic's ValidationError rather than handing back a half-built object:

from pydantic import ValidationError

from tcbs_api.service import market as market_service

try:
    response = market_service.get_foreign_room(token, index=1)
except ValidationError as exc:
    print(exc.errors())  # [{'loc': ('data', 0, 'matchPrice'), 'msg': 'Input should be a valid number', ...}]

Fields the API sends that the model does not declare are ignored, so a new column never breaks a call; a changed type or a missing required field is worth reporting upstream. To get the payload back out of a model, dump it:

response.model_dump()  # field names
response.model_dump(by_alias=True)  # JSON keys — "as" rather than "as_"

5.5's as key is a Python keyword, so the field is as_ and carries the JSON spelling as a pydantic alias:

from typing import Any

from pydantic import Field

as_: Any = Field(default=None, alias="as")

Every model sets populate_by_name, so row.as_ and Row(as_=…) work as well as the payload's as key, and model_dump(by_alias=True) writes as back out.

Operational notes

  • The token endpoint is limited to 10 requests per day by TCBS; cache the token.
  • The library performs no retries, rate limiting, or token refresh — callers own that policy.
  • Requests are issued synchronously with requests. There is no async client.

License

MIT — see LICENSE.

Release files for tcbs-api 0.0.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tcbs-api 0.0.5
File Size Uploaded
tcbs_api-0.0.5.tar.gz 24.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tcbs-api 0.0.5
File Interpreter ABI Platform
tcbs_api-0.0.5-py3-none-any.whl Python 3 none any Details

Total release size: 55.4 kB

Release files / tcbs_api-0.0.5.tar.gz

Download URL tcbs_api-0.0.5.tar.gz
Size 24.8 kB
Tags Source
SHA-256 checksum
How to use checksums
f9695dc1afcdb65e501a7e2513f089d4798cdf8dd8dd0f9b8fd11fc2b5e5d03c
BLAKE2b-256 checksum
How to use checksums
ea548aed355fe3723b25a60ee3314e78223dd39676497811893af8a8a9d030bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / tcbs_api-0.0.5-py3-none-any.whl

Download URL tcbs_api-0.0.5-py3-none-any.whl
Size 30.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e8e031e8d3ad6f2ff999fec66bf0a3cf6023c570b14436e7dac28c476be762f0
BLAKE2b-256 checksum
How to use checksums
46e4387e1c46e90d39d31f5feb60ce3805bd3227d86cdcc25b260c700645f9ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.17 {"installer":{"name":"uv","version":"0.12.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.0.5 This release

2 release files

0.0.4

2 release files

0.0.3

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page