├── .flake8
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── setup.cfg
├── setup.py
└── vnpy_datarecorder
├── __init__.py
├── engine.py
└── ui
├── __init__.py
├── recorder.ico
└── widget.py
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = build,__pycache__,__init__.py
3 | ignore =
4 | E501
5 | W503
6 |
--------------------------------------------------------------------------------
/.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.0.8版本
2 |
3 | 1. 替换所有的Qt信号pyqtSignal为Signal
4 |
5 | # 1.0.7版本
6 |
7 | 1. 补充价差Tick行情中缺失的localtime字段
8 | 2. 类型声明使用Python内置类型
9 |
10 | # 1.0.6版本
11 |
12 | 1. 增加基于Tick时间戳和本地时间戳偏离范围的数据过滤支持
13 |
14 | # 1.0.5版本
15 |
16 | 1. 修复vnpy_spreadtrading升级后报错找不到'to_tick'的问题
17 |
18 | # 1.0.4版本
19 |
20 | 1. 执行写入数据操作时,使用流式参数stream来优化性能
21 |
22 | # 1.0.3版本
23 |
24 | 1. 将模块的图标文件信息,改为完整路径字符串
25 |
--------------------------------------------------------------------------------
/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框架的行情录制模块
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## 说明
15 |
16 | DataRecorder是用于实时行情记录的模块,用户可以利用该模块记录实时Tick数据和K线数据,并自动写入保存到数据库中。
17 |
18 | 记录的数据可以通过DataManager模块查看,也可以用于CtaBacktester的历史回测,以及CtaStrategy、PortfolioStrategy等策略的实盘初始化。
19 |
20 | ## 安装
21 |
22 | 安装环境推荐基于3.9.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com)】。
23 |
24 | 直接使用pip命令:
25 |
26 | ```
27 | pip install vnpy_datarecorder
28 | ```
29 |
30 |
31 | 或者下载源代码后,解压后在cmd中运行:
32 |
33 | ```
34 | pip install .
35 | ```
36 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = vnpy_datarecorder
3 | version = 1.0.8
4 | url = https://www.vnpy.com
5 | license = MIT
6 | author = Xiaoyou Chen
7 | author_email = xiaoyou.chen@mail.vnpy.com
8 | description = Data recorder application for VeighNa quant trading framework.
9 | long_description = file: README.md
10 | long_description_content_type = text/markdown
11 | keywords =
12 | quant
13 | quantitative
14 | investment
15 | trading
16 | algotrading
17 | classifiers =
18 | Development Status :: 5 - Production/Stable
19 | Operating System :: OS Independent
20 | Programming Language :: Python :: 3
21 | Programming Language :: Python :: 3.10
22 | Programming Language :: Python :: 3.11
23 | Programming Language :: Python :: 3.12
24 | Programming Language :: Python :: 3.13
25 | Topic :: Office/Business :: Financial :: Investment
26 | Programming Language :: Python :: Implementation :: CPython
27 | License :: OSI Approved :: MIT License
28 | Natural Language :: Chinese (Simplified)
29 |
30 | [options]
31 | packages = find:
32 | zip_safe = False
33 |
34 | [options.package_data]
35 | * = *.ico
36 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 |
4 | setup()
5 |
--------------------------------------------------------------------------------
/vnpy_datarecorder/__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 pathlib import Path
25 |
26 | import importlib_metadata
27 | from vnpy.trader.app import BaseApp
28 |
29 | from .engine import RecorderEngine, APP_NAME
30 |
31 |
32 | try:
33 | __version__ = importlib_metadata.version("vnpy_datarecorder")
34 | except importlib_metadata.PackageNotFoundError:
35 | __version__ = "dev"
36 |
37 |
38 | class DataRecorderApp(BaseApp):
39 | """"""
40 |
41 | app_name: str = APP_NAME
42 | app_module: str = __module__
43 | app_path: Path = Path(__file__).parent
44 | display_name: str = "行情记录"
45 | engine_class: RecorderEngine = RecorderEngine
46 | widget_name: str = "RecorderManager"
47 | icon_name: str = str(app_path.joinpath("ui", "recorder.ico"))
48 |
--------------------------------------------------------------------------------
/vnpy_datarecorder/engine.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from threading import Thread
3 | from queue import Queue, Empty
4 | from copy import copy
5 | from collections import defaultdict
6 | from typing import Optional
7 | from datetime import datetime, timedelta
8 |
9 | from vnpy.event import Event, EventEngine
10 | from vnpy.trader.engine import BaseEngine, MainEngine
11 | from vnpy.trader.constant import Exchange
12 | from vnpy.trader.object import (
13 | SubscribeRequest,
14 | TickData,
15 | BarData,
16 | ContractData
17 | )
18 | from vnpy.trader.event import EVENT_TICK, EVENT_CONTRACT, EVENT_TIMER
19 | from vnpy.trader.utility import load_json, save_json, BarGenerator
20 | from vnpy.trader.database import BaseDatabase, get_database, DB_TZ
21 | from vnpy_spreadtrading.base import EVENT_SPREAD_DATA, SpreadItem
22 |
23 |
24 | APP_NAME = "DataRecorder"
25 |
26 | EVENT_RECORDER_LOG = "eRecorderLog"
27 | EVENT_RECORDER_UPDATE = "eRecorderUpdate"
28 | EVENT_RECORDER_EXCEPTION = "eRecorderException"
29 |
30 |
31 | class RecorderEngine(BaseEngine):
32 | """
33 | For running data recorder.
34 | """
35 |
36 | setting_filename: str = "data_recorder_setting.json"
37 |
38 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None:
39 | """"""
40 | super().__init__(main_engine, event_engine, APP_NAME)
41 |
42 | self.queue: Queue = Queue()
43 | self.thread: Thread = Thread(target=self.run)
44 | self.active: bool = False
45 |
46 | self.tick_recordings: dict[str, dict] = {}
47 | self.bar_recordings: dict[str, dict] = {}
48 | self.bar_generators: dict[str, BarGenerator] = {}
49 |
50 | self.timer_count: int = 0
51 | self.timer_interval: int = 10
52 |
53 | self.ticks: dict[str, list[TickData]] = defaultdict(list)
54 | self.bars: dict[str, list[BarData]] = defaultdict(list)
55 |
56 | self.filter_dt: datetime = datetime.now(DB_TZ) # Tick数据过滤的时间戳
57 | self.filter_window: int = 60 # Tick数据过滤的时间窗口,默认60秒
58 | self.filter_delta: timedelta = None # Tick数据过滤的时间偏差对象
59 |
60 | self.database: BaseDatabase = get_database()
61 |
62 | self.load_setting()
63 | self.register_event()
64 | self.start()
65 | self.put_event()
66 |
67 | def load_setting(self) -> None:
68 | """"""
69 | setting: dict = load_json(self.setting_filename)
70 | self.tick_recordings = setting.get("tick", {})
71 | self.bar_recordings = setting.get("bar", {})
72 |
73 | self.filter_window = setting.get("filter_window", 60)
74 | self.filter_delta = timedelta(seconds=self.filter_window)
75 |
76 | def save_setting(self) -> None:
77 | """"""
78 | setting: dict = {
79 | "tick": self.tick_recordings,
80 | "bar": self.bar_recordings
81 | }
82 | save_json(self.setting_filename, setting)
83 |
84 | def run(self) -> None:
85 | """"""
86 | while self.active:
87 | try:
88 | task: object = self.queue.get(timeout=1)
89 | task_type, data = task
90 |
91 | if task_type == "tick":
92 | self.database.save_tick_data(data, stream=True)
93 | elif task_type == "bar":
94 | self.database.save_bar_data(data, stream=True)
95 |
96 | except Empty:
97 | continue
98 |
99 | except Exception:
100 | self.active = False
101 |
102 | info = traceback.format_exc()
103 | event: Event = Event(EVENT_RECORDER_EXCEPTION, info)
104 | self.event_engine.put(event)
105 |
106 | def close(self) -> None:
107 | """"""
108 | self.active = False
109 |
110 | if self.thread.is_alive():
111 | self.thread.join()
112 |
113 | def start(self) -> None:
114 | """"""
115 | self.active = True
116 | self.thread.start()
117 |
118 | def add_bar_recording(self, vt_symbol: str) -> None:
119 | """"""
120 | if vt_symbol in self.bar_recordings:
121 | self.write_log(f"已在K线记录列表中:{vt_symbol}")
122 | return
123 |
124 | if Exchange.LOCAL.value not in vt_symbol:
125 | contract: Optional[ContractData] = self.main_engine.get_contract(vt_symbol)
126 | if not contract:
127 | self.write_log(f"找不到合约:{vt_symbol}")
128 | return
129 |
130 | self.bar_recordings[vt_symbol] = {
131 | "symbol": contract.symbol,
132 | "exchange": contract.exchange.value,
133 | "gateway_name": contract.gateway_name
134 | }
135 |
136 | self.subscribe(contract)
137 | else:
138 | self.bar_recordings[vt_symbol] = {}
139 |
140 | self.save_setting()
141 | self.put_event()
142 |
143 | self.write_log(f"添加K线记录成功:{vt_symbol}")
144 |
145 | def add_tick_recording(self, vt_symbol: str) -> None:
146 | """"""
147 | if vt_symbol in self.tick_recordings:
148 | self.write_log(f"已在Tick记录列表中:{vt_symbol}")
149 | return
150 |
151 | # For normal contract
152 | if Exchange.LOCAL.value not in vt_symbol:
153 | contract: Optional[ContractData] = self.main_engine.get_contract(vt_symbol)
154 | if not contract:
155 | self.write_log(f"找不到合约:{vt_symbol}")
156 | return
157 |
158 | self.tick_recordings[vt_symbol] = {
159 | "symbol": contract.symbol,
160 | "exchange": contract.exchange.value,
161 | "gateway_name": contract.gateway_name
162 | }
163 |
164 | self.subscribe(contract)
165 | # No need to subscribe for spread data
166 | else:
167 | self.tick_recordings[vt_symbol] = {}
168 |
169 | self.save_setting()
170 | self.put_event()
171 |
172 | self.write_log(f"添加Tick记录成功:{vt_symbol}")
173 |
174 | def remove_bar_recording(self, vt_symbol: str) -> None:
175 | """"""
176 | if vt_symbol not in self.bar_recordings:
177 | self.write_log(f"不在K线记录列表中:{vt_symbol}")
178 | return
179 |
180 | self.bar_recordings.pop(vt_symbol)
181 | self.save_setting()
182 | self.put_event()
183 |
184 | self.write_log(f"移除K线记录成功:{vt_symbol}")
185 |
186 | def remove_tick_recording(self, vt_symbol: str) -> None:
187 | """"""
188 | if vt_symbol not in self.tick_recordings:
189 | self.write_log(f"不在Tick记录列表中:{vt_symbol}")
190 | return
191 |
192 | self.tick_recordings.pop(vt_symbol)
193 | self.save_setting()
194 | self.put_event()
195 |
196 | self.write_log(f"移除Tick记录成功:{vt_symbol}")
197 |
198 | def register_event(self) -> None:
199 | """"""
200 | self.event_engine.register(EVENT_TIMER, self.process_timer_event)
201 | self.event_engine.register(EVENT_TICK, self.process_tick_event)
202 | self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
203 | self.event_engine.register(EVENT_SPREAD_DATA, self.process_spread_event)
204 |
205 | def update_tick(self, tick: TickData) -> None:
206 | """"""
207 | # 过滤偏离本地时间戳过大的Tick数据
208 | tick_delta: timedelta = abs(tick.datetime - self.filter_dt)
209 | if abs(tick_delta) >= self.filter_delta:
210 | return
211 |
212 | if tick.vt_symbol in self.tick_recordings:
213 | self.record_tick(copy(tick))
214 |
215 | if tick.vt_symbol in self.bar_recordings:
216 | bg: BarGenerator = self.get_bar_generator(tick.vt_symbol)
217 | bg.update_tick(copy(tick))
218 |
219 | def process_timer_event(self, event: Event) -> None:
220 | """"""
221 | self.filter_dt = datetime.now(DB_TZ)
222 |
223 | self.timer_count += 1
224 | if self.timer_count < self.timer_interval:
225 | return
226 | self.timer_count = 0
227 |
228 | for bars in self.bars.values():
229 | self.queue.put(("bar", bars))
230 | self.bars.clear()
231 |
232 | for ticks in self.ticks.values():
233 | self.queue.put(("tick", ticks))
234 | self.ticks.clear()
235 |
236 | def process_tick_event(self, event: Event) -> None:
237 | """"""
238 | tick: TickData = event.data
239 | self.update_tick(tick)
240 |
241 | def process_contract_event(self, event: Event) -> None:
242 | """"""
243 | contract: ContractData = event.data
244 | vt_symbol: str = contract.vt_symbol
245 |
246 | if (vt_symbol in self.tick_recordings or vt_symbol in self.bar_recordings):
247 | self.subscribe(contract)
248 |
249 | def process_spread_event(self, event: Event) -> None:
250 | """"""
251 | spread_item: SpreadItem = event.data
252 | tick: TickData = TickData(
253 | symbol=spread_item.name,
254 | exchange=Exchange.LOCAL,
255 | datetime=spread_item.datetime,
256 | name=spread_item.name,
257 | last_price=(spread_item.bid_price + spread_item.ask_price) / 2,
258 | bid_price_1=spread_item.bid_price,
259 | ask_price_1=spread_item.ask_price,
260 | bid_volume_1=spread_item.bid_volume,
261 | ask_volume_1=spread_item.ask_volume,
262 | localtime=spread_item.datetime,
263 | gateway_name="SPREAD"
264 | )
265 |
266 | # Filter not inited spread data
267 | if tick.datetime:
268 | self.update_tick(tick)
269 |
270 | def write_log(self, msg: str) -> None:
271 | """"""
272 | event: Event = Event(
273 | EVENT_RECORDER_LOG,
274 | msg
275 | )
276 | self.event_engine.put(event)
277 |
278 | def put_event(self) -> None:
279 | """"""
280 | tick_symbols: list[str] = list(self.tick_recordings.keys())
281 | tick_symbols.sort()
282 |
283 | bar_symbols: list[str] = list(self.bar_recordings.keys())
284 | bar_symbols.sort()
285 |
286 | data: dict = {
287 | "tick": tick_symbols,
288 | "bar": bar_symbols
289 | }
290 |
291 | event: Event = Event(
292 | EVENT_RECORDER_UPDATE,
293 | data
294 | )
295 | self.event_engine.put(event)
296 |
297 | def record_tick(self, tick: TickData) -> None:
298 | """"""
299 | self.ticks[tick.vt_symbol].append(tick)
300 |
301 | def record_bar(self, bar: BarData) -> None:
302 | """"""
303 | self.bars[bar.vt_symbol].append(bar)
304 |
305 | def get_bar_generator(self, vt_symbol: str) -> BarGenerator:
306 | """"""
307 | bg: Optional[BarGenerator] = self.bar_generators.get(vt_symbol, None)
308 |
309 | if not bg:
310 | bg = BarGenerator(self.record_bar)
311 | self.bar_generators[vt_symbol] = bg
312 |
313 | return bg
314 |
315 | def subscribe(self, contract: ContractData) -> None:
316 | """"""
317 | req: SubscribeRequest = SubscribeRequest(
318 | symbol=contract.symbol,
319 | exchange=contract.exchange
320 | )
321 | self.main_engine.subscribe(req, contract.gateway_name)
322 |
--------------------------------------------------------------------------------
/vnpy_datarecorder/ui/__init__.py:
--------------------------------------------------------------------------------
1 | from .widget import RecorderManager
2 |
--------------------------------------------------------------------------------
/vnpy_datarecorder/ui/recorder.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vnpy/vnpy_datarecorder/7db93460df2d9094b33bf64f5e8f7887cb1fa9f6/vnpy_datarecorder/ui/recorder.ico
--------------------------------------------------------------------------------
/vnpy_datarecorder/ui/widget.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from vnpy.event import Event, EventEngine
4 | from vnpy.trader.engine import MainEngine
5 | from vnpy.trader.ui import QtCore, QtWidgets
6 | from vnpy.trader.event import EVENT_CONTRACT
7 | from vnpy.trader.object import ContractData
8 |
9 | from ..engine import (
10 | APP_NAME,
11 | EVENT_RECORDER_LOG,
12 | EVENT_RECORDER_UPDATE,
13 | EVENT_RECORDER_EXCEPTION,
14 | RecorderEngine
15 | )
16 |
17 |
18 | class RecorderManager(QtWidgets.QWidget):
19 | """"""
20 |
21 | signal_log: QtCore.Signal = QtCore.Signal(Event)
22 | signal_update: QtCore.Signal = QtCore.Signal(Event)
23 | signal_contract: QtCore.Signal = QtCore.Signal(Event)
24 | signal_exception: QtCore.Signal = QtCore.Signal(Event)
25 |
26 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None:
27 | super().__init__()
28 |
29 | self.main_engine: MainEngine = main_engine
30 | self.event_engine: EventEngine = event_engine
31 | self.recorder_engine: RecorderEngine = main_engine.get_engine(APP_NAME)
32 |
33 | self.init_ui()
34 | self.register_event()
35 | self.recorder_engine.put_event()
36 |
37 | def init_ui(self) -> None:
38 | """"""
39 | self.setWindowTitle("行情记录")
40 | self.resize(1000, 600)
41 |
42 | # Create widgets
43 | self.symbol_line: QtWidgets.QLineEdit = QtWidgets.QLineEdit()
44 |
45 | self.interval_spin: QtWidgets.QSpinBox = QtWidgets.QSpinBox()
46 | self.interval_spin.setMinimum(1)
47 | self.interval_spin.setMaximum(60)
48 | self.interval_spin.setValue(self.recorder_engine.timer_interval)
49 | self.interval_spin.setSuffix("秒")
50 | self.interval_spin.valueChanged.connect(self.set_interval)
51 |
52 | contracts: list[ContractData] = self.main_engine.get_all_contracts()
53 | self.vt_symbols: list = [contract.vt_symbol for contract in contracts]
54 |
55 | self.symbol_completer: QtWidgets.QCompleter = QtWidgets.QCompleter(self.vt_symbols)
56 | self.symbol_completer.setFilterMode(QtCore.Qt.MatchContains)
57 | self.symbol_completer.setCompletionMode(self.symbol_completer.CompletionMode.PopupCompletion)
58 | self.symbol_line.setCompleter(self.symbol_completer)
59 |
60 | add_bar_button: QtWidgets.QPushButton = QtWidgets.QPushButton("添加")
61 | add_bar_button.clicked.connect(self.add_bar_recording)
62 |
63 | remove_bar_button: QtWidgets.QPushButton = QtWidgets.QPushButton("移除")
64 | remove_bar_button.clicked.connect(self.remove_bar_recording)
65 |
66 | add_tick_button: QtWidgets.QPushButton = QtWidgets.QPushButton("添加")
67 | add_tick_button.clicked.connect(self.add_tick_recording)
68 |
69 | remove_tick_button: QtWidgets.QPushButton = QtWidgets.QPushButton("移除")
70 | remove_tick_button.clicked.connect(self.remove_tick_recording)
71 |
72 | self.bar_recording_edit: QtWidgets.QTextEdit = QtWidgets.QTextEdit()
73 | self.bar_recording_edit.setReadOnly(True)
74 |
75 | self.tick_recording_edit: QtWidgets.QTextEdit = QtWidgets.QTextEdit()
76 | self.tick_recording_edit.setReadOnly(True)
77 |
78 | self.log_edit: QtWidgets.QTextEdit = QtWidgets.QTextEdit()
79 | self.log_edit.setReadOnly(True)
80 |
81 | # Set layout
82 | grid: QtWidgets.QGridLayout = QtWidgets.QGridLayout()
83 | grid.addWidget(QtWidgets.QLabel("K线记录"), 0, 0)
84 | grid.addWidget(add_bar_button, 0, 1)
85 | grid.addWidget(remove_bar_button, 0, 2)
86 | grid.addWidget(QtWidgets.QLabel("Tick记录"), 1, 0)
87 | grid.addWidget(add_tick_button, 1, 1)
88 | grid.addWidget(remove_tick_button, 1, 2)
89 |
90 | form: QtWidgets.QFormLayout = QtWidgets.QFormLayout()
91 | form.addRow("本地代码", self.symbol_line)
92 | form.addRow("写入间隔", self.interval_spin)
93 |
94 | hbox: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout()
95 | hbox.addLayout(form)
96 | hbox.addWidget(QtWidgets.QLabel(" "))
97 | hbox.addLayout(grid)
98 | hbox.addStretch()
99 |
100 | grid2: QtWidgets.QGridLayout = QtWidgets.QGridLayout()
101 | grid2.addWidget(QtWidgets.QLabel("K线记录列表"), 0, 0)
102 | grid2.addWidget(QtWidgets.QLabel("Tick记录列表"), 0, 1)
103 | grid2.addWidget(self.bar_recording_edit, 1, 0)
104 | grid2.addWidget(self.tick_recording_edit, 1, 1)
105 | grid2.addWidget(self.log_edit, 2, 0, 1, 2)
106 |
107 | vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout()
108 | vbox.addLayout(hbox)
109 | vbox.addLayout(grid2)
110 | self.setLayout(vbox)
111 |
112 | def register_event(self) -> None:
113 | """"""
114 | self.signal_log.connect(self.process_log_event)
115 | self.signal_contract.connect(self.process_contract_event)
116 | self.signal_update.connect(self.process_update_event)
117 | self.signal_exception.connect(self.process_exception_event)
118 |
119 | self.event_engine.register(EVENT_CONTRACT, self.signal_contract.emit)
120 | self.event_engine.register(
121 | EVENT_RECORDER_LOG, self.signal_log.emit)
122 | self.event_engine.register(
123 | EVENT_RECORDER_UPDATE, self.signal_update.emit)
124 | self.event_engine.register(EVENT_RECORDER_EXCEPTION, self.signal_exception.emit)
125 |
126 | def process_log_event(self, event: Event) -> None:
127 | """"""
128 | timestamp: str = datetime.now().strftime("%H:%M:%S")
129 | msg: str = f"{timestamp}\t{event.data}"
130 | self.log_edit.append(msg)
131 |
132 | def process_update_event(self, event: Event) -> None:
133 | """"""
134 | data: object = event.data
135 |
136 | self.bar_recording_edit.clear()
137 | bar_text: str = "\n".join(data["bar"])
138 | self.bar_recording_edit.setText(bar_text)
139 |
140 | self.tick_recording_edit.clear()
141 | tick_text: str = "\n".join(data["tick"])
142 | self.tick_recording_edit.setText(tick_text)
143 |
144 | def process_contract_event(self, event: Event) -> None:
145 | """"""
146 | contract: ContractData = event.data
147 | self.vt_symbols.append(contract.vt_symbol)
148 |
149 | model: QtCore.QAbstractItemModel = self.symbol_completer.model()
150 | model.setStringList(self.vt_symbols)
151 |
152 | def process_exception_event(self, event: Event) -> None:
153 | """"""
154 | exc_info = event.data
155 | raise exc_info[1].with_traceback(exc_info[2])
156 |
157 | def add_bar_recording(self) -> None:
158 | """"""
159 | vt_symbol: str = self.symbol_line.text()
160 | self.recorder_engine.add_bar_recording(vt_symbol)
161 |
162 | def add_tick_recording(self) -> None:
163 | """"""
164 | vt_symbol: str = self.symbol_line.text()
165 | self.recorder_engine.add_tick_recording(vt_symbol)
166 |
167 | def remove_bar_recording(self) -> None:
168 | """"""
169 | vt_symbol: str = self.symbol_line.text()
170 | self.recorder_engine.remove_bar_recording(vt_symbol)
171 |
172 | def remove_tick_recording(self) -> None:
173 | """"""
174 | vt_symbol: str = self.symbol_line.text()
175 | self.recorder_engine.remove_tick_recording(vt_symbol)
176 |
177 | def set_interval(self, interval) -> None:
178 | """"""
179 | self.recorder_engine.timer_interval = interval
180 |
--------------------------------------------------------------------------------