├── .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 |
--------------------------------------------------------------------------------