├── .flake8 ├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md └── workflows │ └── pythonapp.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── pyproject.toml └── vnpy_mysql ├── __init__.py └── mysql_database.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv,build,__pycache__,__init__.py,ib,talib,uic 3 | ignore = 4 | E501 line too long, fixed by black 5 | W503 line break before binary operator 6 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 行为准则 2 | 3 | 这是一份VeighNa项目社区的行为准则,也是项目作者自己在刚入行量化金融行业时对于理想中的社区的期望: 4 | 5 | * 为交易员而生:作为一款从金融机构量化业务中诞生的交易系统开发框架,设计上都优先满足机构专业交易员的使用习惯,而不是其他用户(散户、爱好者、技术人员等) 6 | 7 | * 对新用户友好,保持耐心:大部分人在接触新东西的时候都是磕磕碰碰、有很多的问题,请记住此时别人对你伸出的援助之手,并把它传递给未来需要的人 8 | 9 | * 尊重他人,慎重言行:礼貌文明的交流方式除了能得到别人同样的回应,更能减少不必要的摩擦,保证高效的交流 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 环境 2 | 3 | * 操作系统: 如Windows 11或者Ubuntu 22.04 4 | * Python版本: 如VeighNa Studio-4.0.0 5 | * VeighNa版本: 如v4.0.0发行版或者dev branch 20250320(下载日期) 6 | 7 | ## Issue类型 8 | 三选一:Bug/Enhancement/Question 9 | 10 | ## 预期程序行为 11 | 12 | 13 | ## 实际程序行为 14 | 15 | 16 | ## 重现步骤 17 | 18 | 针对Bug类型Issue,请提供具体重现步骤以及报错截图 19 | 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 建议每次发起的PR内容尽可能精简,复杂的修改请拆分为多次PR,便于管理合并。 2 | 3 | ## 改进内容 4 | 5 | 1. 6 | 2. 7 | 3. 8 | 9 | ## 相关的Issue号(如有) 10 | 11 | Close # -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # 获取帮助 2 | 3 | 在开发和使用VeighNa项目的过程中遇到问题时,获取帮助的渠道包括: 4 | 5 | * Github Issues:[Issues页面](https://github.com/vnpy/vnpy/issues) 6 | * 官方QQ群: 262656087 7 | * 项目论坛:[VeighNa量化社区](http://www.vnpy.com/forum) 8 | * 项目邮箱: vn.py@foxmail.com 9 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.13 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.13' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install ta-lib==0.6.3 --index=https://pypi.vnpy.com 20 | pip install vnpy ruff mypy uv 21 | - name: Lint with ruff 22 | run: | 23 | # Run ruff linter based on pyproject.toml configuration 24 | ruff check . 25 | - name: Type check with mypy 26 | run: | 27 | # Run mypy type checking based on pyproject.toml configuration 28 | mypy vnpy_mysql 29 | - name: Build packages with uv 30 | run: | 31 | # Build source distribution and wheel distribution 32 | uv build 33 | -------------------------------------------------------------------------------- /.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.1.0版本 2 | 3 | 1. vnpy框架4.0版本升级适配 4 | 5 | # 1.0.5版本 6 | 7 | 1. 兼容无数据库写入权限情况下的数据表初始化 8 | 9 | # 1.0.4版本 10 | 11 | 1. 添加数据库连接的自动重连功能 12 | 2. 修复由于Float类型溢出导致的数据异常问题 13 | 14 | # 1.0.3版本 15 | 16 | 1. 修复加载数据时的时段重复问题 17 | 18 | # 1.0.2版本 19 | 20 | 1. 增加Tick数据汇总支持 21 | 2. 数据时间戳字段支持毫秒 22 | 3. 增加写入数据时的流式参数支持 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeighNa框架的MySQL数据库接口 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 |

