├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pyproject.toml └── vnpy_rest ├── __init__.py └── rest_client.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.2.0版本 2 | 3 | 1. 替换aiohttp异步通讯模式,改为使用requests多线程模式 4 | 2. 替换使用pyproject.toml配置 5 | 3. ruff和mypy代码质量优化 6 | 7 | # 1.1.1版本 8 | 9 | 1. 调整connector的初始化位置 10 | 11 | # 1.1.0版本 12 | 13 | 1. 关闭底层连接的SSL检查,解决Mac系统报错 14 | 15 | # 1.0.9版本 16 | 17 | 1. 对于同步request函数也支持json参数 18 | 19 | # 1.0.8版本 20 | 21 | 1. 发送请求时,支持传入json参数 22 | 23 | # 1.0.7版本 24 | 25 | 1. 在Windows系统上必须使用Selector事件循环 26 | 2. REST客户端停止时,确保关闭所有会话 27 | 3. 等待异步关闭任务完成后,才停止事件循环 28 | 29 | # 1.0.6版本 30 | 31 | 1. 修复aiohttp的代理参数proxy传空时必须为None的问题 32 | 33 | # 1.0.5版本 34 | 35 | 1. 对Python 3.10后asyncio的修改支持 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015-present, Xiaoyou Chen 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeighNa框架的REST API客户端 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | ## 说明 15 | 16 | 基于requests开发的多线程REST API客户端,用于开发高性能的REST交易接口。 17 | 18 | ## 安装 19 | 20 | 安装环境推荐基于4.0.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com)】。 21 | 22 | 直接使用pip命令: 23 | 24 | ``` 25 | pip install vnpy_rest 26 | ``` 27 | 28 | 下载解压后在cmd中运行 29 | 30 | ``` 31 | pip install . 32 | ``` 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vnpy_rest" 3 | dynamic = ["version"] 4 | description = "REST API client for vn.py quant trading framework." 5 | readme = "README.md" 6 | license = {text = "MIT"} 7 | authors = [{name = "Xiaoyou Chen", email = "xiaoyou.chen@mail.vnpy.com"}] 8 | classifiers = [ 9 | "Development Status :: 5 - Production/Stable", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Programming Language :: Python :: 3.13", 17 | "Programming Language :: Python :: Implementation :: CPython", 18 | "Topic :: Office/Business :: Financial :: Investment", 19 | "Natural Language :: Chinese (Simplified)", 20 | "Typing :: Typed", 21 | ] 22 | requires-python = ">=3.10" 23 | dependencies = [ 24 | "requests>=2.32.3", 25 | ] 26 | 27 | keywords = ["quant", "quantitative", "investment", "trading", "algotrading"] 28 | 29 | [project.urls] 30 | "Homepage" = "https://www.vnpy.com" 31 | "Documentation" = "https://www.vnpy.com/docs" 32 | "Changes" = "https://github.com/vnpy/vnpy_rest/blob/master/CHANGELOG.md" 33 | "Source" = "https://github.com/vnpy/vnpy_rest" 34 | "Forum" = "https://www.vnpy.com/forum" 35 | 36 | [build-system] 37 | requires = ["hatchling>=1.27.0"] 38 | build-backend = "hatchling.build" 39 | 40 | [tool.hatch.version] 41 | path = "vnpy_rest/__init__.py" 42 | pattern = "__version__ = ['\"](?P[^'\"]+)['\"]" 43 | 44 | [tool.hatch.build.targets.wheel] 45 | packages = ["vnpy_rest"] 46 | include-package-data = true 47 | 48 | [tool.hatch.build.targets.sdist] 49 | include = ["vnpy_rest*"] 50 | 51 | [tool.ruff] 52 | target-version = "py310" 53 | output-format = "full" 54 | 55 | [tool.ruff.lint] 56 | select = [ 57 | "B", # flake8-bugbear 58 | "E", # pycodestyle error 59 | "F", # pyflakes 60 | "UP", # pyupgrade 61 | "W", # pycodestyle warning 62 | ] 63 | ignore = ["E501"] 64 | 65 | [tool.mypy] 66 | python_version = "3.10" 67 | warn_return_any = true 68 | warn_unused_configs = true 69 | disallow_untyped_defs = true 70 | disallow_incomplete_defs = true 71 | check_untyped_defs = true 72 | disallow_untyped_decorators = true 73 | no_implicit_optional = true 74 | strict_optional = true 75 | warn_redundant_casts = true 76 | warn_unused_ignores = true 77 | warn_no_return = true -------------------------------------------------------------------------------- /vnpy_rest/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-present, Xiaoyou Chen 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. 22 | 23 | from .rest_client import RestClient, Request, Response 24 | 25 | 26 | __all__ = [ 27 | "RestClient", 28 | "Request", 29 | "Response" 30 | ] 31 | 32 | 33 | __version__ = "1.2.0" 34 | -------------------------------------------------------------------------------- /vnpy_rest/rest_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from datetime import datetime 4 | from multiprocessing.dummy import Pool 5 | from multiprocessing.pool import ThreadPool 6 | from queue import Empty, Queue 7 | from typing import Any 8 | from collections.abc import Callable 9 | from types import TracebackType 10 | 11 | import requests 12 | 13 | 14 | CALLBACK_TYPE = Callable[[dict | None, "Request"], Any] 15 | ON_FAILED_TYPE = Callable[[int, "Request"], Any] 16 | ON_ERROR_TYPE = Callable[[type[BaseException], BaseException, TracebackType, "Request"], Any] 17 | 18 | 19 | Response = requests.Response 20 | 21 | 22 | class Request: 23 | """ 24 | Request object 25 | 26 | method: API request method (GET, POST, PUT, DELETE, QUERY) 27 | path: API request path (without base URL) 28 | callback: Callback function on request success 29 | params: Dictionary of request parameters 30 | data: Request body data, dictionaries will be automatically converted to JSON 31 | headers: Dictionary of request headers 32 | on_failed: Callback function on request failure 33 | on_error: Callback function on request exception 34 | extra: Any additional data (for use in callbacks) 35 | """ 36 | 37 | def __init__( 38 | self, 39 | method: str, 40 | path: str, 41 | params: dict | None, 42 | data: dict | str | None, 43 | headers: dict | None, 44 | callback: CALLBACK_TYPE | None = None, 45 | on_failed: ON_FAILED_TYPE | None = None, 46 | on_error: ON_ERROR_TYPE | None = None, 47 | extra: Any | None = None, 48 | ) -> None: 49 | """Initialize a request object""" 50 | self.method: str = method 51 | self.path: str = path 52 | self.callback: CALLBACK_TYPE | None = callback 53 | self.params: dict | None = params 54 | self.data: dict | str | None = data 55 | self.headers: dict | None = headers 56 | 57 | self.on_failed: ON_FAILED_TYPE | None = on_failed 58 | self.on_error: ON_ERROR_TYPE | None = on_error 59 | self.extra: Any | None = extra 60 | 61 | self.response: requests.Response | None = None 62 | 63 | def __str__(self) -> str: 64 | """String representation of the request""" 65 | if self.response is None: 66 | status_code = "terminated" 67 | else: 68 | status_code = str(self.response.status_code) 69 | 70 | text: str = f"request : {self.method} {self.path} because {status_code}: \n" 71 | text += f"headers: {self.headers}\n" 72 | text += f"params: {self.params}\n" 73 | text += f"data: {self.data!r}\n" 74 | text += f"response: {self.response.text if self.response else ''}\n" 75 | return text 76 | 77 | 78 | class RestClient: 79 | """ 80 | Asynchronous client for various REST APIs 81 | 82 | * Override the sign method to implement request signature logic 83 | * Override the on_failed method to implement standard callback handling for request failures 84 | * Override the on_error method to implement standard callback handling for request exceptions 85 | """ 86 | 87 | def __init__(self) -> None: 88 | """Constructor""" 89 | self.url_base: str = "" 90 | self.active: bool = False 91 | 92 | self.queue: Queue = Queue() 93 | 94 | self.proxies: dict | None = None 95 | 96 | def init( 97 | self, 98 | url_base: str, 99 | proxy_host: str = "", 100 | proxy_port: int = 0 101 | ) -> None: 102 | """ 103 | Initialize the client with the REST API base URL 104 | 105 | :param url_base: Base URL for the REST API 106 | :param proxy_host: Proxy host address 107 | :param proxy_port: Proxy port number 108 | """ 109 | self.url_base = url_base 110 | 111 | if proxy_host and proxy_port: 112 | proxy: str = f"http://{proxy_host}:{proxy_port}" 113 | self.proxies = {"http": proxy, "https": proxy} 114 | 115 | def start(self, n: int = 5) -> None: 116 | """ 117 | Start the client 118 | 119 | :param n: Number of worker threads 120 | """ 121 | if self.active: 122 | return 123 | self.active = True 124 | 125 | self.pool: ThreadPool = Pool(n) 126 | self.pool.apply_async(self.run) 127 | 128 | def stop(self) -> None: 129 | """Stop the client""" 130 | self.active = False 131 | 132 | def join(self) -> None: 133 | """Wait for threads to complete""" 134 | self.queue.join() 135 | 136 | def add_request( 137 | self, 138 | method: str, 139 | path: str, 140 | callback: CALLBACK_TYPE, 141 | params: dict | None = None, 142 | data: dict | str | None = None, 143 | headers: dict | None = None, 144 | on_failed: ON_FAILED_TYPE | None = None, 145 | on_error: ON_ERROR_TYPE | None = None, 146 | extra: Any | None = None, 147 | ) -> Request: 148 | """ 149 | Add a new request task 150 | 151 | :param method: HTTP method 152 | :param path: API endpoint path 153 | :param callback: Callback function for successful responses 154 | :param params: Query parameters 155 | :param data: Request body data 156 | :param headers: HTTP headers 157 | :param on_failed: Callback for failed requests 158 | :param on_error: Callback for request exceptions 159 | :param extra: Additional data to pass to callbacks 160 | :return: Request object 161 | """ 162 | request: Request = Request( 163 | method, 164 | path, 165 | params, 166 | data, 167 | headers, 168 | callback, 169 | on_failed, 170 | on_error, 171 | extra, 172 | ) 173 | self.queue.put(request) 174 | return request 175 | 176 | def run(self) -> None: 177 | """Process tasks in each thread""" 178 | try: 179 | session = requests.session() 180 | while self.active: 181 | try: 182 | request = self.queue.get(timeout=1) 183 | try: 184 | self.process_request(request, session) 185 | finally: 186 | self.queue.task_done() 187 | except Empty: 188 | pass 189 | except Exception: 190 | exc, value, tb = sys.exc_info() 191 | if exc and value and tb: 192 | self.on_error(exc, value, tb, None) 193 | 194 | def sign(self, request: Request) -> Request: 195 | """ 196 | Signature function (override to implement specific signature logic) 197 | 198 | :param request: Request to sign 199 | :return: Signed request 200 | """ 201 | return request 202 | 203 | def on_failed(self, status_code: int, request: Request) -> None: 204 | """ 205 | Default callback for request failures 206 | 207 | :param status_code: HTTP status code 208 | :param request: Failed request 209 | """ 210 | print("RestClient on failed" + "-" * 10) 211 | print(str(request)) 212 | 213 | def on_error( 214 | self, 215 | exc: type[BaseException], 216 | value: BaseException, 217 | tb: TracebackType, 218 | request: Request | None, 219 | ) -> None: 220 | """ 221 | Default callback for request exceptions 222 | 223 | :param exc: Exception class 224 | :param value: Exception instance 225 | :param tb: Traceback object 226 | :param request: Request that caused the exception 227 | """ 228 | try: 229 | print("RestClient on error" + "-" * 10) 230 | print(self.exception_detail(exc, value, tb, request)) 231 | except Exception: 232 | traceback.print_exc() 233 | 234 | def exception_detail( 235 | self, 236 | exc: type[BaseException], 237 | value: BaseException, 238 | tb: TracebackType, 239 | request: Request | None, 240 | ) -> str: 241 | """ 242 | Convert exception information to string 243 | 244 | :param exc: Exception class 245 | :param value: Exception instance 246 | :param tb: Traceback object 247 | :param request: Request that caused the exception 248 | :return: Formatted exception details 249 | """ 250 | text = f"[{datetime.now().isoformat()}]: Unhandled RestClient Error:{exc}\n" 251 | text += f"request:{request}\n" 252 | text += "Exception trace: \n" 253 | text += "".join(traceback.format_exception(exc, value, tb)) 254 | return text 255 | 256 | def process_request(self, request: Request, session: requests.Session) -> None: 257 | """ 258 | Send request to server and process response 259 | 260 | :param request: Request to process 261 | :param session: Requests session 262 | """ 263 | try: 264 | # Sign the request 265 | request = self.sign(request) 266 | 267 | # Send synchronous request 268 | response: Response = session.request( 269 | request.method, 270 | self.make_full_url(request.path), 271 | headers=request.headers, 272 | params=request.params, 273 | data=request.data, 274 | proxies=self.proxies, 275 | ) 276 | 277 | # Bind response to request 278 | request.response = response 279 | 280 | # Parse response data 281 | status_code = response.status_code 282 | 283 | if status_code // 100 == 2: # 2xx indicates success 284 | json_body: dict | None = None 285 | 286 | if status_code != 204: 287 | json_body = response.json() 288 | 289 | if request.callback: 290 | request.callback(json_body, request) 291 | else: 292 | if request.on_failed: 293 | request.on_failed(status_code, request) 294 | else: 295 | self.on_failed(status_code, request) 296 | except Exception: 297 | # Get exception information 298 | exc, value, tb = sys.exc_info() 299 | 300 | # Push exception callback 301 | if exc and value and tb: 302 | if request.on_error: 303 | request.on_error(exc, value, tb, request) 304 | else: 305 | self.on_error(exc, value, tb, request) 306 | 307 | def make_full_url(self, path: str) -> str: 308 | """ 309 | Combine base URL and path to generate full request URL 310 | 311 | :param path: API endpoint path 312 | :return: Complete URL 313 | """ 314 | return self.url_base + path 315 | 316 | def request( 317 | self, 318 | method: str, 319 | path: str, 320 | params: dict | None = None, 321 | data: dict | None = None, 322 | headers: dict | None = None, 323 | ) -> Response: 324 | """ 325 | Make a synchronous request 326 | 327 | :param method: HTTP method 328 | :param path: API endpoint path 329 | :param params: Query parameters 330 | :param data: Request body data 331 | :param headers: HTTP headers 332 | :return: Response object 333 | """ 334 | # Create request object 335 | request: Request = Request( 336 | method, 337 | path, 338 | params, 339 | data, 340 | headers 341 | ) 342 | 343 | # Sign the request 344 | request = self.sign(request) 345 | 346 | # Send synchronous request 347 | response: Response = requests.request( 348 | request.method, 349 | self.make_full_url(request.path), 350 | headers=request.headers, 351 | params=request.params, 352 | data=request.data, 353 | proxies=self.proxies, 354 | ) 355 | return response 356 | --------------------------------------------------------------------------------