tcbs-api 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. tcbs_api-1.0.0/.gitignore +46 -0
  2. tcbs_api-1.0.0/CHANGELOG.md +87 -0
  3. tcbs_api-1.0.0/LICENSE +21 -0
  4. tcbs_api-1.0.0/PKG-INFO +273 -0
  5. tcbs_api-1.0.0/README.md +245 -0
  6. tcbs_api-1.0.0/pyproject.toml +63 -0
  7. tcbs_api-1.0.0/tcbs_api/__init__.py +6 -0
  8. tcbs_api-1.0.0/tcbs_api/dto/__init__.py +0 -0
  9. tcbs_api-1.0.0/tcbs_api/dto/account/__init__.py +1 -0
  10. tcbs_api-1.0.0/tcbs_api/dto/account/account_information_response.py +71 -0
  11. tcbs_api-1.0.0/tcbs_api/dto/authen/__init__.py +1 -0
  12. tcbs_api-1.0.0/tcbs_api/dto/authen/token_response_dto.py +7 -0
  13. tcbs_api-1.0.0/tcbs_api/dto/derivative_dto/__init__.py +1 -0
  14. tcbs_api-1.0.0/tcbs_api/dto/derivative_dto/derivative_dto.py +352 -0
  15. tcbs_api-1.0.0/tcbs_api/dto/money/__init__.py +1 -0
  16. tcbs_api-1.0.0/tcbs_api/dto/money/money.py +55 -0
  17. tcbs_api-1.0.0/tcbs_api/dto/stock_normal/__init__.py +1 -0
  18. tcbs_api-1.0.0/tcbs_api/dto/stock_normal/stock_normal_dto.py +237 -0
  19. tcbs_api-1.0.0/tcbs_api/py.typed +0 -0
  20. tcbs_api-1.0.0/tcbs_api/service/__init__.py +0 -0
  21. tcbs_api-1.0.0/tcbs_api/service/account/__init__.py +1 -0
  22. tcbs_api-1.0.0/tcbs_api/service/account/account.py +18 -0
  23. tcbs_api-1.0.0/tcbs_api/service/authen/__init__.py +1 -0
  24. tcbs_api-1.0.0/tcbs_api/service/authen/token.py +20 -0
  25. tcbs_api-1.0.0/tcbs_api/service/derivative/__init__.py +1 -0
  26. tcbs_api-1.0.0/tcbs_api/service/derivative/derivative.py +253 -0
  27. tcbs_api-1.0.0/tcbs_api/service/money/__init__.py +3 -0
  28. tcbs_api-1.0.0/tcbs_api/service/money/money.py +52 -0
  29. tcbs_api-1.0.0/tcbs_api/service/stock_normal/__init__.py +1 -0
  30. tcbs_api-1.0.0/tcbs_api/service/stock_normal/normal.py +161 -0
  31. tcbs_api-1.0.0/tcbs_api/utils/__init__.py +2 -0
  32. tcbs_api-1.0.0/tcbs_api/utils/constant.py +1 -0
  33. tcbs_api-1.0.0/tcbs_api/utils/request_api.py +8 -0
