tcbs-api 0.0.1__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-0.0.1/.gitignore +51 -0
  2. tcbs_api-0.0.1/CHANGELOG.md +87 -0
  3. tcbs_api-0.0.1/LICENSE +21 -0
  4. tcbs_api-0.0.1/PKG-INFO +274 -0
  5. tcbs_api-0.0.1/README.md +246 -0
  6. tcbs_api-0.0.1/pyproject.toml +64 -0
  7. tcbs_api-0.0.1/tcbs_api/__init__.py +8 -0
  8. tcbs_api-0.0.1/tcbs_api/dto/__init__.py +18 -0
  9. tcbs_api-0.0.1/tcbs_api/dto/account/__init__.py +3 -0
  10. tcbs_api-0.0.1/tcbs_api/dto/account/account_information_response.py +79 -0
  11. tcbs_api-0.0.1/tcbs_api/dto/auth/__init__.py +3 -0
  12. tcbs_api-0.0.1/tcbs_api/dto/auth/token_response_dto.py +10 -0
  13. tcbs_api-0.0.1/tcbs_api/dto/derivative_dto/__init__.py +3 -0
  14. tcbs_api-0.0.1/tcbs_api/dto/derivative_dto/derivative_dto.py +378 -0
  15. tcbs_api-0.0.1/tcbs_api/dto/money/__init__.py +3 -0
  16. tcbs_api-0.0.1/tcbs_api/dto/money/money.py +62 -0
  17. tcbs_api-0.0.1/tcbs_api/dto/stock_normal/__init__.py +3 -0
  18. tcbs_api-0.0.1/tcbs_api/dto/stock_normal/stock_normal_dto.py +263 -0
  19. tcbs_api-0.0.1/tcbs_api/py.typed +0 -0
  20. tcbs_api-0.0.1/tcbs_api/service/__init__.py +14 -0
  21. tcbs_api-0.0.1/tcbs_api/service/account/__init__.py +3 -0
  22. tcbs_api-0.0.1/tcbs_api/service/account/account.py +19 -0
  23. tcbs_api-0.0.1/tcbs_api/service/auth/__init__.py +3 -0
  24. tcbs_api-0.0.1/tcbs_api/service/auth/token.py +18 -0
  25. tcbs_api-0.0.1/tcbs_api/service/derivative/__init__.py +3 -0
  26. tcbs_api-0.0.1/tcbs_api/service/derivative/derivative.py +237 -0
  27. tcbs_api-0.0.1/tcbs_api/service/money/__init__.py +5 -0
  28. tcbs_api-0.0.1/tcbs_api/service/money/money.py +44 -0
  29. tcbs_api-0.0.1/tcbs_api/service/stock_normal/__init__.py +3 -0
  30. tcbs_api-0.0.1/tcbs_api/service/stock_normal/normal.py +132 -0
  31. tcbs_api-0.0.1/tcbs_api/utils/__init__.py +6 -0
  32. tcbs_api-0.0.1/tcbs_api/utils/constant.py +9 -0
  33. tcbs_api-0.0.1/tcbs_api/utils/request_api.py +81 -0
