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.
- tcbs_api-1.0.0/.gitignore +46 -0
- tcbs_api-1.0.0/CHANGELOG.md +87 -0
- tcbs_api-1.0.0/LICENSE +21 -0
- tcbs_api-1.0.0/PKG-INFO +273 -0
- tcbs_api-1.0.0/README.md +245 -0
- tcbs_api-1.0.0/pyproject.toml +63 -0
- tcbs_api-1.0.0/tcbs_api/__init__.py +6 -0
- tcbs_api-1.0.0/tcbs_api/dto/__init__.py +0 -0
- tcbs_api-1.0.0/tcbs_api/dto/account/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/dto/account/account_information_response.py +71 -0
- tcbs_api-1.0.0/tcbs_api/dto/authen/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/dto/authen/token_response_dto.py +7 -0
- tcbs_api-1.0.0/tcbs_api/dto/derivative_dto/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/dto/derivative_dto/derivative_dto.py +352 -0
- tcbs_api-1.0.0/tcbs_api/dto/money/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/dto/money/money.py +55 -0
- tcbs_api-1.0.0/tcbs_api/dto/stock_normal/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/dto/stock_normal/stock_normal_dto.py +237 -0
- tcbs_api-1.0.0/tcbs_api/py.typed +0 -0
- tcbs_api-1.0.0/tcbs_api/service/__init__.py +0 -0
- tcbs_api-1.0.0/tcbs_api/service/account/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/service/account/account.py +18 -0
- tcbs_api-1.0.0/tcbs_api/service/authen/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/service/authen/token.py +20 -0
- tcbs_api-1.0.0/tcbs_api/service/derivative/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/service/derivative/derivative.py +253 -0
- tcbs_api-1.0.0/tcbs_api/service/money/__init__.py +3 -0
- tcbs_api-1.0.0/tcbs_api/service/money/money.py +52 -0
- tcbs_api-1.0.0/tcbs_api/service/stock_normal/__init__.py +1 -0
- tcbs_api-1.0.0/tcbs_api/service/stock_normal/normal.py +161 -0
- tcbs_api-1.0.0/tcbs_api/utils/__init__.py +2 -0
- tcbs_api-1.0.0/tcbs_api/utils/constant.py +1 -0
- 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.
|
tcbs_api-1.0.0/PKG-INFO
ADDED
|
@@ -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).
|
tcbs_api-1.0.0/README.md
ADDED
|
@@ -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).
|