├── .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 | - [](https://x.com/veighna_global) Follow us on Twitter
19 | - [](https://t.me/veighna_channel) Follow our important announcements
20 | - [](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 |
--------------------------------------------------------------------------------