@@ -0,0 +1,51 @@
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
47
+
48
+ # Transaction backups (editor / tooling)
49
+ *.bak
50
+ *.orig
51
+ *.rej
@@ -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/v1.0.0.html).
7
+
8
+ ## [0.0.1] - 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-0.0.1/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,274 @@
1
+ Metadata-Version: 2.5
2
+ Name: tcbs-api
3
+ Version: 0.0.1
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.10
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.10 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.auth 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
+ # 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
+ # 2.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
+ # 3.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 docstring naming the
136
+ operation it implements and linking that page — so `help(tcbs_api.service.stock_normal.normal)`
137
+ and IDE hover text both tell you where to look.
138
+
139
+ | Module | Covers |
140
+ | --- | --- |
141
+ | `tcbs_api.service.auth` | Exchange an API key for a JWT token (1.1) |
142
+ | `tcbs_api.service.account` | Account information (2.1) |
143
+ | `tcbs_api.service.money` | Internal transfers, margin deposit and withdrawal (3.x) |
144
+ | `tcbs_api.service.stock_normal` | Stock order lifecycle, purchasing power, assets (4.x) |
145
+ | `tcbs_api.service.derivative` | Derivatives cash, positions, orders (6.x), market data (7.1) |
146
+
147
+ Request and response models live under `tcbs_api.dto`, grouped by the same domains.
148
+
149
+ ## Error handling
150
+
151
+ Non-2xx responses call `requests.Response.raise_for_status()`, so HTTP failures surface
152
+ as `requests.HTTPError`:
153
+
154
+ ```python
155
+ import requests
156
+
157
+ from tcbs_api.service.stock_normal import normal as stock_service
158
+
159
+ try:
160
+ response = stock_service.get_orders(account_no="0001201435", token=token)
161
+ except requests.HTTPError as exc:
162
+ print(exc.response.status_code, exc.response.json())
163
+ ```
164
+
165
+ ## Development
166
+
167
+ ```bash
168
+ git clone https://github.com/nghialuffy/tcbs-api
169
+ cd tcbs-api
170
+
171
+ uv sync --extra dev
172
+ uv run ruff check .
173
+ uv run ruff format --check .
174
+ uv run mypy tcbs_api
175
+ uv build
176
+ ```
177
+
178
+ ## Releasing
179
+
180
+ Publishing is automated by
181
+ [`.github/workflows/publish.yml`](https://github.com/nghialuffy/tcbs-api/blob/main/.github/workflows/publish.yml)
182
+ using PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so no
183
+ API token is stored in this repository.
184
+
185
+ ### One-time setup
186
+
187
+ On PyPI, go to **Account → Publishing → Add a pending publisher** and fill in:
188
+
189
+ | Field | Value |
190
+ | --- | --- |
191
+ | PyPI project name | `tcbs-api` |
192
+ | Owner | `nghialuffy` |
193
+ | Repository name | `tcbs-api` |
194
+ | Workflow name | `publish.yml` |
195
+ | Environment name | `pypi` |
196
+
197
+ The environment name must match the `environment:` key in the publish job.
198
+
199
+ ### Cutting a release
200
+
201
+ `tcbs_api/__init__.py` is the single source of truth for the version — `pyproject.toml`
202
+ reads it dynamically, so there is nothing to keep in sync.
203
+
204
+ 1. Bump `__version__` in `tcbs_api/__init__.py` and add a matching `CHANGELOG.md` entry.
205
+ 2. Commit, then tag and push:
206
+ ```bash
207
+ git commit -am "release: vX.Y.Z"
208
+ git tag -a vX.Y.Z -m "vX.Y.Z"
209
+ git push origin main --follow-tags
210
+ ```
211
+ 3. Publish a GitHub Release for that tag. The workflow builds the sdist and wheel, runs
212
+ `twine check --strict`, verifies the built version matches the tag, and uploads to
213
+ PyPI.
214
+
215
+ Because `tcbs-api` does not exist on PyPI yet, the first release needs a **pending**
216
+ publisher as shown above. Once the project exists, the same entry is managed from the
217
+ project's own settings instead.
218
+
219
+ ### Rehearsing on TestPyPI
220
+
221
+ Run the same commands against TestPyPI before a real release:
222
+
223
+ ```bash
224
+ uv build
225
+ uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-<testpypi-token>
226
+ ```
227
+
228
+ ### Notes
229
+
230
+ - A published version can **never** be re-uploaded, so bump `__version__` for every
231
+ attempt — including failed ones. The tag/version guard in the workflow catches the
232
+ common slip of tagging a version you forgot to bump.
233
+ - Keep the tag (with a `v` prefix) in sync with `__version__` (no `v`); the workflow
234
+ strips the prefix before comparing.
235
+
236
+ ## Known limitations
237
+
238
+ ### Derivative payloads are not typed
239
+
240
+ The derivative endpoints return a `DerivativeResponse` envelope, but its generic `data`
241
+ field stays a raw `dict`. `dataclasses_json` cannot resolve the `Generic[T]` parameter —
242
+ it warns `Unknown type ~T at DerivativeResponse.data` — so the declared element type is
243
+ never applied:
244
+
245
+ ```python
246
+ from tcbs_api.dto.derivative_dto import TotalCashDerivativeResponse
247
+ from tcbs_api.service.derivative import derivative as derivative_service
248
+
249
+ envelope = derivative_service.get_total_cash_derivative(account_id, sub_account_id, "0", token)
250
+ type(envelope) # <class 'tcbs_api.dto.derivative_dto.derivative_dto.DerivativeResponse'>
251
+ type(envelope.data) # <class 'dict'>
252
+
253
+ # Decode the payload yourself to get the typed object:
254
+ cash = TotalCashDerivativeResponse.from_dict(envelope.data, infer_missing=True)
255
+ ```
256
+
257
+ ### `Optional[...]` fields have no defaults
258
+
259
+ Many DTO fields are annotated `Optional[...]` but are not given a `= None` default, so
260
+ `dataclasses_json`'s `from_dict`/`from_json` raise `KeyError` when a field is missing from
261
+ the payload. Pass `infer_missing=True` (as above) to tolerate missing keys. Decoding in
262
+ the library itself is unaffected, because the non-derivative endpoints use `dacite` with
263
+ `Config(strict=False)`.
264
+
265
+ ### Operational notes
266
+
267
+ - The token endpoint is limited to **10 requests per day** by TCBS; cache the token.
268
+ - The library performs no retries, rate limiting, or token refresh — callers own that
269
+ policy.
270
+ - Requests are issued synchronously with `requests`. There is no async client.
271
+
272
+ ## License
273
+
274
+ MIT — see [LICENSE](https://github.com/nghialuffy/tcbs-api/blob/main/LICENSE).
@@ -0,0 +1,246 @@
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.10 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.auth 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
+ # 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
+ # 2.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
+ # 3.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 docstring naming the
108
+ operation it implements and linking that page — so `help(tcbs_api.service.stock_normal.normal)`
109
+ and IDE hover text both tell you where to look.
110
+
111
+ | Module | Covers |
112
+ | --- | --- |
113
+ | `tcbs_api.service.auth` | Exchange an API key for a JWT token (1.1) |
114
+ | `tcbs_api.service.account` | Account information (2.1) |
115
+ | `tcbs_api.service.money` | Internal transfers, margin deposit and withdrawal (3.x) |
116
+ | `tcbs_api.service.stock_normal` | Stock order lifecycle, purchasing power, assets (4.x) |
117
+ | `tcbs_api.service.derivative` | Derivatives cash, positions, orders (6.x), market data (7.1) |
118
+
119
+ Request and response models live under `tcbs_api.dto`, grouped by the same domains.
120
+
121
+ ## Error handling
122
+
123
+ Non-2xx responses call `requests.Response.raise_for_status()`, so HTTP failures surface
124
+ as `requests.HTTPError`:
125
+
126
+ ```python
127
+ import requests
128
+
129
+ from tcbs_api.service.stock_normal import normal as stock_service
130
+
131
+ try:
132
+ response = stock_service.get_orders(account_no="0001201435", token=token)
133
+ except requests.HTTPError as exc:
134
+ print(exc.response.status_code, exc.response.json())
135
+ ```
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ git clone https://github.com/nghialuffy/tcbs-api
141
+ cd tcbs-api
142
+
143
+ uv sync --extra dev
144
+ uv run ruff check .
145
+ uv run ruff format --check .
146
+ uv run mypy tcbs_api
147
+ uv build
148
+ ```
149
+
150
+ ## Releasing
151
+
152
+ Publishing is automated by
153
+ [`.github/workflows/publish.yml`](https://github.com/nghialuffy/tcbs-api/blob/main/.github/workflows/publish.yml)
154
+ using PyPI [trusted publishing](https://docs.pypi.org/trusted-publishers/) (OIDC), so no
155
+ API token is stored in this repository.
156
+
157
+ ### One-time setup
158
+
159
+ On PyPI, go to **Account → Publishing → Add a pending publisher** and fill in:
160
+
161
+ | Field | Value |
162
+ | --- | --- |
163
+ | PyPI project name | `tcbs-api` |
164
+ | Owner | `nghialuffy` |
165
+ | Repository name | `tcbs-api` |
166
+ | Workflow name | `publish.yml` |
167
+ | Environment name | `pypi` |
168
+
169
+ The environment name must match the `environment:` key in the publish job.
170
+
171
+ ### Cutting a release
172
+
173
+ `tcbs_api/__init__.py` is the single source of truth for the version — `pyproject.toml`
174
+ reads it dynamically, so there is nothing to keep in sync.
175
+
176
+ 1. Bump `__version__` in `tcbs_api/__init__.py` and add a matching `CHANGELOG.md` entry.
177
+ 2. Commit, then tag and push:
178
+ ```bash
179
+ git commit -am "release: vX.Y.Z"
180
+ git tag -a vX.Y.Z -m "vX.Y.Z"
181
+ git push origin main --follow-tags
182
+ ```
183
+ 3. Publish a GitHub Release for that tag. The workflow builds the sdist and wheel, runs
184
+ `twine check --strict`, verifies the built version matches the tag, and uploads to
185
+ PyPI.
186
+
187
+ Because `tcbs-api` does not exist on PyPI yet, the first release needs a **pending**
188
+ publisher as shown above. Once the project exists, the same entry is managed from the
189
+ project's own settings instead.
190
+
191
+ ### Rehearsing on TestPyPI
192
+
193
+ Run the same commands against TestPyPI before a real release:
194
+
195
+ ```bash
196
+ uv build
197
+ uv publish --publish-url https://test.pypi.org/legacy/ --token pypi-<testpypi-token>
198
+ ```
199
+
200
+ ### Notes
201
+
202
+ - A published version can **never** be re-uploaded, so bump `__version__` for every
203
+ attempt — including failed ones. The tag/version guard in the workflow catches the
204
+ common slip of tagging a version you forgot to bump.
205
+ - Keep the tag (with a `v` prefix) in sync with `__version__` (no `v`); the workflow
206
+ strips the prefix before comparing.
207
+
208
+ ## Known limitations
209
+
210
+ ### Derivative payloads are not typed
211
+
212
+ The derivative endpoints return a `DerivativeResponse` envelope, but its generic `data`
213
+ field stays a raw `dict`. `dataclasses_json` cannot resolve the `Generic[T]` parameter —
214
+ it warns `Unknown type ~T at DerivativeResponse.data` — so the declared element type is
215
+ never applied:
216
+
217
+ ```python
218
+ from tcbs_api.dto.derivative_dto import TotalCashDerivativeResponse
219
+ from tcbs_api.service.derivative import derivative as derivative_service
220
+
221
+ envelope = derivative_service.get_total_cash_derivative(account_id, sub_account_id, "0", token)
222
+ type(envelope) # <class 'tcbs_api.dto.derivative_dto.derivative_dto.DerivativeResponse'>
223
+ type(envelope.data) # <class 'dict'>
224
+
225
+ # Decode the payload yourself to get the typed object:
226
+ cash = TotalCashDerivativeResponse.from_dict(envelope.data, infer_missing=True)
227
+ ```
228
+
229
+ ### `Optional[...]` fields have no defaults
230
+
231
+ Many DTO fields are annotated `Optional[...]` but are not given a `= None` default, so
232
+ `dataclasses_json`'s `from_dict`/`from_json` raise `KeyError` when a field is missing from
233
+ the payload. Pass `infer_missing=True` (as above) to tolerate missing keys. Decoding in
234
+ the library itself is unaffected, because the non-derivative endpoints use `dacite` with
235
+ `Config(strict=False)`.
236
+
237
+ ### Operational notes
238
+
239
+ - The token endpoint is limited to **10 requests per day** by TCBS; cache the token.
240
+ - The library performs no retries, rate limiting, or token refresh — callers own that
241
+ policy.
242
+ - Requests are issued synchronously with `requests`. There is no async client.
243
+
244
+ ## License
245
+
246
+ MIT — see [LICENSE](https://github.com/nghialuffy/tcbs-api/blob/main/LICENSE).