├── .flake8 ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── candle_chart │ └── run.py ├── evo_trader │ └── run.py └── simple_rpc │ ├── test_client.py │ └── test_server.py ├── install_linux.sh ├── install_macos.sh ├── install_windows.bat ├── logo.png ├── requirements.txt ├── setup.cfg ├── setup.py └── vnpy_evo ├── __init__.py ├── chart └── __init__.py ├── event └── __init__.py ├── rest ├── __init__.py └── rest_client.py ├── rpc └── __init__.py ├── trader ├── __init__.py ├── app.py ├── constant.py ├── converter.py ├── database.py ├── datafeed.py ├── engine.py ├── event.py ├── gateway.py ├── object.py ├── optimize.py ├── setting.py ├── ui │ ├── __init__.py │ ├── ico │ │ ├── __init__.py │ │ └── veighna.ico │ ├── mainwindow.py │ ├── monitor.py │ ├── qt.py │ └── widget.py └── utility.py └── websocket ├── __init__.py └── websocket_client.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = venv,build,__pycache__,__init__.py,ib,talib,uic 3 | ignore = 4 | # line too long, fixed by black 5 | E501, 6 | # line break before binary operator 7 | W503, 8 | per-file-ignores = 9 | vnpy_evo\trader\app.py: F401, F403 10 | vnpy_evo\trader\converter.py: F401, F403 11 | vnpy_evo\trader\constant.py: F401, F403 12 | vnpy_evo\trader\database.py: F401, F403 13 | vnpy_evo\trader\datafeed.py: F401, F403 14 | vnpy_evo\trader\engine.py: F401, F403 15 | vnpy_evo\trader\event.py: F401, F403 16 | vnpy_evo\trader\gateway.py: F401, F403 17 | vnpy_evo\trader\object.py: F401, F403 18 | vnpy_evo\trader\optimize.py: F401, F403 19 | vnpy_evo\trader\setting.py: F401, F403 20 | vnpy_evo\trader\utility.py: F401, F403 21 | -------------------------------------------------------------------------------- /.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 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 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 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.2 2 | 3 | 1. enable auto reconnect of WebsocketClient 4 | 5 | # 0.3.1 6 | 7 | 1. add new vnpy_evo.rest as alternative of vnpy_rest 8 | 2. add new vnpy_evo.websocket as alternative of vnpy_websocket 9 | 10 | # 0.3.0 11 | 12 | 1. New vnpy_btse gateway module 13 | 2. New vnpy_novastrategy module for crypto market quant strategies 14 | 3. New vnpy_duckdb database module 15 | 4. Quick install script for Windows/Linux/Mac OS 16 | 17 | # 0.2.2 18 | 19 | 1. reimplement extract_vt_symbol function 20 | 21 | # 0.2.1 22 | 23 | 1. add BTSE exchange num 24 | 25 | # 0.2.0 26 | 27 | ## Gateway 28 | 29 | 1. Update vnpy_mt5 gateway module 30 | 2. Update vnpy_okx gateway module 31 | 3. Update vnpy_bybit gateway module 32 | 33 | ## Framwork 34 | 35 | 1. Add new OTC exchange enum 36 | 2. Only support English language for i18n 37 | 3. Fix bugs related to qfluentwidgets 38 | 39 | # 0.1.0 40 | 41 | 1. The first release of VeighNa Evo -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present, VeighNa Global 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include vnpy *.ico 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # By Traders, For Traders. 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | VeighNa Evo (vnpy_evo) is the core module for using [VeighNa (vnpy)](https://github.com/vnpy/vnpy) quant trading platform on the crypto market. 15 | 16 | ## Social 17 | 18 | - [![Twitter](https://img.shields.io/twitter/follow/veighna.svg?style=social&label=VeighNa%20Global)](https://x.com/veighna_global) Follow us on Twitter 19 | - [![Telegram Announcements](https://img.shields.io/badge/VeighNa%20Global-Channel-blue?logo=telegram)](https://t.me/veighna_channel) Follow our important announcements 20 | - [![Telegram Chat](https://img.shields.io/badge/VeighNa%20Global-Chat-blue?logo=telegram)](https://t.me/+8KGF_z35nK03YWE1) If you need technical support 21 | 22 | 23 | ## Features 24 | 25 | 1. Full-featured quantitative trading platform (vnpy_evo.trader) 26 | 27 | 2. Gateways which connect to exchanges for receiving market data and sending trading orders: 28 | 29 | * Crypto Market 30 | 31 | * Binance ([binance](https://www.github.com/veighna-global/vnpy_binance)): Spot/Perpetual/Futures/Option 32 | 33 | * OKX ([okx](https://www.github.com/veighna-global/vnpy_okx)): Spot/Perpetual/Futures/Option 34 | 35 | * Bybit ([bybit](https://www.github.com/veighna-global/vnpy_bybit)): Spot/Perpetual/Futures/Option 36 | 37 | * BTSE ([btse](https://www.github.com/veighna-global/vnpy_btse)): Spot/Perpetual/Futures 38 | 39 | * Forex Market 40 | 41 | * MT5 ([mt5](https://www.github.com/veighna-global/vnpy_mt5)): Forex/Gold/Commodity/Crypto 42 | 43 | 3. Applications for various quantitative strategies: 44 | 45 | * Nova Strategy ([nova_strategy](https://www.github.com/veighna-global/vnpy_novastrategy)): The quant strategy app module which is designed specifically for crypto markets, supports trend following, pair trading, multi-factor and many other types of quant strategies. 46 | 47 | 4. Event processing engine (vnpy_evo.event), which is the core of event-driven trading program 48 | 49 | 5. Database adaptors which support most commonly used databases: 50 | 51 | * DuckDB ([duckdb](https://www.github.com/veighna-global/vnpy_duckdb)): The high-performance in-process analytical database which is designed to be fast, reliable, portable, and easy to use. 52 | 53 | 6. Standarad RPC solution (vnpy_evo.rpc) for implementing complex trading systems with distributed deployments 54 | 55 | 7. High-performance charting widget (vnpy_evo.chart), which supports stream market data update 56 | 57 | ## Install 58 | 59 | **MacOS** 60 | 61 | Please ensure you have installed [XCode](https://developer.apple.com/xcode/) and [Homebrew](https://brew.sh/) before running the following command: 62 | 63 | ``` 64 | /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/veighna-global/vnpy_evo/HEAD/install_macos.sh)" 65 | ``` 66 | 67 | **Ubuntu** 68 | 69 | ``` 70 | sudo /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/veighna-global/vnpy_evo/HEAD/install_linux.sh)" 71 | ``` 72 | 73 | ## Example 74 | 75 | You can start running VeighNa Evo with only a few lines of code. 76 | 77 | ```Python 78 | from vnpy_evo.event import EventEngine 79 | from vnpy_evo.trader.engine import MainEngine 80 | from vnpy_evo.trader.ui import MainWindow, create_qapp 81 | 82 | from vnpy_binance import BinanceLinearGateway 83 | from vnpy_novastrategy import NovaStrategyApp 84 | 85 | def main(): 86 | qapp = create_qapp() 87 | 88 | event_engine = EventEngine() 89 | main_engine = MainEngine(event_engine) 90 | 91 | main_engine.add_gateway(BinanceUsdtGateway) 92 | main_engine.add_app(CtaStrategyApp) 93 | main_engine.add_app(NovaStrategyApp) 94 | 95 | main_window = MainWindow(main_engine, event_engine) 96 | main_window.showMaximized() 97 | 98 | qapp.exec() 99 | 100 | if __name__ == "__main__": 101 | main() 102 | ``` 103 | 104 | Open a terminal within the directory and run the following command to start VeighNa Trader. 105 | 106 | python run.py 107 | 108 | ## Licence 109 | 110 | MIT -------------------------------------------------------------------------------- /examples/candle_chart/run.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from vnpy_evo.trader.ui import create_qapp, QtCore 4 | from vnpy_evo.trader.constant import Exchange, Interval 5 | from vnpy_evo.trader.database import get_database 6 | from vnpy_evo.chart import ChartWidget, VolumeItem, CandleItem 7 | 8 | 9 | if __name__ == "__main__": 10 | app = create_qapp() 11 | 12 | database = get_database() 13 | bars = database.load_bar_data( 14 | "BTCUSDT", 15 | Exchange.BINANCE, 16 | interval=Interval.MINUTE, 17 | start=datetime(2023, 1, 1), 18 | end=datetime(2023, 3, 31) 19 | ) 20 | 21 | widget = ChartWidget() 22 | widget.add_plot("candle", hide_x_axis=True) 23 | widget.add_plot("volume", maximum_height=200) 24 | widget.add_item(CandleItem, "candle", "candle") 25 | widget.add_item(VolumeItem, "volume", "volume") 26 | widget.add_cursor() 27 | 28 | n = 1000 29 | history = bars[:n] 30 | new_data = bars[n:] 31 | 32 | widget.update_history(history) 33 | 34 | def update_bar(): 35 | bar = new_data.pop(0) 36 | widget.update_bar(bar) 37 | 38 | timer = QtCore.QTimer() 39 | timer.timeout.connect(update_bar) 40 | # timer.start(100) 41 | 42 | widget.show() 43 | app.exec() 44 | -------------------------------------------------------------------------------- /examples/evo_trader/run.py: -------------------------------------------------------------------------------- 1 | from vnpy_evo.event import EventEngine 2 | from vnpy_evo.trader.engine import MainEngine 3 | from vnpy_evo.trader.ui import MainWindow, create_qapp 4 | 5 | from vnpy_binance import BinanceLinearGateway 6 | from vnpy_novastrategy import NovaStrategyApp 7 | 8 | 9 | def main(): 10 | """""" 11 | qapp = create_qapp() 12 | 13 | event_engine = EventEngine() 14 | 15 | main_engine = MainEngine(event_engine) 16 | 17 | main_engine.add_gateway(BinanceLinearGateway) 18 | 19 | main_engine.add_app(NovaStrategyApp) 20 | 21 | main_window = MainWindow(main_engine, event_engine) 22 | main_window.showMaximized() 23 | 24 | qapp.exec() 25 | 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /examples/simple_rpc/test_client.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import absolute_import 3 | from time import sleep 4 | 5 | from vnpy_evo.rpc import RpcClient 6 | 7 | 8 | class TestClient(RpcClient): 9 | """ 10 | Test RpcClient 11 | """ 12 | 13 | def __init__(self): 14 | """ 15 | Constructor 16 | """ 17 | super(TestClient, self).__init__() 18 | 19 | def callback(self, topic, data): 20 | """ 21 | Realize callable function 22 | """ 23 | print(f"client received topic:{topic}, data:{data}") 24 | 25 | 26 | if __name__ == "__main__": 27 | req_address = "tcp://localhost:2014" 28 | sub_address = "tcp://localhost:4102" 29 | 30 | tc = TestClient() 31 | tc.subscribe_topic("") 32 | tc.start(req_address, sub_address) 33 | 34 | while 1: 35 | print(tc.add(1, 3)) 36 | sleep(2) 37 | -------------------------------------------------------------------------------- /examples/simple_rpc/test_server.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from __future__ import absolute_import 3 | from time import sleep, time 4 | 5 | from vnpy_evo.rpc import RpcServer 6 | 7 | 8 | class TestServer(RpcServer): 9 | """ 10 | Test RpcServer 11 | """ 12 | 13 | def __init__(self): 14 | """ 15 | Constructor 16 | """ 17 | super(TestServer, self).__init__() 18 | 19 | self.register(self.add) 20 | 21 | def add(self, a, b): 22 | """ 23 | Test function 24 | """ 25 | print(f"receiving:{a} {b}") 26 | return a + b 27 | 28 | 29 | if __name__ == "__main__": 30 | rep_address = "tcp://*:2014" 31 | pub_address = "tcp://*:4102" 32 | 33 | ts = TestServer() 34 | ts.start(rep_address, pub_address) 35 | 36 | while 1: 37 | content = f"current server time is {time()}" 38 | print(content) 39 | ts.publish("test", content) 40 | sleep(2) 41 | -------------------------------------------------------------------------------- /install_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install NumPy 4 | pip3 install numpy==1.26.4 5 | 6 | # Install ta-lib 7 | pushd /tmp 8 | wget https://pip.vnpy.com/colletion/ta-lib-0.4.0-src.tar.gz 9 | tar -xf ta-lib-0.4.0-src.tar.gz 10 | cd ta-lib 11 | ./configure --prefix=/usr 12 | make -j1 13 | make install 14 | popd 15 | 16 | pip3 install ta-lib 17 | 18 | # Install vnpy_evo 19 | pip3 install vnpy_evo 20 | pip3 install vnpy_duckdb 21 | pip3 install vnpy_binance vnpy_btse vnpy_okx vnpy_bybit 22 | pip3 install vnpy_novastrategy -------------------------------------------------------------------------------- /install_macos.sh: -------------------------------------------------------------------------------- 1 | #!/bin/zsh 2 | 3 | # Install NumPy 4 | pip3 install numpy==1.26.4 5 | 6 | # Install ta-lib 7 | brew install ta-lib 8 | pip3 install ta-lib 9 | 10 | # Install vnpy_evo 11 | pip3 install vnpy_evo 12 | pip3 install vnpy_duckdb 13 | pip3 install vnpy_binance vnpy_btse vnpy_okx vnpy_bybit 14 | pip3 install vnpy_novastrategy -------------------------------------------------------------------------------- /install_windows.bat: -------------------------------------------------------------------------------- 1 | :: Install NumPy 2 | pip install numpy==1.26.4 3 | 4 | :: Install ta-lib 5 | pip install ta-lib --index=https://pypi.vnpy.com 6 | 7 | :: Install vnpy_evo 8 | pip install vnpy_evo 9 | pip install vnpy_duckdb 10 | pip install vnpy_binance vnpy_btse vnpy_okx vnpy_bybit 11 | pip install vnpy_novastrategy -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veighna-global/vnpy_evo/3dc599124d4caa6343b75fea4b6580fe012ce52a/logo.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PySide6==6.3.0 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = vnpy_evo 3 | version = attr: vnpy_evo.__version__ 4 | author = VeighNa Global 5 | author_email = veighna@hotmail.com 6 | description = Core module for using VeighNa project in crypto markets. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://www.github.com/veighna-global 10 | license = MIT 11 | license_files = LICENSE 12 | keywords = 13 | quant 14 | quantitative 15 | investment 16 | trading 17 | algotrading 18 | crypto 19 | btc 20 | classifiers = 21 | Development Status :: 4 - Beta 22 | Operating System :: Microsoft :: Windows 23 | Operating System :: POSIX :: Linux 24 | Operating System :: MacOS 25 | Programming Language :: Python :: 3 26 | Programming Language :: Python :: 3.10 27 | Programming Language :: Python :: 3.11 28 | Programming Language :: Python :: 3.12 29 | Topic :: Office/Business :: Financial :: Investment 30 | Programming Language :: Python :: Implementation :: CPython 31 | License :: OSI Approved :: MIT License 32 | Natural Language :: English 33 | project_urls = 34 | Documentation = https://github.com/veighna-global/vnpy_evo 35 | 36 | [options] 37 | packages = find: 38 | include_package_data = True 39 | zip_safe = False 40 | install_requires = 41 | numpy==1.26.4 42 | vnpy 43 | importlib_metadata 44 | PySide6-Fluent-Widgets 45 | requests>=2.32.3 46 | websocket-client>=1.8.0 47 | loguru>=0.7.2 48 | 49 | [options.package_data] 50 | * = *.ico 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /vnpy_evo/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | # Copyright (c) 2023-present, VeighNa Global 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 | import os 24 | os.environ["LANG"] = "en" # Only support English 25 | 26 | 27 | __version__ = "0.3.2" 28 | -------------------------------------------------------------------------------- /vnpy_evo/chart/__init__.py: -------------------------------------------------------------------------------- 1 | from vnpy.chart import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/event/__init__.py: -------------------------------------------------------------------------------- 1 | from vnpy.event import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/rest/__init__.py: -------------------------------------------------------------------------------- 1 | from requests import Response 2 | 3 | from .rest_client import Request, RequestStatus, RestClient 4 | -------------------------------------------------------------------------------- /vnpy_evo/rest/rest_client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import traceback 3 | from datetime import datetime 4 | from enum import Enum 5 | from multiprocessing.dummy import Pool 6 | from queue import Empty, Queue 7 | from typing import Callable, Optional, Union, Type 8 | from types import TracebackType 9 | 10 | import requests 11 | 12 | 13 | CALLBACK_TYPE = Callable[[dict, "Request"], object] 14 | ON_FAILED_TYPE = Callable[[int, "Request"], object] 15 | ON_ERROR_TYPE = Callable[[Type, Exception, TracebackType, "Request"], object] 16 | 17 | 18 | class RequestStatus(Enum): 19 | """ 20 | Request status enum. 21 | """ 22 | 23 | ready = 0 # Request created 24 | success = 1 # Request successful (status code 2xx) 25 | failed = 2 # Request failed (status code not 2xx) 26 | error = 3 # Exception raised 27 | 28 | 29 | class Request(object): 30 | """ 31 | Request object for status check. 32 | """ 33 | 34 | def __init__( 35 | self, 36 | method: str, 37 | path: str, 38 | params: dict, 39 | data: Union[dict, str, bytes], 40 | headers: dict, 41 | callback: CALLBACK_TYPE = None, 42 | on_failed: ON_FAILED_TYPE = None, 43 | on_error: ON_ERROR_TYPE = None, 44 | extra: object = None, 45 | ) -> None: 46 | """""" 47 | self.method: str = method 48 | self.path: str = path 49 | self.callback: CALLBACK_TYPE = callback 50 | self.params: dict = params 51 | self.data: Union[dict, str, bytes] = data 52 | self.headers: dict = headers 53 | 54 | self.on_failed: ON_FAILED_TYPE = on_failed 55 | self.on_error: ON_ERROR_TYPE = on_error 56 | self.extra: object = extra 57 | 58 | self.response: requests.Response = None 59 | self.status: RequestStatus = RequestStatus.ready 60 | 61 | def __str__(self) -> str: 62 | """""" 63 | if self.response is None: 64 | status_code = "terminated" 65 | else: 66 | status_code = self.response.status_code 67 | 68 | return ( 69 | "request : {} {} {} because {}: \n" 70 | "headers: {}\n" 71 | "params: {}\n" 72 | "data: {}\n" 73 | "response:" 74 | "{}\n".format( 75 | self.method, 76 | self.path, 77 | self.status.name, 78 | status_code, 79 | self.headers, 80 | self.params, 81 | self.data, 82 | "" if self.response is None else self.response.text, 83 | ) 84 | ) 85 | 86 | 87 | class RestClient(object): 88 | """ 89 | HTTP Client designed for all sorts of trading RESTFul API. 90 | * Reimplement sign function to add signature function. 91 | * Reimplement on_failed function to handle Non-2xx responses. 92 | * Use on_failed parameter in add_request function for individual Non-2xx response handling. 93 | * Reimplement on_error function to handle exception msg. 94 | """ 95 | 96 | def __init__(self) -> None: 97 | """""" 98 | self.url_base: str = "" 99 | self.active: bool = False 100 | 101 | self.queue: Queue = Queue() 102 | self.pool: Pool = None 103 | 104 | self.proxies: dict = None 105 | 106 | def init( 107 | self, 108 | url_base: str, 109 | proxy_host: str = "", 110 | proxy_port: int = 0 111 | ) -> None: 112 | """ 113 | Init rest client with url_base which is the API root address. 114 | """ 115 | self.url_base = url_base 116 | 117 | if proxy_host and proxy_port: 118 | proxy: str = f"http://{proxy_host}:{proxy_port}" 119 | self.proxies = {"http": proxy, "https": proxy} 120 | 121 | def start(self, session_count: int = 5) -> None: 122 | """ 123 | Start rest client with session_count number of threads. 124 | """ 125 | if self.active: 126 | return 127 | 128 | self.active = True 129 | self.pool = Pool(session_count) 130 | self.pool.apply_async(self.run) 131 | 132 | def stop(self) -> None: 133 | """ 134 | Stop rest client immediately. 135 | """ 136 | self.active = False 137 | 138 | def join(self) -> None: 139 | """ 140 | Wait till all requests are processed. 141 | """ 142 | self.queue.join() 143 | 144 | def add_request( 145 | self, 146 | method: str, 147 | path: str, 148 | callback: CALLBACK_TYPE, 149 | params: dict = None, 150 | data: Union[dict, str, bytes] = None, 151 | headers: dict = None, 152 | on_failed: ON_FAILED_TYPE = None, 153 | on_error: ON_ERROR_TYPE = None, 154 | extra: object = None, 155 | ) -> Request: 156 | """ 157 | Add a new request. 158 | :param method: GET, POST, PUT, DELETE, QUERY 159 | :param path: url path for query 160 | :param callback: callback function if 2xx status, type: (dict, Request) 161 | :param params: dict for query string 162 | :param data: Http body. If it is a dict, it will be converted to form-data. Otherwise, it will be converted to bytes. 163 | :param headers: dict for headers 164 | :param on_failed: callback function if Non-2xx status, type, type: (code, dict, Request) 165 | :param on_error: callback function when catching Python exception, type: (etype, evalue, tb, Request) 166 | :param extra: object extra data which can be used when handling callback 167 | :return: Request 168 | """ 169 | request: Request = Request( 170 | method, 171 | path, 172 | params, 173 | data, 174 | headers, 175 | callback, 176 | on_failed, 177 | on_error, 178 | extra, 179 | ) 180 | self.queue.put(request) 181 | return request 182 | 183 | def run(self) -> None: 184 | """""" 185 | try: 186 | session: requests.Session = requests.session() 187 | while self.active: 188 | try: 189 | request: Request = self.queue.get(timeout=1) 190 | try: 191 | self.process_request(request, session) 192 | finally: 193 | self.queue.task_done() 194 | except Empty: 195 | pass 196 | except Exception: 197 | et, ev, tb = sys.exc_info() 198 | self.on_error(et, ev, tb, None) 199 | 200 | def sign(self, request: Request) -> RequestStatus: 201 | """ 202 | This function is called before sending object request out. 203 | Please implement signature method here. 204 | """ 205 | return request 206 | 207 | def on_failed(self, status_code: int, request: Request) -> None: 208 | """ 209 | Default on_failed handler for Non-2xx response. 210 | """ 211 | print("RestClient on failed" + "-" * 10) 212 | print(str(request)) 213 | 214 | def on_error( 215 | self, 216 | exception_type: type, 217 | exception_value: Exception, 218 | tb: TracebackType, 219 | request: Optional[Request], 220 | ) -> None: 221 | """ 222 | Default on_error handler for Python exception. 223 | """ 224 | try: 225 | print("RestClient on error" + "-" * 10) 226 | print(self.exception_detail(exception_type, exception_value, tb, request)) 227 | except Exception: 228 | traceback.print_exc() 229 | 230 | def exception_detail( 231 | self, 232 | exception_type: type, 233 | exception_value: Exception, 234 | tb: TracebackType, 235 | request: Optional[Request], 236 | ) -> str: 237 | text: str = "[{}]: Unhandled RestClient Error:{}\n".format( 238 | datetime.now().isoformat(), exception_type 239 | ) 240 | text += "request:{}\n".format(request) 241 | text += "Exception trace: \n" 242 | text += "".join( 243 | traceback.format_exception(exception_type, exception_value, tb) 244 | ) 245 | return text 246 | 247 | def process_request(self, request: Request, session: requests.Session) -> None: 248 | """ 249 | Sending request to server and get result. 250 | """ 251 | try: 252 | request = self.sign(request) 253 | 254 | url: str = self.make_full_url(request.path) 255 | 256 | response: requests.Response = session.request( 257 | request.method, 258 | url, 259 | headers=request.headers, 260 | params=request.params, 261 | data=request.data, 262 | proxies=self.proxies, 263 | ) 264 | request.response = response 265 | 266 | status_code: int = response.status_code 267 | if status_code // 100 == 2: # 2xx codes are all successful 268 | json_body: dict = None 269 | if status_code != 204: 270 | json_body = response.json() 271 | 272 | request.callback(json_body, request) 273 | request.status = RequestStatus.success 274 | else: 275 | request.status = RequestStatus.failed 276 | 277 | if request.on_failed: 278 | request.on_failed(status_code, request) 279 | else: 280 | self.on_failed(status_code, request) 281 | except Exception: 282 | request.status = RequestStatus.error 283 | t, v, tb = sys.exc_info() 284 | if request.on_error: 285 | request.on_error(t, v, tb, request) 286 | else: 287 | self.on_error(t, v, tb, request) 288 | 289 | def make_full_url(self, path: str) -> str: 290 | """ 291 | Make relative api path into full url. 292 | eg: make_full_url("/get") == "http://xxxxx/get" 293 | """ 294 | url = self.url_base + path 295 | return url 296 | 297 | def request( 298 | self, 299 | method: str, 300 | path: str, 301 | params: dict = None, 302 | data: dict = None, 303 | headers: dict = None, 304 | ) -> requests.Response: 305 | """ 306 | Add a new request. 307 | :param method: GET, POST, PUT, DELETE, QUERY 308 | :param path: url path for query 309 | :param params: dict for query string 310 | :param data: dict for body 311 | :param headers: dict for headers 312 | :return: requests.Response 313 | """ 314 | request: Request = Request( 315 | method, 316 | path, 317 | params, 318 | data, 319 | headers 320 | ) 321 | request = self.sign(request) 322 | 323 | url: str = self.make_full_url(request.path) 324 | 325 | response: requests.Response = requests.request( 326 | request.method, 327 | url, 328 | headers=request.headers, 329 | params=request.params, 330 | data=request.data, 331 | proxies=self.proxies, 332 | ) 333 | return response 334 | -------------------------------------------------------------------------------- /vnpy_evo/rpc/__init__.py: -------------------------------------------------------------------------------- 1 | from vnpy.rpc import * -------------------------------------------------------------------------------- /vnpy_evo/trader/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veighna-global/vnpy_evo/3dc599124d4caa6343b75fea4b6580fe012ce52a/vnpy_evo/trader/__init__.py -------------------------------------------------------------------------------- /vnpy_evo/trader/app.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.app import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from vnpy.trader.constant import ( 4 | Direction, 5 | Offset, 6 | Status, 7 | Product, 8 | OrderType, 9 | OptionType, 10 | Currency, 11 | Interval 12 | ) 13 | 14 | 15 | class Exchange(Enum): 16 | """ 17 | Exchange. 18 | """ 19 | # Crypto 20 | BINANCE = "BINANCE" 21 | OKX = "OKX" 22 | BYBIT = "BYBIT" 23 | BTSE = "BTSE" 24 | DERIBIT = "DERIBIT" 25 | 26 | # Global 27 | OTC = "OTC" 28 | 29 | # Special Function 30 | LOCAL = "LOCAL" # For local generated data 31 | -------------------------------------------------------------------------------- /vnpy_evo/trader/converter.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.converter import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/database.py: -------------------------------------------------------------------------------- 1 | from importlib import import_module 2 | from types import ModuleType 3 | 4 | from vnpy.trader.database import ( 5 | DB_TZ, 6 | convert_tz, 7 | BarOverview, 8 | TickOverview, 9 | BaseDatabase 10 | ) 11 | 12 | from .setting import SETTINGS 13 | 14 | 15 | database: BaseDatabase = None 16 | 17 | 18 | def get_database() -> BaseDatabase: 19 | """""" 20 | # Return database object if already inited 21 | global database 22 | if database: 23 | return database 24 | 25 | # Read database related global setting 26 | database_name: str = SETTINGS["database.name"] 27 | module_name: str = f"vnpy_{database_name}" 28 | 29 | # Try to import database module 30 | try: 31 | module: ModuleType = import_module(module_name) 32 | except ModuleNotFoundError: 33 | print(("Database adapter not found{}, use default DuckDB adpater").format(module_name)) 34 | module: ModuleType = import_module("vnpy_duckdb") 35 | 36 | # Create database object from module 37 | database = module.Database() 38 | return database 39 | -------------------------------------------------------------------------------- /vnpy_evo/trader/datafeed.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.datafeed import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/engine.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from threading import Thread 3 | from queue import Queue, Empty 4 | 5 | import requests 6 | from loguru import logger 7 | 8 | from vnpy.trader.engine import ( 9 | BaseEngine, 10 | EventEngine, 11 | MainEngine as OriginalMainEngine, 12 | OmsEngine, 13 | EmailEngine, 14 | Event, 15 | EVENT_LOG, 16 | Path, 17 | get_folder_path 18 | ) 19 | 20 | from .object import LogData 21 | from .setting import SETTINGS 22 | 23 | 24 | class MainEngine(OriginalMainEngine): 25 | """New main engine of VeighNa Evo""" 26 | 27 | def init_engines(self) -> None: 28 | """ 29 | Init all engines. 30 | """ 31 | self.add_engine(LogEngine) 32 | self.add_engine(OmsEngine) 33 | self.add_engine(EmailEngine) 34 | self.add_engine(TelegramEngine) 35 | 36 | 37 | class LogEngine(BaseEngine): 38 | """Use loguru instead of logging""" 39 | 40 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 41 | """""" 42 | super().__init__(main_engine, event_engine, "log") 43 | 44 | self.active = SETTINGS["log.active"] 45 | self.level: int = SETTINGS["log.level"] 46 | self.format: str = "{time} {level}: {message}" 47 | 48 | if not SETTINGS["log.console"]: 49 | logger.remove() # Remove default stderr output 50 | 51 | if SETTINGS["log.file"]: 52 | today_date: str = datetime.now().strftime("%Y%m%d") 53 | filename: str = f"vt_{today_date}.log" 54 | log_path: Path = get_folder_path("log") 55 | file_path: Path = log_path.joinpath(filename) 56 | 57 | logger.add( 58 | sink=file_path, 59 | level=self.level, 60 | retention="4 weeks" 61 | ) 62 | 63 | self.register_event() 64 | 65 | def register_event(self) -> None: 66 | """Register log event handler""" 67 | self.event_engine.register(EVENT_LOG, self.process_log_event) 68 | 69 | def process_log_event(self, event: Event) -> None: 70 | """Process log event""" 71 | if not self.active: 72 | return 73 | 74 | log: LogData = event.data 75 | logger.log(log.level, log.msg) 76 | 77 | 78 | class TelegramEngine(BaseEngine): 79 | """Telegram message sending engine""" 80 | 81 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 82 | super().__init__(main_engine, event_engine, "telegram") 83 | 84 | self.active: bool = SETTINGS.get("telegram.active", False) 85 | self.token: str = SETTINGS.get("telegram.token", "") 86 | self.chat: str = SETTINGS.get("telegram.chat", "") 87 | self.url: str = f"https://api.telegram.org/bot{self.token}/sendMessage" 88 | 89 | self.proxies: dict[str, str] = {} 90 | proxy: str = SETTINGS.get("telegram.proxy", "") 91 | if proxy: 92 | self.proxies["http"] = proxy 93 | self.proxies["https"] = proxy 94 | 95 | self.thread: Thread = Thread(target=self.run, daemon=True) 96 | self.queue: Queue = Queue() 97 | 98 | if self.active: 99 | self.register_event() 100 | self.thread.start() 101 | 102 | def register_event(self) -> None: 103 | """Register event handler""" 104 | self.event_engine.register(EVENT_LOG, self.process_log_event) 105 | 106 | def process_log_event(self, event: Event) -> None: 107 | """Process log event""" 108 | log: LogData = event.data 109 | 110 | msg = f"{log.time}\t[{log.gateway_name}] {log.msg}" 111 | self.queue.put(msg) 112 | 113 | def close(self) -> None: 114 | """Stop task thread""" 115 | if not self.active: 116 | return 117 | 118 | self.active = False 119 | self.thread.join() 120 | 121 | def run(self) -> None: 122 | """Task thread loop""" 123 | while self.active: 124 | try: 125 | msg: str = self.queue.get(block=True, timeout=1) 126 | self.send_msg(msg) 127 | except Empty: 128 | pass 129 | 130 | def send_msg(self, msg: str) -> dict: 131 | """Sending message""" 132 | data: dict = { 133 | "chat_id": self.chat, 134 | "text": msg 135 | } 136 | 137 | # 发送请求 138 | try: 139 | r: requests.Response = requests.post(self.url, data=data) 140 | return r.json() 141 | except Exception: 142 | return None 143 | -------------------------------------------------------------------------------- /vnpy_evo/trader/event.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.event import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/gateway.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.gateway import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/object.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.object import ( # noqa 2 | BaseData, 3 | TickData, 4 | BarData, 5 | OrderData, 6 | TradeData, 7 | PositionData, 8 | AccountData, 9 | LogData, 10 | ContractData, 11 | QuoteData, 12 | SubscribeRequest, 13 | OrderRequest, 14 | CancelRequest, 15 | HistoryRequest, 16 | QuoteRequest 17 | ) 18 | -------------------------------------------------------------------------------- /vnpy_evo/trader/optimize.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.optimize import * 2 | -------------------------------------------------------------------------------- /vnpy_evo/trader/setting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Global setting of the trading platform. 3 | """ 4 | 5 | from logging import INFO 6 | from tzlocal import get_localzone_name 7 | 8 | from .utility import load_json 9 | 10 | 11 | SETTINGS: dict[str, object] = { 12 | "font.family": "", 13 | "font.size": 12, 14 | 15 | "log.active": True, 16 | "log.level": INFO, 17 | "log.console": True, 18 | "log.file": True, 19 | 20 | "email.server": "", 21 | "email.port": 0, 22 | "email.username": "", 23 | "email.password": "", 24 | "email.sender": "", 25 | "email.receiver": "", 26 | 27 | "datafeed.name": "", 28 | "datafeed.username": "", 29 | "datafeed.password": "", 30 | 31 | "database.timezone": get_localzone_name(), 32 | "database.name": "duckdb", 33 | "database.database": "database.duckdb", 34 | "database.host": "", 35 | "database.port": 0, 36 | "database.user": "", 37 | "database.password": "", 38 | 39 | "telegram.active": False, 40 | "telegram.token": "", 41 | "telegram.chat": 0, 42 | "telegram.proxy": "", 43 | } 44 | 45 | 46 | # Load global setting from json file. 47 | SETTING_FILENAME: str = "vt_setting.json" 48 | SETTINGS.update(load_json(SETTING_FILENAME)) 49 | 50 | 51 | def get_settings(prefix: str = "") -> dict[str, object]: 52 | prefix_length: int = len(prefix) 53 | settings = {k[prefix_length:]: v for k, v in SETTINGS.items() if k.startswith(prefix)} 54 | return settings 55 | -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .qt import QtCore, QtWidgets, QtGui, Qt, create_qapp 2 | from .mainwindow import MainWindow 3 | -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/ico/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veighna-global/vnpy_evo/3dc599124d4caa6343b75fea4b6580fe012ce52a/vnpy_evo/trader/ui/ico/__init__.py -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/ico/veighna.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veighna-global/vnpy_evo/3dc599124d4caa6343b75fea4b6580fe012ce52a/vnpy_evo/trader/ui/ico/veighna.ico -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/mainwindow.py: -------------------------------------------------------------------------------- 1 | from types import ModuleType 2 | import webbrowser 3 | from functools import partial 4 | from importlib import import_module 5 | 6 | from typing import Callable, Type 7 | 8 | from qfluentwidgets import ( 9 | FluentWindow, MessageBox, 10 | FluentIcon as FIF, 11 | PushButton, RoundMenu, 12 | Action, NavigationItemPosition, 13 | ) 14 | 15 | from vnpy.event import EventEngine 16 | 17 | import vnpy_evo 18 | from .qt import QtCore, QtGui, QtWidgets 19 | from .widget import ( 20 | ConnectDialog, 21 | TradingWidget, 22 | AboutDialog, 23 | GlobalDialog, 24 | PivotWidgdet 25 | ) 26 | from .monitor import ( 27 | TickMonitor, 28 | OrderMonitor, 29 | TradeMonitor, 30 | PositionMonitor, 31 | AccountMonitor, 32 | LogMonitor, 33 | ActiveOrderMonitor, 34 | ContractManager 35 | ) 36 | from ..app import BaseApp 37 | from ..engine import MainEngine 38 | from ..utility import get_icon_path, TRADER_DIR 39 | 40 | 41 | class MainWindow(FluentWindow): 42 | """ 43 | Main window of the trading platform. 44 | """ 45 | 46 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 47 | """""" 48 | super().__init__() 49 | 50 | self.main_engine: MainEngine = main_engine 51 | self.event_engine: EventEngine = event_engine 52 | 53 | self.window_title: str = f"VeighNa Evo - {vnpy_evo.__version__} [{TRADER_DIR}]" 54 | 55 | self.app_widgets: dict[str, QtWidgets.QWidget] = {} 56 | 57 | self.init_ui() 58 | 59 | def init_ui(self) -> None: 60 | """""" 61 | self.setWindowTitle(self.window_title) 62 | 63 | icon: QtGui.QIcon = QtGui.QIcon(get_icon_path(__file__, "veighna.ico")) 64 | self.setWindowIcon(icon) 65 | 66 | self.init_widgets() 67 | self.init_navigation() 68 | 69 | def init_widgets(self) -> None: 70 | """""" 71 | self.home_widget = HomeWidget(self.main_engine, self.event_engine) 72 | self.home_widget.setObjectName("home") 73 | 74 | self.contract_manager = ContractManager(self.main_engine, self.event_engine) 75 | self.contract_manager.setObjectName("contract") 76 | 77 | all_apps: list[BaseApp] = self.main_engine.get_all_apps() 78 | for app in all_apps: 79 | ui_module: ModuleType = import_module(app.app_module + ".ui") 80 | widget_class: Type = getattr(ui_module, app.widget_name) 81 | widget: QtWidgets.QWidget = widget_class(self.main_engine, self.event_engine) 82 | widget.setObjectName(app.display_name) 83 | self.app_widgets[app.display_name] = widget 84 | 85 | def init_navigation(self) -> None: 86 | """""" 87 | self.addSubInterface(self.home_widget, FIF.HOME, "Home") 88 | self.addSubInterface(self.contract_manager, FIF.SEARCH, "Find contract") 89 | 90 | for name, widget in self.app_widgets.items(): 91 | self.addSubInterface(widget, FIF.ROBOT, name) 92 | 93 | self.navigationInterface.addItem( 94 | routeKey="froum", 95 | icon=FIF.HELP, 96 | text="Community forum", 97 | onClick=self.open_forum, 98 | selectable=False, 99 | position=NavigationItemPosition.BOTTOM 100 | ) 101 | 102 | self.navigationInterface.addItem( 103 | routeKey="github", 104 | icon=FIF.GITHUB, 105 | text="Github", 106 | onClick=self.open_github, 107 | selectable=False, 108 | position=NavigationItemPosition.BOTTOM 109 | ) 110 | 111 | self.navigationInterface.addItem( 112 | routeKey="setting", 113 | icon=FIF.SETTING, 114 | text="Settings", 115 | onClick=self.edit_global_setting, 116 | selectable=False, 117 | position=NavigationItemPosition.BOTTOM 118 | ) 119 | 120 | self.navigationInterface.addItem( 121 | routeKey="about", 122 | icon=FIF.INFO, 123 | text="About", 124 | onClick=self.open_about, 125 | selectable=False, 126 | position=NavigationItemPosition.BOTTOM 127 | ) 128 | 129 | def closeEvent(self, event: QtGui.QCloseEvent) -> None: 130 | """ 131 | Call main engine close function before exit. 132 | """ 133 | msgbox: MessageBox = MessageBox("Notice", "Do you confirm exit?", self.window()) 134 | reply: int = msgbox.exec() 135 | 136 | if reply: 137 | self.main_engine.close() 138 | 139 | event.accept() 140 | else: 141 | event.ignore() 142 | 143 | def open_forum(self) -> None: 144 | """ 145 | """ 146 | webbrowser.open("https://www.vnpy.com/forum/") 147 | 148 | def open_github(self) -> None: 149 | """ 150 | """ 151 | webbrowser.open("https://github.com/veighna-global/vnpy_evo") 152 | 153 | def edit_global_setting(self) -> None: 154 | """ 155 | """ 156 | dialog: GlobalDialog = GlobalDialog(self) 157 | dialog.exec() 158 | 159 | def open_about(self) -> None: 160 | """ 161 | Open contract manager. 162 | """ 163 | dialog: AboutDialog = AboutDialog(self) 164 | dialog.exec() 165 | 166 | 167 | class HomeWidget(QtWidgets.QWidget): 168 | """""" 169 | 170 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 171 | """""" 172 | super().__init__() 173 | 174 | self.main_engine: MainEngine = main_engine 175 | self.event_engine: EventEngine = event_engine 176 | 177 | self.init_ui() 178 | self.init_menu() 179 | 180 | def init_ui(self) -> None: 181 | """""" 182 | # Create widgets 183 | self.trading_widget = TradingWidget(self.main_engine, self.event_engine) 184 | self.tick_monitor = TickMonitor(self.main_engine, self.event_engine) 185 | self.order_monitor = OrderMonitor(self.main_engine, self.event_engine) 186 | self.active_monitor = ActiveOrderMonitor(self.main_engine, self.event_engine) 187 | self.trade_monitor = TradeMonitor(self.main_engine, self.event_engine) 188 | self.position_monitor = PositionMonitor(self.main_engine, self.event_engine) 189 | self.account_monitor = AccountMonitor(self.main_engine, self.event_engine) 190 | self.log_monitor = LogMonitor(self.main_engine, self.event_engine) 191 | 192 | self.menu: RoundMenu = RoundMenu(parent=self) 193 | 194 | self.menu_button: PushButton = PushButton("Connect Gateway") 195 | self.menu_button.clicked.connect(self.show_menu) 196 | 197 | # Set layout 198 | mid_pivot = PivotWidgdet(self) 199 | mid_pivot.add_widget(self.active_monitor, "Open Orders") 200 | mid_pivot.add_widget(self.order_monitor, "Order History") 201 | 202 | bottom_pivot = PivotWidgdet(self) 203 | bottom_pivot.add_widget(self.log_monitor, "Log") 204 | bottom_pivot.add_widget(self.trade_monitor, "Trade History") 205 | bottom_pivot.add_widget(self.position_monitor, "Positions") 206 | bottom_pivot.add_widget(self.account_monitor, "Assets") 207 | 208 | vbox1 = QtWidgets.QVBoxLayout() 209 | vbox1.addWidget(self.tick_monitor) 210 | vbox1.addWidget(mid_pivot) 211 | vbox1.addWidget(bottom_pivot) 212 | 213 | vbox2 = QtWidgets.QVBoxLayout() 214 | vbox2.addWidget(self.trading_widget) 215 | vbox2.addWidget(self.menu_button) 216 | 217 | hbox = QtWidgets.QHBoxLayout() 218 | hbox.addLayout(vbox2) 219 | hbox.addLayout(vbox1) 220 | 221 | self.setLayout(hbox) 222 | 223 | # Connect signal 224 | self.tick_monitor.itemDoubleClicked.connect(self.trading_widget.update_with_cell) 225 | self.position_monitor.itemDoubleClicked.connect(self.trading_widget.update_with_cell) 226 | 227 | def init_menu(self) -> None: 228 | """""" 229 | gateway_names: list = self.main_engine.get_all_gateway_names() 230 | for name in gateway_names: 231 | func: Callable = partial(self.connect_gateway, name) 232 | 233 | action = Action(f"Connect {name}") 234 | action.triggered.connect(func) 235 | 236 | self.menu.addAction(action) 237 | 238 | def show_menu(self) -> None: 239 | """""" 240 | pos = self.menu_button.mapToGlobal(QtCore.QPoint(self.menu_button.width() + 5, 0)) 241 | self.menu.exec(pos, ani=True) 242 | 243 | def connect_gateway(self, gateway_name: str) -> None: 244 | """ 245 | Open connect dialog for gateway connection. 246 | """ 247 | dialog: ConnectDialog = ConnectDialog(self.main_engine, gateway_name, self) 248 | dialog.exec() 249 | -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/monitor.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic widgets for UI. 3 | """ 4 | 5 | import csv 6 | from datetime import datetime 7 | from enum import Enum 8 | from tzlocal import get_localzone_name 9 | 10 | from qfluentwidgets import TableWidget, PushButton, LineEdit 11 | 12 | from vnpy.trader.locale import _ 13 | from .qt import QtCore, QtGui, QtWidgets 14 | from ..constant import Direction 15 | from ..engine import MainEngine, Event, EventEngine 16 | from ..event import ( 17 | EVENT_QUOTE, 18 | EVENT_TICK, 19 | EVENT_TRADE, 20 | EVENT_ORDER, 21 | EVENT_POSITION, 22 | EVENT_ACCOUNT, 23 | EVENT_LOG 24 | ) 25 | from ..object import ( 26 | CancelRequest, 27 | ContractData, 28 | OrderData, 29 | QuoteData, 30 | ) 31 | from ..utility import ZoneInfo 32 | 33 | 34 | COLOR_LONG = QtGui.QColor("red") 35 | COLOR_SHORT = QtGui.QColor("green") 36 | COLOR_BID = QtGui.QColor("red") 37 | COLOR_ASK = QtGui.QColor("green") 38 | COLOR_BLACK = QtGui.QColor("black") 39 | 40 | 41 | class BaseCell(QtWidgets.QTableWidgetItem): 42 | """ 43 | General cell used in tablewidgets. 44 | """ 45 | 46 | def __init__(self, content: object, data: object) -> None: 47 | """""" 48 | super().__init__() 49 | self.setTextAlignment(QtCore.Qt.AlignCenter) 50 | self.set_content(content, data) 51 | 52 | def set_content(self, content: object, data: object) -> None: 53 | """ 54 | Set text content. 55 | """ 56 | self.setText(str(content)) 57 | self._data = data 58 | 59 | def get_data(self) -> object: 60 | """ 61 | Get data object. 62 | """ 63 | return self._data 64 | 65 | 66 | class EnumCell(BaseCell): 67 | """ 68 | Cell used for showing enum data. 69 | """ 70 | 71 | def __init__(self, content: str, data: object) -> None: 72 | """""" 73 | super().__init__(content, data) 74 | 75 | def set_content(self, content: object, data: object) -> None: 76 | """ 77 | Set text using enum.constant.value. 78 | """ 79 | if content: 80 | super().set_content(content.value, data) 81 | 82 | 83 | class DirectionCell(EnumCell): 84 | """ 85 | Cell used for showing direction data. 86 | """ 87 | 88 | def __init__(self, content: str, data: object) -> None: 89 | """""" 90 | super().__init__(content, data) 91 | 92 | def set_content(self, content: object, data: object) -> None: 93 | """ 94 | Cell color is set according to direction. 95 | """ 96 | super().set_content(content, data) 97 | 98 | if content is Direction.SHORT: 99 | self.setForeground(COLOR_SHORT) 100 | else: 101 | self.setForeground(COLOR_LONG) 102 | 103 | 104 | class BidCell(BaseCell): 105 | """ 106 | Cell used for showing bid price and volume. 107 | """ 108 | 109 | def __init__(self, content: object, data: object) -> None: 110 | """""" 111 | super().__init__(content, data) 112 | 113 | self.setForeground(COLOR_BID) 114 | 115 | 116 | class AskCell(BaseCell): 117 | """ 118 | Cell used for showing ask price and volume. 119 | """ 120 | 121 | def __init__(self, content: object, data: object) -> None: 122 | """""" 123 | super().__init__(content, data) 124 | 125 | self.setForeground(COLOR_ASK) 126 | 127 | 128 | class PnlCell(BaseCell): 129 | """ 130 | Cell used for showing pnl data. 131 | """ 132 | 133 | def __init__(self, content: object, data: object) -> None: 134 | """""" 135 | super().__init__(content, data) 136 | 137 | def set_content(self, content: object, data: object) -> None: 138 | """ 139 | Cell color is set based on whether pnl is 140 | positive or negative. 141 | """ 142 | super().set_content(content, data) 143 | 144 | if str(content).startswith("-"): 145 | self.setForeground(COLOR_SHORT) 146 | else: 147 | self.setForeground(COLOR_LONG) 148 | 149 | 150 | class TimeCell(BaseCell): 151 | """ 152 | Cell used for showing time string from datetime object. 153 | """ 154 | 155 | local_tz = ZoneInfo(get_localzone_name()) 156 | 157 | def __init__(self, content: object, data: object) -> None: 158 | """""" 159 | super().__init__(content, data) 160 | 161 | def set_content(self, content: object, data: object) -> None: 162 | """""" 163 | if content is None: 164 | return 165 | 166 | content: datetime = content.astimezone(self.local_tz) 167 | timestamp: str = content.strftime("%H:%M:%S") 168 | 169 | millisecond: int = int(content.microsecond / 1000) 170 | if millisecond: 171 | timestamp = f"{timestamp}.{millisecond}" 172 | else: 173 | timestamp = f"{timestamp}.000" 174 | 175 | self.setText(timestamp) 176 | self._data = data 177 | 178 | 179 | class DateCell(BaseCell): 180 | """ 181 | Cell used for showing date string from datetime object. 182 | """ 183 | 184 | def __init__(self, content: object, data: object) -> None: 185 | """""" 186 | super().__init__(content, data) 187 | 188 | def set_content(self, content: object, data: object) -> None: 189 | """""" 190 | if content is None: 191 | return 192 | 193 | self.setText(content.strftime("%Y-%m-%d")) 194 | self._data = data 195 | 196 | 197 | class MsgCell(BaseCell): 198 | """ 199 | Cell used for showing msg data. 200 | """ 201 | 202 | def __init__(self, content: str, data: object) -> None: 203 | """""" 204 | super().__init__(content, data) 205 | self.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 206 | 207 | 208 | class BaseMonitor(TableWidget): 209 | """ 210 | Monitor data update. 211 | """ 212 | 213 | event_type: str = "" 214 | data_key: str = "" 215 | sorting: bool = False 216 | headers: dict = {} 217 | 218 | signal: QtCore.Signal = QtCore.Signal(Event) 219 | 220 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 221 | """""" 222 | super().__init__() 223 | 224 | self.main_engine: MainEngine = main_engine 225 | self.event_engine: EventEngine = event_engine 226 | self.cells: dict[str, dict] = {} 227 | 228 | self.init_ui() 229 | self.load_setting() 230 | self.register_event() 231 | 232 | def init_ui(self) -> None: 233 | """""" 234 | self.init_table() 235 | self.init_menu() 236 | 237 | def init_table(self) -> None: 238 | """ 239 | Initialize table. 240 | """ 241 | self.setColumnCount(len(self.headers)) 242 | 243 | labels: list = [d["display"] for d in self.headers.values()] 244 | self.setHorizontalHeaderLabels(labels) 245 | 246 | self.verticalHeader().setVisible(False) 247 | self.setEditTriggers(self.EditTrigger.NoEditTriggers) 248 | self.setAlternatingRowColors(True) 249 | self.setSortingEnabled(self.sorting) 250 | 251 | def init_menu(self) -> None: 252 | """ 253 | Create right click menu. 254 | """ 255 | self.menu: QtWidgets.QMenu = QtWidgets.QMenu(self) 256 | 257 | resize_action: QtGui.QAction = QtWidgets.QAction(_("调整列宽"), self) 258 | resize_action.triggered.connect(self.resize_columns) 259 | self.menu.addAction(resize_action) 260 | 261 | save_action: QtGui.QAction = QtWidgets.QAction(_("保存数据"), self) 262 | save_action.triggered.connect(self.save_csv) 263 | self.menu.addAction(save_action) 264 | 265 | def register_event(self) -> None: 266 | """ 267 | Register event handler into event engine. 268 | """ 269 | if self.event_type: 270 | self.signal.connect(self.process_event) 271 | self.event_engine.register(self.event_type, self.signal.emit) 272 | 273 | def process_event(self, event: Event) -> None: 274 | """ 275 | Process new data from event and update into table. 276 | """ 277 | # Disable sorting to prevent unwanted error. 278 | if self.sorting: 279 | self.setSortingEnabled(False) 280 | 281 | # Update data into table. 282 | data = event.data 283 | 284 | if not self.data_key: 285 | self.insert_new_row(data) 286 | else: 287 | key: str = data.__getattribute__(self.data_key) 288 | 289 | if key in self.cells: 290 | self.update_old_row(data) 291 | else: 292 | self.insert_new_row(data) 293 | 294 | # Enable sorting 295 | if self.sorting: 296 | self.setSortingEnabled(True) 297 | 298 | def insert_new_row(self, data: object) -> None: 299 | """ 300 | Insert a new row at the top of table. 301 | """ 302 | self.insertRow(0) 303 | 304 | row_cells: dict = {} 305 | for column, header in enumerate(self.headers.keys()): 306 | setting: dict = self.headers[header] 307 | 308 | content = data.__getattribute__(header) 309 | cell: QtWidgets.QTableWidgetItem = setting["cell"](content, data) 310 | self.setItem(0, column, cell) 311 | 312 | if setting["update"]: 313 | row_cells[header] = cell 314 | 315 | if self.data_key: 316 | key: str = data.__getattribute__(self.data_key) 317 | self.cells[key] = row_cells 318 | 319 | def update_old_row(self, data: object) -> None: 320 | """ 321 | Update an old row in table. 322 | """ 323 | key: str = data.__getattribute__(self.data_key) 324 | row_cells = self.cells[key] 325 | 326 | for header, cell in row_cells.items(): 327 | content = data.__getattribute__(header) 328 | cell.set_content(content, data) 329 | 330 | def resize_columns(self) -> None: 331 | """ 332 | Resize all columns according to contents. 333 | """ 334 | self.horizontalHeader().resizeSections(QtWidgets.QHeaderView.ResizeToContents) 335 | 336 | def save_csv(self) -> None: 337 | """ 338 | Save table data into a csv file 339 | """ 340 | path, __ = QtWidgets.QFileDialog.getSaveFileName( 341 | self, _("保存数据"), "", "CSV(*.csv)") 342 | 343 | if not path: 344 | return 345 | 346 | with open(path, "w") as f: 347 | writer = csv.writer(f, lineterminator="\n") 348 | 349 | headers: list = [d["display"] for d in self.headers.values()] 350 | writer.writerow(headers) 351 | 352 | for row in range(self.rowCount()): 353 | if self.isRowHidden(row): 354 | continue 355 | 356 | row_data: list = [] 357 | for column in range(self.columnCount()): 358 | item: QtWidgets.QTableWidgetItem = self.item(row, column) 359 | if item: 360 | row_data.append(str(item.text())) 361 | else: 362 | row_data.append("") 363 | writer.writerow(row_data) 364 | 365 | def contextMenuEvent(self, event: QtGui.QContextMenuEvent) -> None: 366 | """ 367 | Show menu with right click. 368 | """ 369 | self.menu.popup(QtGui.QCursor.pos()) 370 | 371 | def save_setting(self) -> None: 372 | """""" 373 | settings: QtCore.QSettings = QtCore.QSettings(self.__class__.__name__, "custom") 374 | settings.setValue("column_state", self.horizontalHeader().saveState()) 375 | 376 | def load_setting(self) -> None: 377 | """""" 378 | settings: QtCore.QSettings = QtCore.QSettings(self.__class__.__name__, "custom") 379 | column_state = settings.value("column_state") 380 | 381 | if isinstance(column_state, QtCore.QByteArray): 382 | self.horizontalHeader().restoreState(column_state) 383 | self.horizontalHeader().setSortIndicator(-1, QtCore.Qt.AscendingOrder) 384 | 385 | 386 | class TickMonitor(BaseMonitor): 387 | """ 388 | Monitor for tick data. 389 | """ 390 | 391 | event_type: str = EVENT_TICK 392 | data_key: str = "vt_symbol" 393 | sorting: bool = True 394 | 395 | headers: dict = { 396 | "symbol": {"display": _("代码"), "cell": BaseCell, "update": False}, 397 | "exchange": {"display": _("交易所"), "cell": EnumCell, "update": False}, 398 | "name": {"display": _("名称"), "cell": BaseCell, "update": True}, 399 | "last_price": {"display": _("最新价"), "cell": BaseCell, "update": True}, 400 | "volume": {"display": _("成交量"), "cell": BaseCell, "update": True}, 401 | "open_price": {"display": _("开盘价"), "cell": BaseCell, "update": True}, 402 | "high_price": {"display": _("最高价"), "cell": BaseCell, "update": True}, 403 | "low_price": {"display": _("最低价"), "cell": BaseCell, "update": True}, 404 | "bid_price_1": {"display": _("买1价"), "cell": BidCell, "update": True}, 405 | "bid_volume_1": {"display": _("买1量"), "cell": BidCell, "update": True}, 406 | "ask_price_1": {"display": _("卖1价"), "cell": AskCell, "update": True}, 407 | "ask_volume_1": {"display": _("卖1量"), "cell": AskCell, "update": True}, 408 | "datetime": {"display": _("时间"), "cell": TimeCell, "update": True}, 409 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 410 | } 411 | 412 | 413 | class LogMonitor(BaseMonitor): 414 | """ 415 | Monitor for log data. 416 | """ 417 | 418 | event_type: str = EVENT_LOG 419 | data_key: str = "" 420 | sorting: bool = False 421 | 422 | headers: dict = { 423 | "time": {"display": _("时间"), "cell": TimeCell, "update": False}, 424 | "msg": {"display": _("信息"), "cell": MsgCell, "update": False}, 425 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 426 | } 427 | 428 | 429 | class TradeMonitor(BaseMonitor): 430 | """ 431 | Monitor for trade data. 432 | """ 433 | 434 | event_type: str = EVENT_TRADE 435 | data_key: str = "" 436 | sorting: bool = True 437 | 438 | headers: dict = { 439 | "tradeid": {"display": _("成交号"), "cell": BaseCell, "update": False}, 440 | "orderid": {"display": _("委托号"), "cell": BaseCell, "update": False}, 441 | "symbol": {"display": _("代码"), "cell": BaseCell, "update": False}, 442 | "exchange": {"display": _("交易所"), "cell": EnumCell, "update": False}, 443 | "direction": {"display": _("方向"), "cell": DirectionCell, "update": False}, 444 | "offset": {"display": _("开平"), "cell": EnumCell, "update": False}, 445 | "price": {"display": _("价格"), "cell": BaseCell, "update": False}, 446 | "volume": {"display": _("数量"), "cell": BaseCell, "update": False}, 447 | "datetime": {"display": _("时间"), "cell": TimeCell, "update": False}, 448 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 449 | } 450 | 451 | 452 | class OrderMonitor(BaseMonitor): 453 | """ 454 | Monitor for order data. 455 | """ 456 | 457 | event_type: str = EVENT_ORDER 458 | data_key: str = "vt_orderid" 459 | sorting: bool = True 460 | 461 | headers: dict = { 462 | "orderid": {"display": _("委托号"), "cell": BaseCell, "update": False}, 463 | "reference": {"display": _("来源"), "cell": BaseCell, "update": False}, 464 | "symbol": {"display": _("代码"), "cell": BaseCell, "update": False}, 465 | "exchange": {"display": _("交易所"), "cell": EnumCell, "update": False}, 466 | "type": {"display": _("类型"), "cell": EnumCell, "update": False}, 467 | "direction": {"display": _("方向"), "cell": DirectionCell, "update": False}, 468 | "offset": {"display": _("开平"), "cell": EnumCell, "update": False}, 469 | "price": {"display": _("价格"), "cell": BaseCell, "update": False}, 470 | "volume": {"display": _("总数量"), "cell": BaseCell, "update": True}, 471 | "traded": {"display": _("已成交"), "cell": BaseCell, "update": True}, 472 | "status": {"display": _("状态"), "cell": EnumCell, "update": True}, 473 | "datetime": {"display": _("时间"), "cell": TimeCell, "update": True}, 474 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 475 | } 476 | 477 | def init_ui(self) -> None: 478 | """ 479 | Connect signal. 480 | """ 481 | super().init_ui() 482 | 483 | self.setToolTip(_("双击单元格撤单")) 484 | self.itemDoubleClicked.connect(self.cancel_order) 485 | 486 | def cancel_order(self, cell: BaseCell) -> None: 487 | """ 488 | Cancel order if cell double clicked. 489 | """ 490 | order: OrderData = cell.get_data() 491 | req: CancelRequest = order.create_cancel_request() 492 | self.main_engine.cancel_order(req, order.gateway_name) 493 | 494 | 495 | class PositionMonitor(BaseMonitor): 496 | """ 497 | Monitor for position data. 498 | """ 499 | 500 | event_type: str = EVENT_POSITION 501 | data_key: str = "vt_positionid" 502 | sorting: bool = True 503 | 504 | headers: dict = { 505 | "symbol": {"display": _("代码"), "cell": BaseCell, "update": False}, 506 | "exchange": {"display": _("交易所"), "cell": EnumCell, "update": False}, 507 | "direction": {"display": _("方向"), "cell": DirectionCell, "update": False}, 508 | "volume": {"display": _("数量"), "cell": BaseCell, "update": True}, 509 | "yd_volume": {"display": _("昨仓"), "cell": BaseCell, "update": True}, 510 | "frozen": {"display": _("冻结"), "cell": BaseCell, "update": True}, 511 | "price": {"display": _("均价"), "cell": BaseCell, "update": True}, 512 | "pnl": {"display": _("盈亏"), "cell": PnlCell, "update": True}, 513 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 514 | } 515 | 516 | 517 | class AccountMonitor(BaseMonitor): 518 | """ 519 | Monitor for account data. 520 | """ 521 | 522 | event_type: str = EVENT_ACCOUNT 523 | data_key: str = "vt_accountid" 524 | sorting: bool = True 525 | 526 | headers: dict = { 527 | "accountid": {"display": _("账号"), "cell": BaseCell, "update": False}, 528 | "balance": {"display": _("余额"), "cell": BaseCell, "update": True}, 529 | "frozen": {"display": _("冻结"), "cell": BaseCell, "update": True}, 530 | "available": {"display": _("可用"), "cell": BaseCell, "update": True}, 531 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 532 | } 533 | 534 | 535 | class QuoteMonitor(BaseMonitor): 536 | """ 537 | Monitor for quote data. 538 | """ 539 | 540 | event_type: str = EVENT_QUOTE 541 | data_key: str = "vt_quoteid" 542 | sorting: bool = True 543 | 544 | headers: dict = { 545 | "quoteid": {"display": _("报价号"), "cell": BaseCell, "update": False}, 546 | "reference": {"display": _("来源"), "cell": BaseCell, "update": False}, 547 | "symbol": {"display": _("代码"), "cell": BaseCell, "update": False}, 548 | "exchange": {"display": _("交易所"), "cell": EnumCell, "update": False}, 549 | "bid_offset": {"display": _("买开平"), "cell": EnumCell, "update": False}, 550 | "bid_volume": {"display": _("买量"), "cell": BidCell, "update": False}, 551 | "bid_price": {"display": _("买价"), "cell": BidCell, "update": False}, 552 | "ask_price": {"display": _("卖价"), "cell": AskCell, "update": False}, 553 | "ask_volume": {"display": _("卖量"), "cell": AskCell, "update": False}, 554 | "ask_offset": {"display": _("卖开平"), "cell": EnumCell, "update": False}, 555 | "status": {"display": _("状态"), "cell": EnumCell, "update": True}, 556 | "datetime": {"display": _("时间"), "cell": TimeCell, "update": True}, 557 | "gateway_name": {"display": _("接口"), "cell": BaseCell, "update": False}, 558 | } 559 | 560 | def init_ui(self): 561 | """ 562 | Connect signal. 563 | """ 564 | super().init_ui() 565 | 566 | self.setToolTip(_("双击单元格撤销报价")) 567 | self.itemDoubleClicked.connect(self.cancel_quote) 568 | 569 | def cancel_quote(self, cell: BaseCell) -> None: 570 | """ 571 | Cancel quote if cell double clicked. 572 | """ 573 | quote: QuoteData = cell.get_data() 574 | req: CancelRequest = quote.create_cancel_request() 575 | self.main_engine.cancel_quote(req, quote.gateway_name) 576 | 577 | 578 | class ActiveOrderMonitor(OrderMonitor): 579 | """ 580 | Monitor which shows active order only. 581 | """ 582 | 583 | def process_event(self, event) -> None: 584 | """ 585 | Hides the row if order is not active. 586 | """ 587 | super().process_event(event) 588 | 589 | order: OrderData = event.data 590 | row_cells: dict = self.cells[order.vt_orderid] 591 | row: int = self.row(row_cells["volume"]) 592 | 593 | if order.is_active(): 594 | self.showRow(row) 595 | else: 596 | self.hideRow(row) 597 | 598 | 599 | class ContractManager(QtWidgets.QWidget): 600 | """ 601 | Query contract data available to trade in system. 602 | """ 603 | 604 | headers: dict[str, str] = { 605 | "vt_symbol": _("本地代码"), 606 | "symbol": _("代码"), 607 | "exchange": _("交易所"), 608 | "name": _("名称"), 609 | "product": _("合约分类"), 610 | "size": _("合约乘数"), 611 | "pricetick": _("价格跳动"), 612 | "min_volume": _("最小委托量"), 613 | "option_portfolio": _("期权产品"), 614 | "option_expiry": _("期权到期日"), 615 | "option_strike": _("期权行权价"), 616 | "option_type": _("期权类型"), 617 | "gateway_name": _("交易接口"), 618 | } 619 | 620 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 621 | super().__init__() 622 | 623 | self.main_engine: MainEngine = main_engine 624 | self.event_engine: EventEngine = event_engine 625 | 626 | self.init_ui() 627 | 628 | def init_ui(self) -> None: 629 | """""" 630 | self.setWindowTitle(_("合约查询")) 631 | self.resize(1000, 600) 632 | 633 | self.filter_line: LineEdit = LineEdit() 634 | self.filter_line.setPlaceholderText(_("输入合约代码或者交易所,留空则查询所有合约")) 635 | 636 | self.button_show: PushButton = PushButton(_("查询")) 637 | self.button_show.clicked.connect(self.show_contracts) 638 | 639 | labels: list = [] 640 | for name, display in self.headers.items(): 641 | label: str = f"{display}" 642 | labels.append(label) 643 | 644 | self.contract_table: TableWidget = TableWidget() 645 | self.contract_table.setColumnCount(len(self.headers)) 646 | self.contract_table.setHorizontalHeaderLabels(labels) 647 | self.contract_table.verticalHeader().setVisible(False) 648 | self.contract_table.setEditTriggers(self.contract_table.EditTrigger.NoEditTriggers) 649 | self.contract_table.setAlternatingRowColors(True) 650 | 651 | hbox: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() 652 | hbox.addWidget(self.filter_line) 653 | hbox.addWidget(self.button_show) 654 | 655 | vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() 656 | vbox.addLayout(hbox) 657 | vbox.addWidget(self.contract_table) 658 | 659 | self.setLayout(vbox) 660 | 661 | def show_contracts(self) -> None: 662 | """ 663 | Show contracts by symbol 664 | """ 665 | flt: str = str(self.filter_line.text()) 666 | 667 | all_contracts: list[ContractData] = self.main_engine.get_all_contracts() 668 | if flt: 669 | contracts: list[ContractData] = [ 670 | contract for contract in all_contracts if flt in contract.vt_symbol 671 | ] 672 | else: 673 | contracts: list[ContractData] = all_contracts 674 | 675 | self.contract_table.clearContents() 676 | self.contract_table.setRowCount(len(contracts)) 677 | 678 | for row, contract in enumerate(contracts): 679 | for column, name in enumerate(self.headers.keys()): 680 | value: object = getattr(contract, name) 681 | 682 | if value in {None, 0, 0.0}: 683 | value = "" 684 | 685 | if isinstance(value, Enum): 686 | cell: EnumCell = EnumCell(value, contract) 687 | elif isinstance(value, datetime): 688 | cell: DateCell = DateCell(value, contract) 689 | else: 690 | cell: BaseCell = BaseCell(value, contract) 691 | self.contract_table.setItem(row, column, cell) 692 | 693 | self.contract_table.resizeColumnsToContents() 694 | -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/qt.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import platform 3 | import sys 4 | import traceback 5 | import webbrowser 6 | import types 7 | import threading 8 | 9 | from PySide6 import QtGui, QtWidgets, QtCore 10 | from qfluentwidgets import ( 11 | setTheme, Theme, 12 | PushButton, TextEdit, SubtitleLabel 13 | ) 14 | 15 | from ..setting import SETTINGS 16 | from ..utility import get_icon_path 17 | 18 | 19 | Qt = QtCore.Qt 20 | QtCore.pyqtSignal = QtCore.Signal 21 | QtWidgets.QAction = QtGui.QAction 22 | QtCore.QDate.toPyDate = QtCore.QDate.toPython 23 | QtCore.QDateTime.toPyDate = QtCore.QDateTime.toPython 24 | 25 | 26 | def create_qapp(app_name: str = "VeighNa Evo") -> QtWidgets.QApplication: 27 | """ 28 | Create Qt Application. 29 | """ 30 | qapp: QtWidgets.QApplication = QtWidgets.QApplication(sys.argv) 31 | setTheme(Theme.LIGHT) 32 | 33 | # Set up font 34 | font: QtGui.QFont = QtGui.QFont(SETTINGS["font.family"], SETTINGS["font.size"]) 35 | qapp.setFont(font) 36 | 37 | # Set up icon 38 | icon: QtGui.QIcon = QtGui.QIcon(get_icon_path(__file__, "veighna.ico")) 39 | qapp.setWindowIcon(icon) 40 | 41 | # Set up windows process ID 42 | if "Windows" in platform.uname(): 43 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( 44 | app_name 45 | ) 46 | 47 | # Hide help button for all dialogs 48 | # qapp.setAttribute(QtCore.Qt.AA_DisableWindowContextHelpButton) 49 | 50 | # Exception Handling 51 | exception_widget: ExceptionWidget = ExceptionWidget() 52 | 53 | def excepthook(exctype: type, value: Exception, tb: types.TracebackType) -> None: 54 | """Show exception detail with QMessageBox.""" 55 | sys.__excepthook__(exctype, value, tb) 56 | 57 | msg: str = "".join(traceback.format_exception(exctype, value, tb)) 58 | exception_widget.signal.emit(msg) 59 | 60 | sys.excepthook = excepthook 61 | 62 | def threading_excepthook(args: threading.ExceptHookArgs) -> None: 63 | """Show exception detail from background threads with QMessageBox.""" 64 | sys.__excepthook__(args.exc_type, args.exc_value, args.exc_traceback) 65 | 66 | msg: str = "".join(traceback.format_exception(args.exc_type, args.exc_value, args.exc_traceback)) 67 | exception_widget.signal.emit(msg) 68 | 69 | threading.excepthook = threading_excepthook 70 | 71 | return qapp 72 | 73 | 74 | class ExceptionWidget(QtWidgets.QWidget): 75 | """""" 76 | signal: QtCore.Signal = QtCore.Signal(str) 77 | 78 | def __init__(self, parent: QtWidgets.QWidget = None) -> None: 79 | """""" 80 | super().__init__(parent) 81 | 82 | self.init_ui() 83 | self.signal.connect(self.show_exception) 84 | 85 | def init_ui(self) -> None: 86 | """""" 87 | self.setFixedSize(600, 600) 88 | self.setWindowFlags(QtCore.Qt.FramelessWindowHint) 89 | self.setStyleSheet("background-color: rgb(243, 243, 243)") 90 | 91 | self.title_label = SubtitleLabel("Exception Triggered", self) 92 | 93 | self.msg_edit: TextEdit = TextEdit() 94 | self.msg_edit.setReadOnly(True) 95 | 96 | copy_button: PushButton = PushButton("Copy") 97 | copy_button.clicked.connect(self._copy_text) 98 | 99 | community_button: PushButton = PushButton("Help") 100 | community_button.clicked.connect(self._open_community) 101 | 102 | close_button: PushButton = PushButton("Close") 103 | close_button.clicked.connect(self.close) 104 | 105 | hbox: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() 106 | hbox.addWidget(copy_button) 107 | hbox.addWidget(community_button) 108 | hbox.addWidget(close_button) 109 | 110 | vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() 111 | vbox.addWidget(self.title_label) 112 | vbox.addWidget(self.msg_edit) 113 | vbox.addLayout(hbox) 114 | 115 | self.setLayout(vbox) 116 | 117 | def show_exception(self, msg: str) -> None: 118 | """""" 119 | self.msg_edit.setText(msg) 120 | self.show() 121 | 122 | def _copy_text(self) -> None: 123 | """""" 124 | self.msg_edit.selectAll() 125 | self.msg_edit.copy() 126 | 127 | def _open_community(self) -> None: 128 | """""" 129 | webbrowser.open("https://www.vnpy.com/forum/forum/2-ti-wen-qiu-zhu") 130 | -------------------------------------------------------------------------------- /vnpy_evo/trader/ui/widget.py: -------------------------------------------------------------------------------- 1 | """ 2 | Basic widgets for UI. 3 | """ 4 | 5 | import platform 6 | from copy import copy 7 | from typing import TYPE_CHECKING 8 | 9 | import importlib_metadata 10 | from qfluentwidgets import ( 11 | Pivot, MessageBoxBase, BodyLabel, SubtitleLabel, 12 | PushButton, ComboBox, LineEdit, CheckBox, 13 | ) 14 | 15 | from vnpy.trader.locale import _ 16 | from .qt import QtCore, QtGui, QtWidgets 17 | from ..constant import Direction, Exchange, Offset, OrderType 18 | from ..engine import MainEngine, Event, EventEngine 19 | from ..event import EVENT_TICK 20 | from ..object import ( 21 | OrderRequest, 22 | SubscribeRequest, 23 | CancelRequest, 24 | ContractData, 25 | PositionData, 26 | OrderData, 27 | TickData 28 | ) 29 | from ..utility import load_json, save_json, get_digits 30 | from ..setting import SETTING_FILENAME, SETTINGS 31 | 32 | if TYPE_CHECKING: 33 | from .monitor import BaseCell 34 | 35 | 36 | class ConnectDialog(MessageBoxBase): 37 | """ 38 | Start connection of a certain gateway. 39 | """ 40 | 41 | def __init__(self, main_engine: MainEngine, gateway_name: str, parent: QtWidgets.QWidget = None) -> None: 42 | """""" 43 | super().__init__(parent) 44 | 45 | self.main_engine: MainEngine = main_engine 46 | self.gateway_name: str = gateway_name 47 | self.filename: str = f"connect_{gateway_name.lower()}.json" 48 | 49 | self.widgets: dict[str, QtWidgets.QWidget] = {} 50 | 51 | self.init_ui() 52 | 53 | def init_ui(self) -> None: 54 | """""" 55 | self.title_label = SubtitleLabel(f"Connect {self.gateway_name}", self) 56 | 57 | # Default setting provides field name, field data type and field default value. 58 | default_setting: dict = self.main_engine.get_default_setting( 59 | self.gateway_name) 60 | 61 | # Saved setting provides field data used last time. 62 | loaded_setting: dict = load_json(self.filename) 63 | 64 | # Initialize line edits and form layout based on setting. 65 | grid: QtWidgets.QGridLayout = QtWidgets.QGridLayout() 66 | row: int = 0 67 | 68 | for field_name, field_value in default_setting.items(): 69 | field_type: type = type(field_value) 70 | 71 | if field_type == list: 72 | widget: ComboBox = ComboBox() 73 | widget.addItems(field_value) 74 | 75 | if field_name in loaded_setting: 76 | saved_value = loaded_setting[field_name] 77 | ix: int = widget.findText(saved_value) 78 | widget.setCurrentIndex(ix) 79 | else: 80 | widget: LineEdit = LineEdit() 81 | widget.setText(str(field_value)) 82 | 83 | if field_name in loaded_setting: 84 | saved_value = loaded_setting[field_name] 85 | widget.setText(str(saved_value)) 86 | 87 | if _("密码") in field_name: 88 | widget.setEchoMode(LineEdit.Password) 89 | 90 | if field_type == int: 91 | validator: QtGui.QIntValidator = QtGui.QIntValidator() 92 | widget.setValidator(validator) 93 | 94 | grid.addWidget(BodyLabel(f"{field_name} <{field_type.__name__}>"), row, 0) 95 | grid.addWidget(widget, row, 1) 96 | self.widgets[field_name] = (widget, field_type) 97 | 98 | row += 1 99 | 100 | self.viewLayout.addWidget(self.title_label) 101 | self.viewLayout.addLayout(grid) 102 | 103 | self.yesButton.setText("Connect") 104 | self.yesButton.clicked.connect(self.connect_gateway) 105 | 106 | self.cancelButton.setText("Cancel") 107 | 108 | self.widget.setFixedWidth(self.widget.width() * 6) 109 | 110 | def connect_gateway(self) -> None: 111 | """ 112 | Get setting value from line edits and connect the gateway. 113 | """ 114 | setting: dict = {} 115 | for field_name, tp in self.widgets.items(): 116 | widget, field_type = tp 117 | if field_type == list: 118 | field_value = str(widget.currentText()) 119 | else: 120 | try: 121 | field_value = field_type(widget.text()) 122 | except ValueError: 123 | field_value = field_type() 124 | setting[field_name] = field_value 125 | 126 | save_json(self.filename, setting) 127 | 128 | self.main_engine.connect(setting, self.gateway_name) 129 | self.accept() 130 | 131 | 132 | class TradingWidget(QtWidgets.QWidget): 133 | """ 134 | General manual trading widget. 135 | """ 136 | 137 | signal_tick: QtCore.Signal = QtCore.Signal(Event) 138 | 139 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 140 | """""" 141 | super().__init__() 142 | 143 | self.main_engine: MainEngine = main_engine 144 | self.event_engine: EventEngine = event_engine 145 | 146 | self.vt_symbol: str = "" 147 | self.price_digits: int = 0 148 | 149 | self.init_ui() 150 | self.register_event() 151 | 152 | def init_ui(self) -> None: 153 | """""" 154 | self.setFixedWidth(300) 155 | 156 | # Trading function area 157 | exchanges: list[Exchange] = self.main_engine.get_all_exchanges() 158 | self.exchange_combo: ComboBox = ComboBox() 159 | self.exchange_combo.addItems([exchange.value for exchange in exchanges]) 160 | 161 | self.symbol_line: LineEdit = LineEdit() 162 | self.symbol_line.returnPressed.connect(self.set_vt_symbol) 163 | 164 | self.name_line: LineEdit = LineEdit() 165 | self.name_line.setReadOnly(True) 166 | 167 | self.direction_combo: ComboBox = ComboBox() 168 | self.direction_combo.addItems([Direction.LONG.value, Direction.SHORT.value]) 169 | 170 | self.offset_combo: ComboBox = ComboBox() 171 | self.offset_combo.addItems([offset.value for offset in Offset]) 172 | 173 | self.order_type_combo: ComboBox = ComboBox() 174 | self.order_type_combo.addItems([order_type.value for order_type in OrderType]) 175 | 176 | double_validator: QtGui.QDoubleValidator = QtGui.QDoubleValidator() 177 | double_validator.setBottom(0) 178 | 179 | self.price_line: LineEdit = LineEdit() 180 | self.price_line.setValidator(double_validator) 181 | self.price_line.textChanged.connect(self.update_value) 182 | 183 | self.volume_line: LineEdit = LineEdit() 184 | self.volume_line.setValidator(double_validator) 185 | self.volume_line.textChanged.connect(self.update_value) 186 | 187 | self.value_line: LineEdit = LineEdit() 188 | self.value_line.setReadOnly(True) 189 | 190 | self.gateway_combo: ComboBox = ComboBox() 191 | self.gateway_combo.addItems(self.main_engine.get_all_gateway_names()) 192 | 193 | self.price_check: CheckBox = CheckBox() 194 | self.price_check.setToolTip(_("设置价格随行情更新")) 195 | 196 | send_button: PushButton = PushButton("Send Order") 197 | send_button.clicked.connect(self.send_order) 198 | 199 | cancel_button: PushButton = PushButton("Cancel All") 200 | cancel_button.clicked.connect(self.cancel_all) 201 | 202 | grid: QtWidgets.QGridLayout = QtWidgets.QGridLayout() 203 | grid.addWidget(BodyLabel(_("交易所")), 0, 0) 204 | grid.addWidget(BodyLabel(_("代码")), 1, 0) 205 | grid.addWidget(BodyLabel(_("名称")), 2, 0) 206 | grid.addWidget(BodyLabel(_("方向")), 3, 0) 207 | grid.addWidget(BodyLabel(_("开平")), 4, 0) 208 | grid.addWidget(BodyLabel(_("类型")), 5, 0) 209 | grid.addWidget(BodyLabel(_("价格")), 6, 0) 210 | grid.addWidget(BodyLabel(_("数量")), 7, 0) 211 | grid.addWidget(BodyLabel("Value"), 8, 0) 212 | grid.addWidget(BodyLabel(_("接口")), 9, 0) 213 | grid.addWidget(self.exchange_combo, 0, 1, 1, 2) 214 | grid.addWidget(self.symbol_line, 1, 1, 1, 2) 215 | grid.addWidget(self.name_line, 2, 1, 1, 2) 216 | grid.addWidget(self.direction_combo, 3, 1, 1, 2) 217 | grid.addWidget(self.offset_combo, 4, 1, 1, 2) 218 | grid.addWidget(self.order_type_combo, 5, 1, 1, 2) 219 | grid.addWidget(self.price_line, 6, 1, 1, 1) 220 | grid.addWidget(self.price_check, 6, 2, 1, 1) 221 | grid.addWidget(self.volume_line, 7, 1, 1, 2) 222 | grid.addWidget(self.value_line, 8, 1, 1, 2) 223 | grid.addWidget(self.gateway_combo, 9, 1, 1, 2) 224 | grid.addWidget(send_button, 10, 0, 1, 3) 225 | grid.addWidget(cancel_button, 11, 0, 1, 3) 226 | 227 | # Market depth display area 228 | bid_color: str = "red" 229 | ask_color: str = "green" 230 | 231 | self.bp1_label: BodyLabel = self.create_label(bid_color) 232 | self.bp2_label: BodyLabel = self.create_label(bid_color) 233 | self.bp3_label: BodyLabel = self.create_label(bid_color) 234 | self.bp4_label: BodyLabel = self.create_label(bid_color) 235 | self.bp5_label: BodyLabel = self.create_label(bid_color) 236 | 237 | self.bv1_label: BodyLabel = self.create_label( 238 | bid_color, alignment=QtCore.Qt.AlignRight) 239 | self.bv2_label: BodyLabel = self.create_label( 240 | bid_color, alignment=QtCore.Qt.AlignRight) 241 | self.bv3_label: BodyLabel = self.create_label( 242 | bid_color, alignment=QtCore.Qt.AlignRight) 243 | self.bv4_label: BodyLabel = self.create_label( 244 | bid_color, alignment=QtCore.Qt.AlignRight) 245 | self.bv5_label: BodyLabel = self.create_label( 246 | bid_color, alignment=QtCore.Qt.AlignRight) 247 | 248 | self.ap1_label: BodyLabel = self.create_label(ask_color) 249 | self.ap2_label: BodyLabel = self.create_label(ask_color) 250 | self.ap3_label: BodyLabel = self.create_label(ask_color) 251 | self.ap4_label: BodyLabel = self.create_label(ask_color) 252 | self.ap5_label: BodyLabel = self.create_label(ask_color) 253 | 254 | self.av1_label: BodyLabel = self.create_label( 255 | ask_color, alignment=QtCore.Qt.AlignRight) 256 | self.av2_label: BodyLabel = self.create_label( 257 | ask_color, alignment=QtCore.Qt.AlignRight) 258 | self.av3_label: BodyLabel = self.create_label( 259 | ask_color, alignment=QtCore.Qt.AlignRight) 260 | self.av4_label: BodyLabel = self.create_label( 261 | ask_color, alignment=QtCore.Qt.AlignRight) 262 | self.av5_label: BodyLabel = self.create_label( 263 | ask_color, alignment=QtCore.Qt.AlignRight) 264 | 265 | self.lp_label: BodyLabel = self.create_label() 266 | self.return_label: BodyLabel = self.create_label(alignment=QtCore.Qt.AlignRight) 267 | 268 | form: QtWidgets.QFormLayout = QtWidgets.QFormLayout() 269 | form.addRow(self.ap5_label, self.av5_label) 270 | form.addRow(self.ap4_label, self.av4_label) 271 | form.addRow(self.ap3_label, self.av3_label) 272 | form.addRow(self.ap2_label, self.av2_label) 273 | form.addRow(self.ap1_label, self.av1_label) 274 | form.addRow(self.lp_label, self.return_label) 275 | form.addRow(self.bp1_label, self.bv1_label) 276 | form.addRow(self.bp2_label, self.bv2_label) 277 | form.addRow(self.bp3_label, self.bv3_label) 278 | form.addRow(self.bp4_label, self.bv4_label) 279 | form.addRow(self.bp5_label, self.bv5_label) 280 | 281 | # Overall layout 282 | vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() 283 | vbox.addLayout(grid) 284 | vbox.addLayout(form) 285 | self.setLayout(vbox) 286 | 287 | def create_label( 288 | self, 289 | color: str = "", 290 | alignment: int = QtCore.Qt.AlignLeft 291 | ) -> BodyLabel: 292 | """ 293 | Create label with certain font color. 294 | """ 295 | label: BodyLabel = BodyLabel() 296 | if color: 297 | label.setStyleSheet(f"color:{color}") 298 | label.setAlignment(alignment) 299 | return label 300 | 301 | def register_event(self) -> None: 302 | """""" 303 | self.signal_tick.connect(self.process_tick_event) 304 | self.event_engine.register(EVENT_TICK, self.signal_tick.emit) 305 | 306 | def process_tick_event(self, event: Event) -> None: 307 | """""" 308 | tick: TickData = event.data 309 | if tick.vt_symbol != self.vt_symbol: 310 | return 311 | 312 | price_digits: int = self.price_digits 313 | 314 | self.lp_label.setText(f"{tick.last_price:.{price_digits}f}") 315 | self.bp1_label.setText(f"{tick.bid_price_1:.{price_digits}f}") 316 | self.bv1_label.setText(str(tick.bid_volume_1)) 317 | self.ap1_label.setText(f"{tick.ask_price_1:.{price_digits}f}") 318 | self.av1_label.setText(str(tick.ask_volume_1)) 319 | 320 | if tick.pre_close: 321 | r: float = (tick.last_price / tick.pre_close - 1) * 100 322 | self.return_label.setText(f"{r:.2f}%") 323 | 324 | if tick.bid_price_2: 325 | self.bp2_label.setText(f"{tick.bid_price_2:.{price_digits}f}") 326 | self.bv2_label.setText(str(tick.bid_volume_2)) 327 | self.ap2_label.setText(f"{tick.ask_price_2:.{price_digits}f}") 328 | self.av2_label.setText(str(tick.ask_volume_2)) 329 | 330 | self.bp3_label.setText(f"{tick.bid_price_3:.{price_digits}f}") 331 | self.bv3_label.setText(str(tick.bid_volume_3)) 332 | self.ap3_label.setText(f"{tick.ask_price_3:.{price_digits}f}") 333 | self.av3_label.setText(str(tick.ask_volume_3)) 334 | 335 | self.bp4_label.setText(f"{tick.bid_price_4:.{price_digits}f}") 336 | self.bv4_label.setText(str(tick.bid_volume_4)) 337 | self.ap4_label.setText(f"{tick.ask_price_4:.{price_digits}f}") 338 | self.av4_label.setText(str(tick.ask_volume_4)) 339 | 340 | self.bp5_label.setText(f"{tick.bid_price_5:.{price_digits}f}") 341 | self.bv5_label.setText(str(tick.bid_volume_5)) 342 | self.ap5_label.setText(f"{tick.ask_price_5:.{price_digits}f}") 343 | self.av5_label.setText(str(tick.ask_volume_5)) 344 | 345 | if self.price_check.isChecked(): 346 | self.price_line.setText(f"{tick.last_price:.{price_digits}f}") 347 | 348 | def set_vt_symbol(self) -> None: 349 | """ 350 | Set the tick depth data to monitor by vt_symbol. 351 | """ 352 | symbol: str = str(self.symbol_line.text()) 353 | if not symbol: 354 | return 355 | 356 | # Generate vt_symbol from symbol and exchange 357 | exchange_value: str = str(self.exchange_combo.currentText()) 358 | vt_symbol: str = f"{symbol}.{exchange_value}" 359 | 360 | if vt_symbol == self.vt_symbol: 361 | return 362 | self.vt_symbol = vt_symbol 363 | 364 | # Update name line widget and clear all labels 365 | contract: ContractData = self.main_engine.get_contract(vt_symbol) 366 | if not contract: 367 | self.name_line.setText("") 368 | gateway_name: str = self.gateway_combo.currentText() 369 | else: 370 | self.name_line.setText(contract.name) 371 | gateway_name: str = contract.gateway_name 372 | 373 | # Update gateway combo box. 374 | ix: int = self.gateway_combo.findText(gateway_name) 375 | self.gateway_combo.setCurrentIndex(ix) 376 | 377 | # Update price digits 378 | self.price_digits = get_digits(contract.pricetick) 379 | 380 | self.clear_label_text() 381 | self.volume_line.setText("") 382 | self.price_line.setText("") 383 | 384 | # Subscribe tick data 385 | req: SubscribeRequest = SubscribeRequest( 386 | symbol=symbol, exchange=Exchange(exchange_value) 387 | ) 388 | 389 | self.main_engine.subscribe(req, gateway_name) 390 | 391 | def clear_label_text(self) -> None: 392 | """ 393 | Clear text on all labels. 394 | """ 395 | self.lp_label.setText("") 396 | self.return_label.setText("") 397 | 398 | self.bv1_label.setText("") 399 | self.bv2_label.setText("") 400 | self.bv3_label.setText("") 401 | self.bv4_label.setText("") 402 | self.bv5_label.setText("") 403 | 404 | self.av1_label.setText("") 405 | self.av2_label.setText("") 406 | self.av3_label.setText("") 407 | self.av4_label.setText("") 408 | self.av5_label.setText("") 409 | 410 | self.bp1_label.setText("") 411 | self.bp2_label.setText("") 412 | self.bp3_label.setText("") 413 | self.bp4_label.setText("") 414 | self.bp5_label.setText("") 415 | 416 | self.ap1_label.setText("") 417 | self.ap2_label.setText("") 418 | self.ap3_label.setText("") 419 | self.ap4_label.setText("") 420 | self.ap5_label.setText("") 421 | 422 | def send_order(self) -> None: 423 | """ 424 | Send new order manually. 425 | """ 426 | symbol: str = str(self.symbol_line.text()) 427 | if not symbol: 428 | QtWidgets.QMessageBox.critical(self, _("委托失败"), _("请输入合约代码")) 429 | return 430 | 431 | volume_text: str = str(self.volume_line.text()) 432 | if not volume_text: 433 | QtWidgets.QMessageBox.critical(self, _("委托失败"), _("请输入委托数量")) 434 | return 435 | volume: float = float(volume_text) 436 | 437 | price_text: str = str(self.price_line.text()) 438 | if not price_text: 439 | price = 0 440 | else: 441 | price = float(price_text) 442 | 443 | req: OrderRequest = OrderRequest( 444 | symbol=symbol, 445 | exchange=Exchange(str(self.exchange_combo.currentText())), 446 | direction=Direction(str(self.direction_combo.currentText())), 447 | type=OrderType(str(self.order_type_combo.currentText())), 448 | volume=volume, 449 | price=price, 450 | offset=Offset(str(self.offset_combo.currentText())), 451 | reference="ManualTrading" 452 | ) 453 | 454 | gateway_name: str = str(self.gateway_combo.currentText()) 455 | 456 | self.main_engine.send_order(req, gateway_name) 457 | 458 | def cancel_all(self) -> None: 459 | """ 460 | Cancel all active orders. 461 | """ 462 | order_list: list[OrderData] = self.main_engine.get_all_active_orders() 463 | for order in order_list: 464 | req: CancelRequest = order.create_cancel_request() 465 | self.main_engine.cancel_order(req, order.gateway_name) 466 | 467 | def update_with_cell(self, cell: "BaseCell") -> None: 468 | """""" 469 | data = cell.get_data() 470 | 471 | self.symbol_line.setText(data.symbol) 472 | self.exchange_combo.setCurrentIndex( 473 | self.exchange_combo.findText(data.exchange.value) 474 | ) 475 | 476 | self.set_vt_symbol() 477 | 478 | if isinstance(data, PositionData): 479 | if data.direction == Direction.SHORT: 480 | direction: Direction = Direction.LONG 481 | elif data.direction == Direction.LONG: 482 | direction: Direction = Direction.SHORT 483 | else: # Net position mode 484 | if data.volume > 0: 485 | direction: Direction = Direction.SHORT 486 | else: 487 | direction: Direction = Direction.LONG 488 | 489 | self.direction_combo.setCurrentIndex( 490 | self.direction_combo.findText(direction.value) 491 | ) 492 | self.offset_combo.setCurrentIndex( 493 | self.offset_combo.findText(Offset.CLOSE.value) 494 | ) 495 | self.volume_line.setText(str(abs(data.volume))) 496 | 497 | def update_value(self) -> None: 498 | """Update order value""" 499 | contract: ContractData = self.main_engine.get_contract(self.vt_symbol) 500 | price_text: str = self.price_line.text() 501 | volume_text: str = self.volume_line.text() 502 | 503 | if not all([contract, price_text, volume_text]): 504 | self.value_line.setText("") 505 | return 506 | 507 | value: float = float(price_text) * float(volume_text) * contract.size 508 | self.value_line.setText(f"{value:.2f}") 509 | 510 | 511 | class AboutDialog(MessageBoxBase): 512 | """ 513 | Information about the trading platform. 514 | """ 515 | 516 | def __init__(self, parent: QtWidgets.QWidget) -> None: 517 | """""" 518 | super().__init__(parent) 519 | 520 | self.init_ui() 521 | 522 | def init_ui(self) -> None: 523 | """""" 524 | self.title_label = SubtitleLabel("About VeighNa Evo", self) 525 | 526 | from vnpy import __version__ as vnpy_version 527 | from ... import __version__ as evo_version 528 | 529 | text: str = f""" 530 | By Traders, For Traders. 531 | 532 | Created by VeighNa Technology 533 | 534 | 535 | License:MIT 536 | Website:www.vnpy.com 537 | Github:www.github.com/vnpy/vnpy 538 | 539 | 540 | VeighNa Evo - {evo_version} 541 | VeighNa - {vnpy_version} 542 | Python - {platform.python_version()} 543 | PySide6 - {importlib_metadata.version("pyside6")} 544 | NumPy - {importlib_metadata.version("numpy")} 545 | pandas - {importlib_metadata.version("pandas")} 546 | """ 547 | 548 | label: BodyLabel = BodyLabel() 549 | label.setText(text) 550 | label.setMinimumWidth(500) 551 | 552 | self.viewLayout.addWidget(self.title_label) 553 | self.viewLayout.addWidget(label) 554 | 555 | self.cancelButton.hide() 556 | 557 | 558 | class GlobalDialog(MessageBoxBase): 559 | """ 560 | Edit global setting. 561 | """ 562 | 563 | def __init__(self, parent: QtWidgets.QWidget = None) -> None: 564 | """""" 565 | super().__init__(parent) 566 | 567 | self.widgets: dict[str, object] = {} 568 | 569 | self.init_ui() 570 | 571 | def init_ui(self) -> None: 572 | """""" 573 | self.title_label = SubtitleLabel("Global Configuration", self) 574 | 575 | settings: dict = copy(SETTINGS) 576 | settings.update(load_json(SETTING_FILENAME)) 577 | 578 | # Initialize line edits and form layout based on setting. 579 | scroll_widget: QtWidgets.QWidget = QtWidgets.QWidget(parent=self) 580 | 581 | grid: QtWidgets.QFormLayout = QtWidgets.QGridLayout(parent=scroll_widget) 582 | row: int = 0 583 | 584 | for field_name, field_value in settings.items(): 585 | if "datafeed" in field_name: 586 | continue 587 | 588 | field_type: type = type(field_value) 589 | widget: LineEdit = LineEdit() 590 | widget.setText(str(field_value)) 591 | 592 | grid.addWidget(BodyLabel(f"{field_name} <{field_type.__name__}>"), row, 0) 593 | grid.addWidget(widget, row, 1) 594 | self.widgets[field_name] = (widget, field_type) 595 | 596 | row += 1 597 | 598 | scroll_widget.setLayout(grid) 599 | 600 | self.viewLayout.addWidget(self.title_label) 601 | self.viewLayout.addWidget(scroll_widget) 602 | 603 | self.yesButton.setText("Confirm") 604 | self.yesButton.clicked.connect(self.update_setting) 605 | 606 | self.widget.setFixedWidth(self.widget.width() * 6) 607 | 608 | def update_setting(self) -> None: 609 | """ 610 | Get setting value from line edits and update global setting file. 611 | """ 612 | settings: dict = {} 613 | for field_name, tp in self.widgets.items(): 614 | widget, field_type = tp 615 | value_text: str = widget.text() 616 | 617 | if field_type == bool: 618 | if value_text == "True": 619 | field_value: bool = True 620 | else: 621 | field_value: bool = False 622 | else: 623 | field_value = field_type(value_text) 624 | 625 | settings[field_name] = field_value 626 | 627 | QtWidgets.QMessageBox.information( 628 | self, 629 | _("注意"), 630 | _("全局配置的修改需要重启后才会生效!"), 631 | QtWidgets.QMessageBox.Ok 632 | ) 633 | 634 | save_json(SETTING_FILENAME, settings) 635 | self.accept() 636 | 637 | 638 | class PivotWidgdet(QtWidgets.QWidget): 639 | """""" 640 | 641 | def __init__(self, parent=None): 642 | super().__init__(parent=parent) 643 | 644 | self.pivot = Pivot(self) 645 | self.stacked_widget = QtWidgets.QStackedWidget(self) 646 | self.vbox = QtWidgets.QVBoxLayout(self) 647 | 648 | self.vbox.addWidget(self.pivot, 0, QtCore.Qt.AlignLeft) 649 | self.vbox.addWidget(self.stacked_widget) 650 | self.vbox.setContentsMargins(0, 0, 0, 0) 651 | 652 | self.stacked_widget.currentChanged.connect(self.onCurrentIndexChanged) 653 | 654 | def add_widget(self, widget: QtWidgets.QWidget, name: str) -> None: 655 | """""" 656 | widget.setObjectName(name) 657 | 658 | self.stacked_widget.addWidget(widget) 659 | 660 | self.pivot.addItem( 661 | routeKey=name, 662 | text=name, 663 | onClick=lambda: self.stacked_widget.setCurrentWidget(widget) 664 | ) 665 | 666 | if self.stacked_widget.count() == 1: 667 | self.stacked_widget.setCurrentWidget(widget) 668 | self.pivot.setCurrentItem(widget.objectName()) 669 | 670 | def onCurrentIndexChanged(self, index: int) -> None: 671 | """""" 672 | widget: QtWidgets.QWidget = self.stacked_widget.widget(index) 673 | self.pivot.setCurrentItem(widget.objectName()) 674 | -------------------------------------------------------------------------------- /vnpy_evo/trader/utility.py: -------------------------------------------------------------------------------- 1 | from vnpy.trader.utility import * 2 | 3 | from .constant import Exchange 4 | 5 | 6 | def extract_vt_symbol(vt_symbol: str) -> tuple[str, Exchange]: 7 | """ 8 | return (symbol, exchange) 9 | """ 10 | symbol, exchange_str = vt_symbol.rsplit(".", 1) 11 | return symbol, Exchange(exchange_str) 12 | -------------------------------------------------------------------------------- /vnpy_evo/websocket/__init__.py: -------------------------------------------------------------------------------- 1 | from .websocket_client import WebsocketClient 2 | -------------------------------------------------------------------------------- /vnpy_evo/websocket/websocket_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import ssl 3 | import traceback 4 | from threading import Thread 5 | from typing import Optional 6 | 7 | import websocket 8 | 9 | 10 | class WebsocketClient: 11 | """ 12 | Websocket API 13 | 14 | After creating the client object, use start() to run worker thread. 15 | The worker thread connects websocket automatically. 16 | 17 | Use stop to stop threads and disconnect websocket before destroying the client 18 | object (especially when exiting the programme). 19 | 20 | Default serialization format is json. 21 | 22 | Callbacks to overrides: 23 | * on_connected 24 | * on_disconnected 25 | * on_packet 26 | * on_error 27 | 28 | If you want to send anything other than JSON, override send_packet. 29 | """ 30 | 31 | def __init__(self) -> None: 32 | """Constructor""" 33 | self.active: bool = False 34 | self.host: str = "" 35 | 36 | self.wsapp: websocket.WebSocketApp = None 37 | self.thread: Thread = None 38 | 39 | self.proxy_host: Optional[str] = None 40 | self.proxy_port: Optional[int] = None 41 | self.header: Optional[dict] = None 42 | self.ping_interval: int = 0 43 | self.receive_timeout: int = 0 44 | 45 | self.trace: bool = False 46 | 47 | def init( 48 | self, 49 | host: str, 50 | proxy_host: str = "", 51 | proxy_port: int = 0, 52 | ping_interval: int = 10, 53 | receive_timeout: int = 60, 54 | header: dict = None, 55 | trace: bool = False 56 | ) -> None: 57 | """ 58 | :param host: 59 | :param proxy_host: 60 | :param proxy_port: 61 | :param header: 62 | :param ping_interval: unit: seconds, type: int 63 | """ 64 | self.host = host 65 | self.ping_interval = ping_interval # seconds 66 | self.receive_timeout = receive_timeout 67 | 68 | if header: 69 | self.header = header 70 | 71 | if proxy_host and proxy_port: 72 | self.proxy_host = proxy_host 73 | self.proxy_port = proxy_port 74 | 75 | websocket.enableTrace(trace) 76 | websocket.setdefaulttimeout(receive_timeout) 77 | 78 | def start(self) -> None: 79 | """ 80 | Start the client and on_connected function is called after webscoket 81 | is connected succesfully. 82 | 83 | Please don't send packet untill on_connected fucntion is called. 84 | """ 85 | self.active = True 86 | self.thread = Thread(target=self.run) 87 | self.thread.start() 88 | 89 | def stop(self) -> None: 90 | """ 91 | Stop the client. 92 | """ 93 | if not self.active: 94 | return 95 | 96 | self.active = False 97 | self.wsapp.close() 98 | 99 | def join(self) -> None: 100 | """ 101 | Wait till all threads finish. 102 | 103 | This function cannot be called from worker thread or callback function. 104 | """ 105 | self.thread.join() 106 | 107 | def send_packet(self, packet: dict) -> None: 108 | """ 109 | Send a packet (dict data) to server 110 | 111 | override this if you want to send non-json packet 112 | """ 113 | text: str = json.dumps(packet) 114 | self.wsapp.send(text) 115 | 116 | def run(self) -> None: 117 | """ 118 | Keep running till stop is called. 119 | """ 120 | def on_open(wsapp: websocket.WebSocketApp) -> None: 121 | self.on_connected() 122 | 123 | def on_close(wsapp: websocket.WebSocketApp, status_code: int, msg: str) -> None: 124 | self.on_disconnected(status_code, msg) 125 | 126 | def on_error(wsapp: websocket.WebSocketApp, e: Exception) -> None: 127 | self.on_error(e) 128 | 129 | def on_message(wsapp: websocket.WebSocketApp, message: str) -> None: 130 | self.on_message(message) 131 | 132 | self.wsapp = websocket.WebSocketApp( 133 | url=self.host, 134 | header=self.header, 135 | on_open=on_open, 136 | on_close=on_close, 137 | on_error=on_error, 138 | on_message=on_message 139 | ) 140 | 141 | proxy_type: Optional[str] = None 142 | if self.proxy_host: 143 | proxy_type = "http" 144 | 145 | self.wsapp.run_forever( 146 | sslopt={"cert_reqs": ssl.CERT_NONE}, 147 | ping_interval=self.ping_interval, 148 | http_proxy_host=self.proxy_host, 149 | http_proxy_port=self.proxy_port, 150 | proxy_type=proxy_type, 151 | reconnect=1 152 | ) 153 | 154 | def on_message(self, message: str) -> None: 155 | """ 156 | Callback when weboscket app receives new message 157 | """ 158 | self.on_packet(json.loads(message)) 159 | 160 | def on_connected(self) -> None: 161 | """ 162 | Callback when websocket is connected successfully. 163 | """ 164 | pass 165 | 166 | def on_disconnected(self, status_code: int, msg: str) -> None: 167 | """ 168 | Callback when websocket connection is closed. 169 | """ 170 | pass 171 | 172 | def on_packet(packet: dict) -> None: 173 | """ 174 | Callback when receiving data from server. 175 | """ 176 | pass 177 | 178 | def on_error(self, e: Exception) -> None: 179 | """ 180 | Callback when exception raised. 181 | """ 182 | try: 183 | print("WebsocketClient on error" + "-" * 10) 184 | print(e) 185 | except Exception: 186 | traceback.print_exc() 187 | --------------------------------------------------------------------------------