12 | 13 | ## 说明 14 | 15 | 基于peewee开发的MySQL数据库接口。 16 | 17 | ## 使用 18 | 19 | ### 全局配置 20 | 21 | 在VeighNa中使用MySQL时,需要在全局配置中填写以下字段信息: 22 | 23 | |名称|含义|必填|举例| 24 | |---------|----|---|---| 25 | |database.name|名称|是|mysql| 26 | |database.host|地址|是|localhost| 27 | |database.port|端口|是|3306| 28 | |database.database|实例|是|vnpy| 29 | |database.user|用户名|是|root| 30 | |database.password|密码|是|123456| 31 | 32 | ### 创建实例(Schema) 33 | 34 | VeighNa不会主动为MySQL数据库创建实例,所以使用前请确保database.database字段中填写的的数据库实例已经创建了。 35 | 36 | 若实例尚未创建,可以使用【MySQL Workbench】客户端的【new_schema】进行操作。 37 | 38 | 39 | ### 字符串大小写敏感支持 40 | 41 | 由于peewee的建表功能限制,默认情况下在保存合约代码的【symbol】字段时,无法区分字符串大小写。如果影响使用,可按照以下方式手动修改MySQL数据表来解决: 42 | 43 | ``` 44 | # 用MySQL命令行工具连接数据库 45 | 46 | # 选择数据实例 47 | use vnpy; 48 | 49 | # 修改四张表symbol字段的BINARY属性 50 | ALTER TABLE `dbbaroverview` MODIFY COLUMN `symbol` VARCHAR(45) BINARY; 51 | 52 | ALTER TABLE `dbtickoverview` MODIFY COLUMN `symbol` VARCHAR(45) BINARY; 53 | 54 | ALTER TABLE `dbbardata` MODIFY COLUMN `symbol` VARCHAR(45) BINARY; 55 | 56 | ALTER TABLE `dbtickdata` MODIFY COLUMN `symbol` VARCHAR(45) BINARY; 57 | ``` 58 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vnpy_mysql" 3 | dynamic = ["version"] 4 | description = "MySQL database adapter for VeighNa 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 | "Topic :: Office/Business :: Financial :: Investment", 18 | "Programming Language :: Python :: Implementation :: CPython", 19 | "Natural Language :: Chinese (Simplified)", 20 | "Typing :: Typed" 21 | ] 22 | requires-python = ">=3.10" 23 | dependencies = [ 24 | "peewee>=3.17.9", 25 | "cryptography>=3.17.9", 26 | "pymysql>=1.1.1" 27 | ] 28 | keywords = ["quant", "quantitative", "investment", "trading", "algotrading"] 29 | 30 | [project.urls] 31 | "Homepage" = "https://www.vnpy.com" 32 | "Documentation" = "https://www.vnpy.com/docs" 33 | "Changes" = "https://github.com/vnpy/vnpy_mysql/blob/master/CHANGELOG.md" 34 | "Source" = "https://github.com/vnpy/vnpy_mysql/" 35 | "Forum" = "https://www.vnpy.com/forum" 36 | 37 | [build-system] 38 | requires = ["hatchling>=1.27.0"] 39 | build-backend = "hatchling.build" 40 | 41 | [tool.hatch.version] 42 | path = "vnpy_mysql/__init__.py" 43 | pattern = "__version__ = ['\"](?P[^'\"]+)['\"]" 44 | 45 | [tool.hatch.build.targets.wheel] 46 | packages = ["vnpy_mysql"] 47 | include-package-data = true 48 | 49 | [tool.hatch.build.targets.sdist] 50 | include = ["vnpy_mysql*"] 51 | 52 | [tool.ruff] 53 | target-version = "py310" 54 | output-format = "full" 55 | 56 | [tool.ruff.lint] 57 | select = [ 58 | "B", # flake8-bugbear 59 | "E", # pycodestyle error 60 | "F", # pyflakes 61 | "UP", # pyupgrade 62 | "W", # pycodestyle warning 63 | ] 64 | ignore = ["E501"] 65 | 66 | [tool.mypy] 67 | python_version = "3.10" 68 | warn_return_any = true 69 | warn_unused_configs = true 70 | disallow_untyped_defs = true 71 | disallow_incomplete_defs = true 72 | check_untyped_defs = true 73 | disallow_untyped_decorators = true 74 | no_implicit_optional = true 75 | strict_optional = true 76 | warn_redundant_casts = true 77 | warn_unused_ignores = true 78 | warn_no_return = true 79 | ignore_missing_imports = true 80 | -------------------------------------------------------------------------------- /vnpy_mysql/__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 | 24 | from .mysql_database import MysqlDatabase as Database 25 | 26 | 27 | __all__ = ["Database"] 28 | 29 | 30 | __version__ = "1.1.0" 31 | -------------------------------------------------------------------------------- /vnpy_mysql/mysql_database.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from peewee import ( 4 | AutoField, 5 | CharField, 6 | DateTimeField, 7 | DoubleField, 8 | IntegerField, 9 | Model, 10 | MySQLDatabase as PeeweeMySQLDatabase, 11 | ModelSelect, 12 | ModelDelete, 13 | chunked, 14 | fn, 15 | Asc, 16 | Desc 17 | ) 18 | from playhouse.shortcuts import ReconnectMixin 19 | 20 | from vnpy.trader.constant import Exchange, Interval 21 | from vnpy.trader.object import BarData, TickData 22 | from vnpy.trader.database import ( 23 | BaseDatabase, 24 | BarOverview, 25 | TickOverview, 26 | DB_TZ, 27 | convert_tz 28 | ) 29 | from vnpy.trader.setting import SETTINGS 30 | 31 | 32 | class ReconnectMySQLDatabase(ReconnectMixin, PeeweeMySQLDatabase): 33 | """带有重连混入的MySQL数据库类""" 34 | pass 35 | 36 | db = ReconnectMySQLDatabase( 37 | database=SETTINGS["database.database"], 38 | user=SETTINGS["database.user"], 39 | password=SETTINGS["database.password"], 40 | host=SETTINGS["database.host"], 41 | port=SETTINGS["database.port"] 42 | ) 43 | 44 | 45 | class DateTimeMillisecondField(DateTimeField): 46 | """支持毫秒的日期时间戳字段""" 47 | 48 | def get_modifiers(self) -> list: 49 | """毫秒支持""" 50 | return [3] 51 | 52 | 53 | class DbBarData(Model): 54 | """K线数据表映射对象""" 55 | 56 | id: AutoField = AutoField() 57 | 58 | symbol: CharField = CharField() 59 | exchange: CharField = CharField() 60 | datetime: DateTimeField = DateTimeField() 61 | interval: CharField = CharField() 62 | 63 | volume: DoubleField = DoubleField() 64 | turnover: DoubleField = DoubleField() 65 | open_interest: DoubleField = DoubleField() 66 | open_price: DoubleField = DoubleField() 67 | high_price: DoubleField = DoubleField() 68 | low_price: DoubleField = DoubleField() 69 | close_price: DoubleField = DoubleField() 70 | 71 | class Meta: 72 | database: PeeweeMySQLDatabase = db 73 | indexes: tuple = ((("symbol", "exchange", "interval", "datetime"), True),) 74 | 75 | 76 | class DbTickData(Model): 77 | """TICK数据表映射对象""" 78 | 79 | id: AutoField = AutoField() 80 | 81 | symbol: CharField = CharField() 82 | exchange: CharField = CharField() 83 | datetime: DateTimeField = DateTimeMillisecondField() 84 | 85 | name: CharField = CharField() 86 | volume: DoubleField = DoubleField() 87 | turnover: DoubleField = DoubleField() 88 | open_interest: DoubleField = DoubleField() 89 | last_price: DoubleField = DoubleField() 90 | last_volume: DoubleField = DoubleField() 91 | limit_up: DoubleField = DoubleField() 92 | limit_down: DoubleField = DoubleField() 93 | 94 | open_price: DoubleField = DoubleField() 95 | high_price: DoubleField = DoubleField() 96 | low_price: DoubleField = DoubleField() 97 | pre_close: DoubleField = DoubleField() 98 | 99 | bid_price_1: DoubleField = DoubleField() 100 | bid_price_2: DoubleField = DoubleField(null=True) 101 | bid_price_3: DoubleField = DoubleField(null=True) 102 | bid_price_4: DoubleField = DoubleField(null=True) 103 | bid_price_5: DoubleField = DoubleField(null=True) 104 | 105 | ask_price_1: DoubleField = DoubleField() 106 | ask_price_2: DoubleField = DoubleField(null=True) 107 | ask_price_3: DoubleField = DoubleField(null=True) 108 | ask_price_4: DoubleField = DoubleField(null=True) 109 | ask_price_5: DoubleField = DoubleField(null=True) 110 | 111 | bid_volume_1: DoubleField = DoubleField() 112 | bid_volume_2: DoubleField = DoubleField(null=True) 113 | bid_volume_3: DoubleField = DoubleField(null=True) 114 | bid_volume_4: DoubleField = DoubleField(null=True) 115 | bid_volume_5: DoubleField = DoubleField(null=True) 116 | 117 | ask_volume_1: DoubleField = DoubleField() 118 | ask_volume_2: DoubleField = DoubleField(null=True) 119 | ask_volume_3: DoubleField = DoubleField(null=True) 120 | ask_volume_4: DoubleField = DoubleField(null=True) 121 | ask_volume_5: DoubleField = DoubleField(null=True) 122 | 123 | localtime: DateTimeField = DateTimeMillisecondField(null=True) 124 | 125 | class Meta: 126 | database: PeeweeMySQLDatabase = db 127 | indexes: tuple = ((("symbol", "exchange", "datetime"), True),) 128 | 129 | 130 | class DbBarOverview(Model): 131 | """K线汇总数据表映射对象""" 132 | 133 | id: AutoField = AutoField() 134 | 135 | symbol: CharField = CharField() 136 | exchange: CharField = CharField() 137 | interval: CharField = CharField() 138 | count: IntegerField = IntegerField() 139 | start: DateTimeField = DateTimeField() 140 | end: DateTimeField = DateTimeField() 141 | 142 | class Meta: 143 | database: PeeweeMySQLDatabase = db 144 | indexes: tuple = ((("symbol", "exchange", "interval"), True),) 145 | 146 | 147 | class DbTickOverview(Model): 148 | """Tick汇总数据表映射对象""" 149 | 150 | id: AutoField = AutoField() 151 | 152 | symbol: CharField = CharField() 153 | exchange: CharField = CharField() 154 | count: IntegerField = IntegerField() 155 | start: DateTimeField = DateTimeField() 156 | end: DateTimeField = DateTimeField() 157 | 158 | class Meta: 159 | database: PeeweeMySQLDatabase = db 160 | indexes: tuple = ((("symbol", "exchange"), True),) 161 | 162 | 163 | class MysqlDatabase(BaseDatabase): 164 | """Mysql数据库接口""" 165 | 166 | def __init__(self) -> None: 167 | """""" 168 | self.db: PeeweeMySQLDatabase = db 169 | self.db.connect() 170 | 171 | # 如果数据表不存在,则执行创建初始化 172 | if not DbBarData.table_exists(): 173 | self.db.create_tables([DbBarData, DbTickData, DbBarOverview, DbTickOverview]) 174 | 175 | def save_bar_data(self, bars: list[BarData], stream: bool = False) -> bool: 176 | """保存K线数据""" 177 | # 读取主键参数 178 | bar: BarData = bars[0] 179 | symbol: str = bar.symbol 180 | exchange: Exchange = bar.exchange 181 | interval: Interval = bar.interval 182 | 183 | # 将BarData数据转换为字典,并调整时区 184 | data: list = [] 185 | 186 | for bar in bars: 187 | bar.datetime = convert_tz(bar.datetime) 188 | 189 | d: dict = bar.__dict__ 190 | d["exchange"] = d["exchange"].value 191 | d["interval"] = d["interval"].value 192 | d.pop("gateway_name") 193 | d.pop("vt_symbol") 194 | d.pop("extra") 195 | data.append(d) 196 | 197 | # 使用upsert操作将数据更新到数据库中 198 | with self.db.atomic(): 199 | for c in chunked(data, 50): 200 | DbBarData.insert_many(c).on_conflict_replace().execute() 201 | 202 | # 更新K线汇总数据 203 | overview: DbBarOverview = DbBarOverview.get_or_none( 204 | DbBarOverview.symbol == symbol, 205 | DbBarOverview.exchange == exchange.value, 206 | DbBarOverview.interval == interval.value, 207 | ) 208 | 209 | if not overview: 210 | overview = DbBarOverview() 211 | overview.symbol = symbol 212 | overview.exchange = exchange.value 213 | overview.interval = interval.value 214 | overview.start = bars[0].datetime 215 | overview.end = bars[-1].datetime 216 | overview.count = len(bars) 217 | elif stream: 218 | overview.end = bars[-1].datetime 219 | overview.count += len(bars) 220 | else: 221 | overview.start = min(bars[0].datetime, overview.start) 222 | overview.end = max(bars[-1].datetime, overview.end) 223 | 224 | s: ModelSelect = DbBarData.select().where( 225 | (DbBarData.symbol == symbol) 226 | & (DbBarData.exchange == exchange.value) 227 | & (DbBarData.interval == interval.value) 228 | ) 229 | overview.count = s.count() 230 | 231 | overview.save() 232 | 233 | return True 234 | 235 | def save_tick_data(self, ticks: list[TickData], stream: bool = False) -> bool: 236 | """保存TICK数据""" 237 | # 读取主键参数 238 | tick: TickData = ticks[0] 239 | symbol: str = tick.symbol 240 | exchange: Exchange = tick.exchange 241 | 242 | # 将TickData数据转换为字典,并调整时区 243 | data: list = [] 244 | 245 | for tick in ticks: 246 | tick.datetime = convert_tz(tick.datetime) 247 | 248 | d: dict = tick.__dict__ 249 | d["exchange"] = d["exchange"].value 250 | d.pop("gateway_name") 251 | d.pop("vt_symbol") 252 | d.pop("extra") 253 | data.append(d) 254 | 255 | # 使用upsert操作将数据更新到数据库中 256 | with self.db.atomic(): 257 | for c in chunked(data, 50): 258 | DbTickData.insert_many(c).on_conflict_replace().execute() 259 | 260 | # 更新Tick汇总数据 261 | overview: DbTickOverview = DbTickOverview.get_or_none( 262 | DbTickOverview.symbol == symbol, 263 | DbTickOverview.exchange == exchange.value, 264 | ) 265 | 266 | if not overview: 267 | overview = DbTickOverview() 268 | overview.symbol = symbol 269 | overview.exchange = exchange.value 270 | overview.start = ticks[0].datetime 271 | overview.end = ticks[-1].datetime 272 | overview.count = len(ticks) 273 | elif stream: 274 | overview.end = ticks[-1].datetime 275 | overview.count += len(ticks) 276 | else: 277 | overview.start = min(ticks[0].datetime, overview.start) 278 | overview.end = max(ticks[-1].datetime, overview.end) 279 | 280 | s: ModelSelect = DbTickData.select().where( 281 | (DbTickData.symbol == symbol) 282 | & (DbTickData.exchange == exchange.value) 283 | ) 284 | overview.count = s.count() 285 | 286 | overview.save() 287 | 288 | return True 289 | 290 | def load_bar_data( 291 | self, 292 | symbol: str, 293 | exchange: Exchange, 294 | interval: Interval, 295 | start: datetime, 296 | end: datetime 297 | ) -> list[BarData]: 298 | """""" 299 | s: ModelSelect = ( 300 | DbBarData.select().where( 301 | (DbBarData.symbol == symbol) 302 | & (DbBarData.exchange == exchange.value) 303 | & (DbBarData.interval == interval.value) 304 | & (DbBarData.datetime >= start) 305 | & (DbBarData.datetime <= end) 306 | ).order_by(DbBarData.datetime) 307 | ) 308 | 309 | bars: list[BarData] = [] 310 | for db_bar in s: 311 | bar: BarData = BarData( 312 | symbol=db_bar.symbol, 313 | exchange=Exchange(db_bar.exchange), 314 | datetime=datetime.fromtimestamp(db_bar.datetime.timestamp(), DB_TZ), 315 | interval=Interval(db_bar.interval), 316 | volume=db_bar.volume, 317 | turnover=db_bar.turnover, 318 | open_interest=db_bar.open_interest, 319 | open_price=db_bar.open_price, 320 | high_price=db_bar.high_price, 321 | low_price=db_bar.low_price, 322 | close_price=db_bar.close_price, 323 | gateway_name="DB" 324 | ) 325 | bars.append(bar) 326 | 327 | return bars 328 | 329 | def load_tick_data( 330 | self, 331 | symbol: str, 332 | exchange: Exchange, 333 | start: datetime, 334 | end: datetime 335 | ) -> list[TickData]: 336 | """读取TICK数据""" 337 | s: ModelSelect = ( 338 | DbTickData.select().where( 339 | (DbTickData.symbol == symbol) 340 | & (DbTickData.exchange == exchange.value) 341 | & (DbTickData.datetime >= start) 342 | & (DbTickData.datetime <= end) 343 | ).order_by(DbTickData.datetime) 344 | ) 345 | 346 | ticks: list[TickData] = [] 347 | for db_tick in s: 348 | tick: TickData = TickData( 349 | symbol=db_tick.symbol, 350 | exchange=Exchange(db_tick.exchange), 351 | datetime=datetime.fromtimestamp(db_tick.datetime.timestamp(), DB_TZ), 352 | name=db_tick.name, 353 | volume=db_tick.volume, 354 | turnover=db_tick.turnover, 355 | open_interest=db_tick.open_interest, 356 | last_price=db_tick.last_price, 357 | last_volume=db_tick.last_volume, 358 | limit_up=db_tick.limit_up, 359 | limit_down=db_tick.limit_down, 360 | open_price=db_tick.open_price, 361 | high_price=db_tick.high_price, 362 | low_price=db_tick.low_price, 363 | pre_close=db_tick.pre_close, 364 | bid_price_1=db_tick.bid_price_1, 365 | bid_price_2=db_tick.bid_price_2, 366 | bid_price_3=db_tick.bid_price_3, 367 | bid_price_4=db_tick.bid_price_4, 368 | bid_price_5=db_tick.bid_price_5, 369 | ask_price_1=db_tick.ask_price_1, 370 | ask_price_2=db_tick.ask_price_2, 371 | ask_price_3=db_tick.ask_price_3, 372 | ask_price_4=db_tick.ask_price_4, 373 | ask_price_5=db_tick.ask_price_5, 374 | bid_volume_1=db_tick.bid_volume_1, 375 | bid_volume_2=db_tick.bid_volume_2, 376 | bid_volume_3=db_tick.bid_volume_3, 377 | bid_volume_4=db_tick.bid_volume_4, 378 | bid_volume_5=db_tick.bid_volume_5, 379 | ask_volume_1=db_tick.ask_volume_1, 380 | ask_volume_2=db_tick.ask_volume_2, 381 | ask_volume_3=db_tick.ask_volume_3, 382 | ask_volume_4=db_tick.ask_volume_4, 383 | ask_volume_5=db_tick.ask_volume_5, 384 | localtime=db_tick.localtime, 385 | gateway_name="DB" 386 | ) 387 | ticks.append(tick) 388 | 389 | return ticks 390 | 391 | def delete_bar_data( 392 | self, 393 | symbol: str, 394 | exchange: Exchange, 395 | interval: Interval 396 | ) -> int: 397 | """删除K线数据""" 398 | d: ModelDelete = DbBarData.delete().where( 399 | (DbBarData.symbol == symbol) 400 | & (DbBarData.exchange == exchange.value) 401 | & (DbBarData.interval == interval.value) 402 | ) 403 | count: int = d.execute() 404 | 405 | # 删除K线汇总数据 406 | d2: ModelDelete = DbBarOverview.delete().where( 407 | (DbBarOverview.symbol == symbol) 408 | & (DbBarOverview.exchange == exchange.value) 409 | & (DbBarOverview.interval == interval.value) 410 | ) 411 | d2.execute() 412 | return count 413 | 414 | def delete_tick_data( 415 | self, 416 | symbol: str, 417 | exchange: Exchange 418 | ) -> int: 419 | """删除TICK数据""" 420 | d: ModelDelete = DbTickData.delete().where( 421 | (DbTickData.symbol == symbol) 422 | & (DbTickData.exchange == exchange.value) 423 | ) 424 | 425 | count: int = d.execute() 426 | 427 | # 删除Tick汇总数据 428 | d2: ModelDelete = DbTickOverview.delete().where( 429 | (DbTickOverview.symbol == symbol) 430 | & (DbTickOverview.exchange == exchange.value) 431 | ) 432 | d2.execute() 433 | return count 434 | 435 | def get_bar_overview(self) -> list[BarOverview]: 436 | """查询数据库中的K线汇总信息""" 437 | # 如果已有K线,但缺失汇总信息,则执行初始化 438 | data_count: int = DbBarData.select().count() 439 | overview_count: int = DbBarOverview.select().count() 440 | if data_count and not overview_count: 441 | self.init_bar_overview() 442 | 443 | s: ModelSelect = DbBarOverview.select() 444 | overviews: list[BarOverview] = [] 445 | for overview in s: 446 | overview.exchange = Exchange(overview.exchange) 447 | overview.interval = Interval(overview.interval) 448 | overviews.append(overview) 449 | return overviews 450 | 451 | def get_tick_overview(self) -> list[TickOverview]: 452 | """查询数据库中的Tick汇总信息""" 453 | s: ModelSelect = DbTickOverview.select() 454 | overviews: list = [] 455 | for overview in s: 456 | overview.exchange = Exchange(overview.exchange) 457 | overviews.append(overview) 458 | return overviews 459 | 460 | def init_bar_overview(self) -> None: 461 | """初始化数据库中的K线汇总信息""" 462 | s: ModelSelect = ( 463 | DbBarData.select( 464 | DbBarData.symbol, 465 | DbBarData.exchange, 466 | DbBarData.interval, 467 | fn.COUNT(DbBarData.id).alias("count") 468 | ).group_by( 469 | DbBarData.symbol, 470 | DbBarData.exchange, 471 | DbBarData.interval 472 | ) 473 | ) 474 | 475 | for data in s: 476 | overview: DbBarOverview = DbBarOverview() 477 | overview.symbol = data.symbol 478 | overview.exchange = data.exchange 479 | overview.interval = data.interval 480 | overview.count = data.count 481 | 482 | start_bar: DbBarData = ( 483 | DbBarData.select() 484 | .where( 485 | (DbBarData.symbol == data.symbol) 486 | & (DbBarData.exchange == data.exchange) 487 | & (DbBarData.interval == data.interval) 488 | ) 489 | .order_by(Asc(DbBarData.datetime)) 490 | .first() 491 | ) 492 | overview.start = start_bar.datetime 493 | 494 | end_bar: DbBarData = ( 495 | DbBarData.select() 496 | .where( 497 | (DbBarData.symbol == data.symbol) 498 | & (DbBarData.exchange == data.exchange) 499 | & (DbBarData.interval == data.interval) 500 | ) 501 | .order_by(Desc(DbBarData.datetime)) 502 | .first() 503 | ) 504 | overview.end = end_bar.datetime 505 | 506 | overview.save() 507 | --------------------------------------------------------------------------------