@@ -0,0 +1,46 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ *.egg
11
+ .eggs/
12
+
13
+ # Installers
14
+ pip-wheel-metadata/
15
+ share/python-wheels/
16
+
17
+ # Unit test / coverage
18
+ .pytest_cache/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ .tox/
23
+ .nox/
24
+ coverage.xml
25
+ *.cover
26
+ .hypothesis/
27
+
28
+ # Type checkers / linters
29
+ .mypy_cache/
30
+ .dmypy.json
31
+ .ruff_cache/
32
+
33
+ # Virtual environments
34
+ .venv/
35
+ venv/
36
+ ENV/
37
+ env/
38
+
39
+ # uv
40
+ # (uv.lock is intentionally committed for reproducible dev environments)
41
+
42
+ # Editors / OS
43
+ .idea/
44
+ .vscode/
45
+ *.swp
46
+ .DS_Store
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-09-19
9
+
10
+ ### Changed
11
+
12
+ - **Repackaged for distribution.** The project is now a standard, PEP 517/518
13
+ installable package that works with `uv`, `pip`, and any other standards-compliant
14
+ installer.
15
+ - **Import package renamed** from the generic top-level `openapi` to `tcbs_api`, so it
16
+ matches the distribution name `tcbs-api` and no longer risks colliding with another
17
+ package on `sys.path`. Every public import changes accordingly, for example
18
+ `import openapi.service.stock_normal.normal` becomes
19
+ `from tcbs_api.service.stock_normal import normal`.
20
+ - Replaced `setup.py` with a declarative `pyproject.toml` (hatchling build backend) and
21
+ a single-sourced `__version__` in `tcbs_api/__init__.py`.
22
+ - Trimmed `install_requires` from a 65-entry `pip freeze` dump down to the three
23
+ packages actually imported: `requests`, `dacite`, and `dataclasses-json`.
24
+ - Moved the commented-out usage demo out of the installed package and into the README.
25
+ - Package metadata now credits **nghialuffy** as the author and points at the GitHub
26
+ repository: `Homepage`, `Repository`, `Issues`, and `Changelog` URLs were added, and the
27
+ TCBS documentation site moved to the `Documentation` URL. The MIT `LICENSE` copyright
28
+ holder was updated to match.
29
+ - The 28 doc-comment links in the service modules were repointed at the current
30
+ `https://developers.tcbs.com.vn/docs/v1.0.0/…` documentation. The Redoc-style
31
+ `#tag/…/operation/…` anchors they pointed at no longer exist.
32
+ - License metadata now uses the PEP 639 SPDX form — `license = "MIT"` plus
33
+ `license-files = ["LICENSE"]`, which publishes `License-Expression: MIT` — and the
34
+ `License :: OSI Approved :: MIT License` classifier was dropped, because PEP 639 forbids
35
+ combining a license classifier with a `License-Expression`.
36
+
37
+ ### Fixed
38
+
39
+ - **`from .money import *` shadowed the money service module.** Because
40
+ `tcbs_api/service/money/money.py` imports the DTO module as `money`, the wildcard
41
+ re-export in its `__init__.py` rebound the `money` attribute to the DTO module. As a
42
+ result both `from tcbs_api.service.money import money` and
43
+ `import tcbs_api.service.money.money as money_service` — the form the old README
44
+ documented — resolved to `tcbs_api.dto.money.money`, so
45
+ `money_service.transfer_between_subaccounts(...)` raised `AttributeError`.
46
+ `tcbs_api/service/money/__init__.py` is the one package that now lists its three
47
+ functions explicitly. Every other package keeps its wildcard re-export, so those
48
+ packages continue to leak their imports (`requests`, `Config`, `constant`, …) into
49
+ their namespaces.
50
+ - **`get_command_match_information` called the wrong endpoint.** It requested
51
+ `/aion/v1/accounts/{accountNo}/orders` — the same URL as `get_orders`, decoding a
52
+ different response DTO. The documented `get order matching information` operation is
53
+ `GET /aion/v1/accounts/{accountNo}/matching-details`, whose response fields
54
+ (`orderId`, `side`, `symbol`, `quoteQtty`, `quotePrice`, `tradeId`, `qtty`, `price`,
55
+ `timeExec`) match `CommandMatchInformationDetailResponse` exactly.
56
+ - **Four DTO classes could not be instantiated.** `OrderConditionDerivativeResponseDTO`,
57
+ `EditOrderNormalDerivativeResponseDTO`, `EditOrderConditionDerivativeResponseDTO`, and
58
+ `CancelOrderNormalDerivativeResponseDTO` had `@dataclass` above `@dataclass_json`
59
+ instead of below it. That order leaves `__init__` set to `object.__init__`, so the
60
+ classes raised `TypeError: takes no arguments` on construction and on `from_json()`.
61
+ The decorator order now matches the other DTOs in the module.
62
+
63
+ ### Removed
64
+
65
+ - Committed build artifacts: `open_api_tcbs.egg-info/`, `PKG-INFO`, `setup.cfg`,
66
+ `SOURCES.txt`, `requires.txt`, and `top_level.txt`.
67
+ - The shadowed duplicate `OrderConditionDerivativeRequestDTO` definition in
68
+ `tcbs_api/dto/derivative_dto/derivative_dto.py` (the first had `str` fields and was
69
+ overwritten by the second, which is the signature the documented call site uses).
70
+ - Unused `Optional`/`field` imports in `tcbs_api/dto/money/money.py`.
71
+ - **All `print()` calls in the service layer.** A stray `print(response)` in
72
+ `get_token()` wrote a raw `requests.Response` to stdout on every call, and 11
73
+ `print(response.json())` calls ran on the error path. The latter executed *before*
74
+ `raise_for_status()`, so a non-JSON error body (a proxy 502, an HTML error page) raised
75
+ `JSONDecodeError` from `json()` and masked the `requests.HTTPError` the README
76
+ documents; they also wrote response payloads to stdout. Errors now surface solely as
77
+ `requests.HTTPError`, whose `response` attribute carries the body.
78
+
79
+ ### Added
80
+
81
+ - `LICENSE` (MIT), this changelog, and a `.gitignore`.
82
+ - `tcbs_api/py.typed` (PEP 561) so type checkers use the inline DTO annotations.
83
+ - `ruff` and `mypy` configuration in `pyproject.toml`.
84
+ - Type annotations across the public service and utility API.
85
+ - `.github/workflows/publish.yml`, which publishes to PyPI via OIDC trusted publishing
86
+ (no API token in the repo), gated on `twine check --strict` and a tag/version match,
87
+ plus a "Releasing" section in the README covering the one-time PyPI setup.
tcbs_api-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 nghialuffy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,273 @@
1
+ Metadata-Version: 2.5
2
+ Name: tcbs-api
3
+ Version: 1.0.0
4
+ Summary: Python client library for the TCBS Open API.
5
+ Project-URL: Homepage, https://github.com/nghialuffy/tcbs-api
6
+ Project-URL: Repository, https://github.com/nghialuffy/tcbs-api
7
+ Project-URL: Issues, https://github.com/nghialuffy/tcbs-api/issues
8
+ Project-URL: Changelog, https://github.com/nghialuffy/tcbs-api/releases
9
+ Project-URL: Documentation, https://developers.tcbs.com.vn/
10
+ Author: nghialuffy
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: derivative,open-api,stock,tcbs,tcbs-api,trading,vietnam
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Topic :: Office/Business :: Financial :: Investment
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.8
21
+ Requires-Dist: dacite>=1.8
22
+ Requires-Dist: dataclasses-json>=0.6
23
+ Requires-Dist: requests>=2.28
24
+ Provides-Extra: dev
25
+ Requires-Dist: mypy>=1.10; extra == 'dev'
26
+ Requires-Dist: ruff>=0.6; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # tcbs-api
30
+
31
+ Python client library for the [TCBS Open API](https://developers.tcbs.com.vn/).
32
+
33
+ Thin, dependency-light wrapper over the TCBS REST endpoints: authentication, account
34
+ information, cash transfers, stock (normal) trading, and derivatives trading. Responses
35
+ are parsed into typed `dataclass` DTOs with `dacite`, so you get autocompletion instead
36
+ of hand-written `dict` poking.
37
+
38
+ The one exception is the derivative endpoints, whose response envelope is typed but whose
39
+ `data` payload is left as a raw `dict` — see [Known limitations](#known-limitations).
40
+
41
+ - Distribution name: `tcbs-api`
42
+ - Import package: `tcbs_api`
43
+ - Source code: <https://github.com/nghialuffy/tcbs-api>
44
+ - Issue tracker: <https://github.com/nghialuffy/tcbs-api/issues>
45
+ - Official API docs: <https://developers.tcbs.com.vn/>
46
+
47
+ ## Requirements
48
+
49
+ - Python 3.8 or newer
50
+ - A TCBS Open API key (obtain it from the [TCBS developer portal](https://developers.tcbs.com.vn/))
51
+
52
+ ## Installation
53
+
54
+ With [uv](https://docs.astral.sh/uv/):
55
+
56
+ ```bash
57
+ uv add tcbs-api
58
+ ```
59
+
60
+ With pip:
61
+
62
+ ```bash
63
+ pip install tcbs-api
64
+ ```
65
+
66
+ From a source checkout (either tool):
67
+
68
+ ```bash
69
+ uv add git+https://github.com/nghialuffy/tcbs-api
70
+ pip install git+https://github.com/nghialuffy/tcbs-api
71
+ ```
72
+
73
+ For development, install the lint/type-check extras:
74
+
75
+ ```bash
76
+ uv sync --extra dev # or: pip install -e ".[dev]"
77
+ ```
78
+
79
+ ## Quick start
80
+
81
+ ```python
82
+ from tcbs_api.dto import money as money_dto
83
+ from tcbs_api.service.account import account as account_service
84
+ from tcbs_api.service.authen import token as token_service
85
+ from tcbs_api.service.money import money as money_service
86
+
87
+ API_KEY = "your-api-key"
88
+ OTP = "your-otp"
89
+
90
+ # 2.1.1. Exchange the API key for a JWT token.
91
+ # NOTE: this endpoint is rate limited to 10 requests/day — persist the token
92
+ # and reuse it instead of calling this on every request.
93
+ token = token_service.get_token(API_KEY, OTP).token
94
+
95
+ # 3.1.1. Account information.
96
+ custody_code = "0001201435"
97
+ info = account_service.get_subaccount_info(
98
+ custody_code,
99
+ "basicInfo,personalInfo,bankSubAccounts,bankAccounts",
100
+ token,
101
+ )
102
+ print(info.personalInfo.fullName)
103
+
104
+ # 4.1.1. Transfer money between sub-accounts.
105
+ request_dto = money_dto.TransferBetweenSubaccountRequestDTO(
106
+ sourceAccountNumber="105C336655A",
107
+ destinationAccountNumber="0001201435",
108
+ amount=10_000,
109
+ description="CHUYEN TIEN PHAI SINH",
110
+ )
111
+ result = money_service.transfer_between_subaccounts(request_dto, token)
112
+ print(result.code, result.message)
113
+ ```
114
+
115
+ ## Placing an order
116
+
117
+ ```python
118
+ from tcbs_api.dto.stock_normal import stock_normal_dto
119
+ from tcbs_api.service.stock_normal import normal as stock_service
120
+
121
+ request_dto = stock_normal_dto.PlaceOrderExternalDto(
122
+ execType="NS",
123
+ price=1000,
124
+ priceType="LO",
125
+ quantity=100,
126
+ symbol="FPT",
127
+ )
128
+ response = stock_service.place_order(request_dto, account_no="0001201435", token=token)
129
+ print(response.orderId)
130
+ ```
131
+
132
+ ## API reference
133
+
134
+ The public API mirrors the numbering used in the TCBS documentation. Every function takes
135
+ the JWT `token` as its last positional argument, and each carries a comment naming the
136
+ section it implements.
137
+
138
+ | Module | Covers |
139
+ | --- | --- |
140
+ | `tcbs_api.service.authen` | Exchange an API key for a JWT token (2.1.1) |
141
+ | `tcbs_api.service.account` | Account information (3.1.1) |
142
+ | `tcbs_api.service.money` | Internal transfers, margin deposit and withdrawal (4.x) |
143
+ | `tcbs_api.service.stock_normal` | Stock order lifecycle, purchasing power, assets (5.x) |
144
+ | `tcbs_api.service.derivative` | Derivatives cash, positions, orders, market data (6.x) |
145
+
146
+ Request and response models live under `tcbs_api.dto`, grouped by the same domains.
147
+
148
+ ## Error handling
149
+
150
+ Non-2xx responses call `requests.Response.raise_for_status()`, so HTTP failures surface
151
+ as `requests.HTTPError`:
152
+
153
+ ```python
154
+ import requests
155
+
156
+ from tcbs_api.service.stock_normal import normal as stock_service
157
+
158
+ try:
159
+ response = stock_service.get_orders(account_no="0001201435", token=token)
160
+ except requests.HTTPError as exc:
161
+ print(exc.response.status_code, exc.response.json())
162
+ ```
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ git clone https://github.com/nghialuffy/tcbs-api
168
+ cd tcbs-api
169
+
170
+ uv sync --extra dev
171
+ uv run ruff check .
172
+ uv run ruff format --check .
173
+ uv run mypy tcbs_api
174
+ uv build
175
+ ```
176
+
177
+ ## Releasing
178
+
179
+ Publishing is automated by
180
+ [`.github/workflows/publish.yml`](https://github.com/nghialuffy/tcbs-api/blob/main/.github/workflows/publish.yml)
181
+ using PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so no
182
+ API token is stored in this repository.
183
+
184
+ ### One-time setup
185
+
186
+ On PyPI, go to **Account → Publishing → Add a pending publisher** and fill in:
187
+
188
+ | Field | Value |
189
+ | --- | --- |
190
+ | PyPI project name | `tcbs-api` |
191
+ | Owner | `nghialuffy` |
192
+ | Repository name | `tcbs-api` |
193
+ | Workflow name | `publish.yml` |
194
+ | Environment name | `pypi` |
195
+
196
+ The environment name must match the `environment:` key in the publish job.
197
+
198
+ ### Cutting a release
199
+
200
+ `tcbs_api/__init__.py` is the single source of truth for the version — `pyproject.toml`
201
+ reads it dynamically, so there is nothing to keep in sync.
202
+
203
+ 1. Bump `__version__` in `tcbs_api/__init__.py` and add a matching `CHANGELOG.md` entry.
204
+ 2. Commit, then tag and push:
205
+ ```bash
206
+ git commit -am "release: vX.Y.Z"
207
+ git tag -a vX.Y.Z -m "vX.Y.Z"
208
+ git push origin main --follow-tags
209
+ ```
210
+ 3. Publish a GitHub Release for that tag. The workflow builds the sdist and wheel, runs
211
+ `twine check --strict`, verifies the built version matches the tag, and uploads to
212
+ PyPI.
213
+
214
+ Because `tcbs-api` does not exist on PyPI yet, the first release needs a **pending**
215
+ publisher as shown above. Once the project exists, the same entry is managed from the
216
+ project's own settings instead.
217
+
218
+ ### Rehearsing on TestPyPI
219
+
220
+ Run the same commands against TestPyPI before a real release:
221
+
222
+ ```bash
223
+ uv build
224
+ uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-<testpypi-token>
225
+ ```
226
+
227
+ ### Notes
228
+
229
+ - A published version can **never** be re-uploaded, so bump `__version__` for every
230
+ attempt — including failed ones. The tag/version guard in the workflow catches the
231
+ common slip of tagging a version you forgot to bump.
232
+ - Keep the tag (with a `v` prefix) in sync with `__version__` (no `v`); the workflow
233
+ strips the prefix before comparing.
234
+
235
+ ## Known limitations
236
+
237
+ ### Derivative payloads are not typed
238
+
239
+ The derivative endpoints return a `DerivativeResponse` envelope, but its generic `data`
240
+ field stays a raw `dict`. `dataclasses_json` cannot resolve the `Generic[T]` parameter —
241
+ it warns `Unknown type ~T at DerivativeResponse.data` — so the declared element type is
242
+ never applied:
243
+
244
+ ```python
245
+ from tcbs_api.dto.derivative_dto import TotalCashDerivativeResponse
246
+ from tcbs_api.service.derivative import derivative as derivative_service
247
+
248
+ envelope = derivative_service.get_total_cash_derivative(account_id, sub_account_id, "0", token)
249
+ type(envelope) # <class 'tcbs_api.dto.derivative_dto.derivative_dto.DerivativeResponse'>
250
+ type(envelope.data) # <class 'dict'>
251
+
252
+ # Decode the payload yourself to get the typed object:
253
+ cash = TotalCashDerivativeResponse.from_dict(envelope.data, infer_missing=True)
254
+ ```
255
+
256
+ ### `Optional[...]` fields have no defaults
257
+
258
+ Many DTO fields are annotated `Optional[...]` but are not given a `= None` default, so
259
+ `dataclasses_json`'s `from_dict`/`from_json` raise `KeyError` when a field is missing from
260
+ the payload. Pass `infer_missing=True` (as above) to tolerate missing keys. Decoding in
261
+ the library itself is unaffected, because the non-derivative endpoints use `dacite` with
262
+ `Config(strict=False)`.
263
+
264
+ ### Operational notes
265
+
266
+ - The token endpoint is limited to **10 requests per day** by TCBS; cache the token.
267
+ - The library performs no retries, rate limiting, or token refresh — callers own that
268
+ policy.
269
+ - Requests are issued synchronously with `requests`. There is no async client.
270
+
271
+ ## License
272
+
273
+ MIT — see [LICENSE](https://github.com/nghialuffy/tcbs-api/blob/main/LICENSE).
@@ -0,0 +1,245 @@
1
+ # tcbs-api
2
+
3
+ Python client library for the [TCBS Open API](https://developers.tcbs.com.vn/).
4
+
5
+ Thin, dependency-light wrapper over the TCBS REST endpoints: authentication, account
6
+ information, cash transfers, stock (normal) trading, and derivatives trading. Responses
7
+ are parsed into typed `dataclass` DTOs with `dacite`, so you get autocompletion instead
8
+ of hand-written `dict` poking.
9
+
10
+ The one exception is the derivative endpoints, whose response envelope is typed but whose
11
+ `data` payload is left as a raw `dict` — see [Known limitations](#known-limitations).
12
+
13
+ - Distribution name: `tcbs-api`
14
+ - Import package: `tcbs_api`
15
+ - Source code: <https://github.com/nghialuffy/tcbs-api>
16
+ - Issue tracker: <https://github.com/nghialuffy/tcbs-api/issues>
17
+ - Official API docs: <https://developers.tcbs.com.vn/>
18
+
19
+ ## Requirements
20
+
21
+ - Python 3.8 or newer
22
+ - A TCBS Open API key (obtain it from the [TCBS developer portal](https://developers.tcbs.com.vn/))
23
+
24
+ ## Installation
25
+
26
+ With [uv](https://docs.astral.sh/uv/):
27
+
28
+ ```bash
29
+ uv add tcbs-api
30
+ ```
31
+
32
+ With pip:
33
+
34
+ ```bash
35
+ pip install tcbs-api
36
+ ```
37
+
38
+ From a source checkout (either tool):
39
+
40
+ ```bash
41
+ uv add git+https://github.com/nghialuffy/tcbs-api
42
+ pip install git+https://github.com/nghialuffy/tcbs-api
43
+ ```
44
+
45
+ For development, install the lint/type-check extras:
46
+
47
+ ```bash
48
+ uv sync --extra dev # or: pip install -e ".[dev]"
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ```python
54
+ from tcbs_api.dto import money as money_dto
55
+ from tcbs_api.service.account import account as account_service
56
+ from tcbs_api.service.authen import token as token_service
57
+ from tcbs_api.service.money import money as money_service
58
+
59
+ API_KEY = "your-api-key"
60
+ OTP = "your-otp"
61
+
62
+ # 2.1.1. Exchange the API key for a JWT token.
63
+ # NOTE: this endpoint is rate limited to 10 requests/day — persist the token
64
+ # and reuse it instead of calling this on every request.
65
+ token = token_service.get_token(API_KEY, OTP).token
66
+
67
+ # 3.1.1. Account information.
68
+ custody_code = "0001201435"
69
+ info = account_service.get_subaccount_info(
70
+ custody_code,
71
+ "basicInfo,personalInfo,bankSubAccounts,bankAccounts",
72
+ token,
73
+ )
74
+ print(info.personalInfo.fullName)
75
+
76
+ # 4.1.1. Transfer money between sub-accounts.
77
+ request_dto = money_dto.TransferBetweenSubaccountRequestDTO(
78
+ sourceAccountNumber="105C336655A",
79
+ destinationAccountNumber="0001201435",
80
+ amount=10_000,
81
+ description="CHUYEN TIEN PHAI SINH",
82
+ )
83
+ result = money_service.transfer_between_subaccounts(request_dto, token)
84
+ print(result.code, result.message)
85
+ ```
86
+
87
+ ## Placing an order
88
+
89
+ ```python
90
+ from tcbs_api.dto.stock_normal import stock_normal_dto
91
+ from tcbs_api.service.stock_normal import normal as stock_service
92
+
93
+ request_dto = stock_normal_dto.PlaceOrderExternalDto(
94
+ execType="NS",
95
+ price=1000,
96
+ priceType="LO",
97
+ quantity=100,
98
+ symbol="FPT",
99
+ )
100
+ response = stock_service.place_order(request_dto, account_no="0001201435", token=token)
101
+ print(response.orderId)
102
+ ```
103
+
104
+ ## API reference
105
+
106
+ The public API mirrors the numbering used in the TCBS documentation. Every function takes
107
+ the JWT `token` as its last positional argument, and each carries a comment naming the
108
+ section it implements.
109
+
110
+ | Module | Covers |
111
+ | --- | --- |
112
+ | `tcbs_api.service.authen` | Exchange an API key for a JWT token (2.1.1) |
113
+ | `tcbs_api.service.account` | Account information (3.1.1) |
114
+ | `tcbs_api.service.money` | Internal transfers, margin deposit and withdrawal (4.x) |
115
+ | `tcbs_api.service.stock_normal` | Stock order lifecycle, purchasing power, assets (5.x) |
116
+ | `tcbs_api.service.derivative` | Derivatives cash, positions, orders, market data (6.x) |
117
+
118
+ Request and response models live under `tcbs_api.dto`, grouped by the same domains.
119
+
120
+ ## Error handling
121
+
122
+ Non-2xx responses call `requests.Response.raise_for_status()`, so HTTP failures surface
123
+ as `requests.HTTPError`:
124
+
125
+ ```python
126
+ import requests
127
+
128
+ from tcbs_api.service.stock_normal import normal as stock_service
129
+
130
+ try:
131
+ response = stock_service.get_orders(account_no="0001201435", token=token)
132
+ except requests.HTTPError as exc:
133
+ print(exc.response.status_code, exc.response.json())
134
+ ```
135
+
136
+ ## Development
137
+
138
+ ```bash
139
+ git clone https://github.com/nghialuffy/tcbs-api
140
+ cd tcbs-api
141
+
142
+ uv sync --extra dev
143
+ uv run ruff check .
144
+ uv run ruff format --check .
145
+ uv run mypy tcbs_api
146
+ uv build
147
+ ```
148
+
149
+ ## Releasing
150
+
151
+ Publishing is automated by
152
+ [`.github/workflows/publish.yml`](https://github.com/nghialuffy/tcbs-api/blob/main/.github/workflows/publish.yml)
153
+ using PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so no
154
+ API token is stored in this repository.
155
+
156
+ ### One-time setup
157
+
158
+ On PyPI, go to **Account → Publishing → Add a pending publisher** and fill in:
159
+
160
+ | Field | Value |
161
+ | --- | --- |
162
+ | PyPI project name | `tcbs-api` |
163
+ | Owner | `nghialuffy` |
164
+ | Repository name | `tcbs-api` |
165
+ | Workflow name | `publish.yml` |
166
+ | Environment name | `pypi` |
167
+
168
+ The environment name must match the `environment:` key in the publish job.
169
+
170
+ ### Cutting a release
171
+
172
+ `tcbs_api/__init__.py` is the single source of truth for the version — `pyproject.toml`
173
+ reads it dynamically, so there is nothing to keep in sync.
174
+
175
+ 1. Bump `__version__` in `tcbs_api/__init__.py` and add a matching `CHANGELOG.md` entry.
176
+ 2. Commit, then tag and push:
177
+ ```bash
178
+ git commit -am "release: vX.Y.Z"
179
+ git tag -a vX.Y.Z -m "vX.Y.Z"
180
+ git push origin main --follow-tags
181
+ ```
182
+ 3. Publish a GitHub Release for that tag. The workflow builds the sdist and wheel, runs
183
+ `twine check --strict`, verifies the built version matches the tag, and uploads to
184
+ PyPI.
185
+
186
+ Because `tcbs-api` does not exist on PyPI yet, the first release needs a **pending**
187
+ publisher as shown above. Once the project exists, the same entry is managed from the
188
+ project's own settings instead.
189
+
190
+ ### Rehearsing on TestPyPI
191
+
192
+ Run the same commands against TestPyPI before a real release:
193
+
194
+ ```bash
195
+ uv build
196
+ uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-<testpypi-token>
197
+ ```
198
+
199
+ ### Notes
200
+
201
+ - A published version can **never** be re-uploaded, so bump `__version__` for every
202
+ attempt — including failed ones. The tag/version guard in the workflow catches the
203
+ common slip of tagging a version you forgot to bump.
204
+ - Keep the tag (with a `v` prefix) in sync with `__version__` (no `v`); the workflow
205
+ strips the prefix before comparing.
206
+
207
+ ## Known limitations
208
+
209
+ ### Derivative payloads are not typed
210
+
211
+ The derivative endpoints return a `DerivativeResponse` envelope, but its generic `data`
212
+ field stays a raw `dict`. `dataclasses_json` cannot resolve the `Generic[T]` parameter —
213
+ it warns `Unknown type ~T at DerivativeResponse.data` — so the declared element type is
214
+ never applied:
215
+
216
+ ```python
217
+ from tcbs_api.dto.derivative_dto import TotalCashDerivativeResponse
218
+ from tcbs_api.service.derivative import derivative as derivative_service
219
+
220
+ envelope = derivative_service.get_total_cash_derivative(account_id, sub_account_id, "0", token)
221
+ type(envelope) # <class 'tcbs_api.dto.derivative_dto.derivative_dto.DerivativeResponse'>
222
+ type(envelope.data) # <class 'dict'>
223
+
224
+ # Decode the payload yourself to get the typed object:
225
+ cash = TotalCashDerivativeResponse.from_dict(envelope.data, infer_missing=True)
226
+ ```
227
+
228
+ ### `Optional[...]` fields have no defaults
229
+
230
+ Many DTO fields are annotated `Optional[...]` but are not given a `= None` default, so
231
+ `dataclasses_json`'s `from_dict`/`from_json` raise `KeyError` when a field is missing from
232
+ the payload. Pass `infer_missing=True` (as above) to tolerate missing keys. Decoding in
233
+ the library itself is unaffected, because the non-derivative endpoints use `dacite` with
234
+ `Config(strict=False)`.
235
+
236
+ ### Operational notes
237
+
238
+ - The token endpoint is limited to **10 requests per day** by TCBS; cache the token.
239
+ - The library performs no retries, rate limiting, or token refresh — callers own that
240
+ policy.
241
+ - Requests are issued synchronously with `requests`. There is no async client.
242
+
243
+ ## License
244
+
245
+ MIT — see [LICENSE](https://github.com/nghialuffy/tcbs-api/blob/main/LICENSE).