├── vnpy_scripttrader ├── ui │ ├── __init__.py │ ├── script.ico │ └── widget.py ├── cli.py ├── __init__.py └── engine.py ├── .github ├── PULL_REQUEST_TEMPLATE.md ├── SUPPORT.md ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── workflows │ └── pythonapp.yml ├── CHANGELOG.md ├── script └── run.py ├── README.md ├── LICENSE ├── .gitignore └── pyproject.toml /vnpy_scripttrader/ui/__init__.py: -------------------------------------------------------------------------------- 1 | from .widget import ScriptManager 2 | 3 | 4 | __all__ = ["ScriptManager"] 5 | -------------------------------------------------------------------------------- /vnpy_scripttrader/ui/script.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vnpy/vnpy_scripttrader/HEAD/vnpy_scripttrader/ui/script.ico -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 建议每次发起的PR内容尽可能精简,复杂的修改请拆分为多次PR,便于管理合并。 2 | 3 | ## 改进内容 4 | 5 | 1. 6 | 2. 7 | 3. 8 | 9 | ## 相关的Issue号(如有) 10 | 11 | Close # -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # 获取帮助 2 | 3 | 在开发和使用VeighNa项目的过程中遇到问题时,获取帮助的渠道包括: 4 | 5 | * Github Issues:[Issues页面](https://github.com/vnpy/vnpy/issues) 6 | * 官方QQ群: 262656087 7 | * 项目论坛:[VeighNa量化社区](http://www.vnpy.com/forum) 8 | * 项目邮箱: vn.py@foxmail.com 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.1.1版本 2 | 3 | 1. 注册模块日志输出到日志引擎 4 | 5 | # 1.1.0版本 6 | 7 | 1. vnpy框架4.0版本升级适配 8 | 9 | # 1.0.3版本 10 | 11 | 1. 替换所有的Qt信号pyqtSignal为Signal 12 | 13 | # 1.0.2版本 14 | 15 | 1. 增加基于vt_symbol和direction的持仓查询函数 16 | 17 | # 1.0.1版本 18 | 19 | 1. 将模块的图标文件信息,改为完整路径字符串 20 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # 行为准则 2 | 3 | 这是一份VeighNa项目社区的行为准则,也是项目作者自己在刚入行量化金融行业时对于理想中的社区的期望: 4 | 5 | * 为交易员而生:作为一款从金融机构量化业务中诞生的交易系统开发框架,设计上都优先满足机构专业交易员的使用习惯,而不是其他用户(散户、爱好者、技术人员等) 6 | 7 | * 对新用户友好,保持耐心:大部分人在接触新东西的时候都是磕磕碰碰、有很多的问题,请记住此时别人对你伸出的援助之手,并把它传递给未来需要的人 8 | 9 | * 尊重他人,慎重言行:礼貌文明的交流方式除了能得到别人同样的回应,更能减少不必要的摩擦,保证高效的交流 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 环境 2 | 3 | * 操作系统: 如Windows 11或者Ubuntu 22.04 4 | * Python版本: 如VeighNa Studio-4.0.0 5 | * VeighNa版本: 如v4.0.0发行版或者dev branch 20250320(下载日期) 6 | 7 | ## Issue类型 8 | 三选一:Bug/Enhancement/Question 9 | 10 | ## 预期程序行为 11 | 12 | 13 | ## 实际程序行为 14 | 15 | 16 | ## 重现步骤 17 | 18 | 针对Bug类型Issue,请提供具体重现步骤以及报错截图 19 | 20 | -------------------------------------------------------------------------------- /script/run.py: -------------------------------------------------------------------------------- 1 | from vnpy.event import EventEngine 2 | from vnpy.trader.engine import MainEngine 3 | from vnpy.trader.ui import MainWindow, create_qapp 4 | 5 | from vnpy_ctp import CtpGateway 6 | from vnpy_scripttrader import ScriptTraderApp 7 | 8 | 9 | def main() -> None: 10 | """Start Trader""" 11 | qapp = create_qapp() 12 | 13 | event_engine = EventEngine() 14 | main_engine = MainEngine(event_engine) 15 | 16 | main_engine.add_gateway(CtpGateway) 17 | main_engine.add_app(ScriptTraderApp) 18 | 19 | main_window = MainWindow(main_engine, event_engine) 20 | main_window.showMaximized() 21 | 22 | qapp.exec() 23 | 24 | 25 | if __name__ == "__main__": 26 | main() 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VeighNa框架的脚本交易模块 2 | 3 |

4 | 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 |

13 | 14 | ## 说明 15 | 16 | ScriptTrader是用于交易脚本执行的功能模块,和其他策略模块最大的区别在于其基于【时间驱动】的【同步逻辑】,也支持在命令行(Jupyter Notebook)中以REPL指令形式的进行交易操作,该模块没有回测功能。 17 | 18 | ## 安装 19 | 20 | 安装环境推荐基于4.0.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com)】。 21 | 22 | 直接使用pip命令: 23 | 24 | ``` 25 | pip install vnpy_scripttrader 26 | ``` 27 | 28 | 29 | 或者下载源代码后,解压后在cmd中运行: 30 | 31 | ``` 32 | pip install . 33 | ``` 34 | -------------------------------------------------------------------------------- /vnpy_scripttrader/cli.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Sequence 2 | 3 | from vnpy.event import EventEngine, Event 4 | from vnpy.trader.engine import MainEngine 5 | from vnpy.trader.gateway import BaseGateway 6 | from vnpy.trader.event import EVENT_LOG 7 | from vnpy.trader.object import LogData 8 | 9 | from .engine import ScriptEngine, BaseEngine 10 | 11 | 12 | def process_log_event(event: Event) -> None: 13 | """打印日志输出""" 14 | log: LogData = event.data 15 | print(f"{log.time}\t{log.msg}") 16 | 17 | 18 | def init_cli_trading(gateways: Sequence[type[BaseGateway]]) -> BaseEngine: 19 | """初始化命令行交易""" 20 | event_engine: EventEngine = EventEngine() 21 | event_engine.register(EVENT_LOG, process_log_event) 22 | 23 | main_engine: MainEngine = MainEngine(event_engine) 24 | for gateway in gateways: 25 | main_engine.add_gateway(gateway) 26 | 27 | script_engine: ScriptEngine = main_engine.add_engine(ScriptEngine) 28 | 29 | return script_engine 30 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up Python 3.13 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.13' 16 | - name: Install dependencies 17 | run: | 18 | python -m pip install --upgrade pip 19 | pip install ta-lib==0.6.4 --index=https://pypi.vnpy.com 20 | pip install vnpy ruff mypy uv 21 | - name: Lint with ruff 22 | run: | 23 | # Run ruff linter based on pyproject.toml configuration 24 | ruff check . 25 | - name: Type check with mypy 26 | run: | 27 | # Run mypy type checking based on pyproject.toml configuration 28 | mypy vnpy_scripttrader 29 | - name: Build packages with uv 30 | run: | 31 | # Build source distribution and wheel distribution 32 | uv build 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Xiaoyou Chen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vnpy_scripttrader/__init__.py: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | # 3 | # Copyright (c) 2015-present, Xiaoyou Chen 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | from pathlib import Path 25 | 26 | from vnpy.trader.app import BaseApp 27 | 28 | from .engine import ScriptEngine, APP_NAME 29 | from .cli import init_cli_trading 30 | 31 | 32 | __all__ = [ 33 | "APP_NAME", 34 | "ScriptEngine", 35 | "ScriptTraderApp", 36 | "init_cli_trading", 37 | ] 38 | 39 | 40 | __version__ = "1.1.1" 41 | 42 | 43 | class ScriptTraderApp(BaseApp): 44 | """""" 45 | 46 | app_name: str = APP_NAME 47 | app_module: str = __module__ 48 | app_path: Path = Path(__file__).parent 49 | display_name: str = "脚本策略" 50 | engine_class: type[ScriptEngine] = ScriptEngine 51 | widget_name: str = "ScriptManager" 52 | icon_name: str = str(app_path.joinpath("ui", "script.ico")) 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vnpy_scripttrader" 3 | dynamic = ["version"] 4 | description = "Script trading application for VeighNa quant trading framework." 5 | readme = "README.md" 6 | license = {text = "MIT"} 7 | authors = [{name = "Xiaoyou Chen", email = "xiaoyou.chen@mail.vnpy.com"}] 8 | classifiers = [ 9 | "Development Status :: 5 - Production/Stable", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Programming Language :: Python :: 3", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Programming Language :: Python :: 3.13", 17 | "Topic :: Office/Business :: Financial :: Investment", 18 | "Programming Language :: Python :: Implementation :: CPython", 19 | "Natural Language :: Chinese (Simplified)", 20 | "Typing :: Typed", 21 | ] 22 | requires-python = ">=3.10" 23 | dependencies = [ 24 | "vnpy>=4.0.0" 25 | ] 26 | keywords = ["quant", "quantitative", "investment", "trading", "algotrading"] 27 | 28 | [project.urls] 29 | "Homepage" = "https://www.vnpy.com" 30 | "Documentation" = "https://www.vnpy.com/docs" 31 | "Changes" = "https://github.com/vnpy/vnpy_scripttrader/blob/master/CHANGELOG.md" 32 | "Source" = "https://github.com/vnpy/vnpy_scripttrader/" 33 | "Forum" = "https://www.vnpy.com/forum" 34 | 35 | [build-system] 36 | requires = ["hatchling>=1.27.0"] 37 | build-backend = "hatchling.build" 38 | 39 | [tool.hatch.version] 40 | path = "vnpy_scripttrader/__init__.py" 41 | pattern = "__version__ = ['\"](?P[^'\"]+)['\"]" 42 | 43 | [tool.hatch.build.targets.wheel] 44 | packages = ["vnpy_scripttrader"] 45 | include-package-data = true 46 | 47 | [tool.hatch.build.targets.sdist] 48 | include = ["vnpy_scripttrader*"] 49 | 50 | [tool.ruff] 51 | target-version = "py310" 52 | output-format = "full" 53 | 54 | [tool.ruff.lint] 55 | select = [ 56 | "B", # flake8-bugbear 57 | "E", # pycodestyle error 58 | "F", # pyflakes 59 | "UP", # pyupgrade 60 | "W", # pycodestyle warning 61 | ] 62 | ignore = ["E501"] 63 | 64 | [tool.mypy] 65 | python_version = "3.10" 66 | warn_return_any = true 67 | warn_unused_configs = true 68 | disallow_untyped_defs = true 69 | disallow_incomplete_defs = true 70 | check_untyped_defs = true 71 | disallow_untyped_decorators = true 72 | no_implicit_optional = true 73 | strict_optional = true 74 | warn_redundant_casts = true 75 | warn_unused_ignores = true 76 | warn_no_return = true 77 | ignore_missing_imports = true 78 | -------------------------------------------------------------------------------- /vnpy_scripttrader/ui/widget.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from vnpy.event import EventEngine, Event 4 | from vnpy.trader.engine import MainEngine 5 | from vnpy.trader.ui import QtWidgets, QtCore 6 | from vnpy.trader.object import LogData 7 | from ..engine import APP_NAME, EVENT_SCRIPT_LOG, BaseEngine 8 | 9 | 10 | class ScriptManager(QtWidgets.QWidget): 11 | """""" 12 | signal_log: QtCore.Signal = QtCore.Signal(Event) 13 | 14 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 15 | """""" 16 | super().__init__() 17 | 18 | self.main_engine: MainEngine = main_engine 19 | self.event_engine: EventEngine = event_engine 20 | 21 | self.script_engine: BaseEngine = main_engine.get_engine(APP_NAME) 22 | 23 | self.script_path: str = "" 24 | 25 | self.init_ui() 26 | self.register_event() 27 | 28 | self.script_engine.init() 29 | 30 | def init_ui(self) -> None: 31 | """""" 32 | self.setWindowTitle("脚本策略") 33 | 34 | start_button: QtWidgets.QPushButton = QtWidgets.QPushButton("启动") 35 | start_button.clicked.connect(self.start_script) 36 | 37 | stop_button: QtWidgets.QPushButton = QtWidgets.QPushButton("停止") 38 | stop_button.clicked.connect(self.stop_script) 39 | 40 | select_button: QtWidgets.QPushButton = QtWidgets.QPushButton("打开") 41 | select_button.clicked.connect(self.select_script) 42 | 43 | self.strategy_line: QtWidgets.QLineEdit = QtWidgets.QLineEdit() 44 | 45 | self.log_monitor: QtWidgets.QTextEdit = QtWidgets.QTextEdit() 46 | self.log_monitor.setReadOnly(True) 47 | 48 | clear_button: QtWidgets.QPushButton = QtWidgets.QPushButton("清空") 49 | clear_button.clicked.connect(self.log_monitor.clear) 50 | 51 | hbox: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() 52 | hbox.addWidget(self.strategy_line) 53 | hbox.addWidget(select_button) 54 | hbox.addWidget(start_button) 55 | hbox.addWidget(stop_button) 56 | hbox.addStretch() 57 | hbox.addWidget(clear_button) 58 | 59 | vbox: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() 60 | vbox.addLayout(hbox) 61 | vbox.addWidget(self.log_monitor) 62 | 63 | self.setLayout(vbox) 64 | 65 | def register_event(self) -> None: 66 | """""" 67 | self.signal_log.connect(self.process_log_event) 68 | 69 | self.event_engine.register(EVENT_SCRIPT_LOG, self.signal_log.emit) 70 | 71 | def show(self) -> None: 72 | """""" 73 | self.showMaximized() 74 | 75 | def process_log_event(self, event: Event) -> None: 76 | """""" 77 | log: LogData = event.data 78 | msg: str = f"{log.time}\t{log.msg}" 79 | self.log_monitor.append(msg) 80 | 81 | def start_script(self) -> None: 82 | """""" 83 | if self.script_path: 84 | self.script_engine.start_strategy(self.script_path) 85 | 86 | def stop_script(self) -> None: 87 | """""" 88 | self.script_engine.stop_strategy() 89 | 90 | def select_script(self) -> None: 91 | """""" 92 | cwd: str = str(Path.cwd()) 93 | 94 | path, type_ = QtWidgets.QFileDialog.getOpenFileName( 95 | self, 96 | "载入策略脚本", 97 | cwd, 98 | "Python File(*.py)" 99 | ) 100 | 101 | if path: 102 | self.script_path = path 103 | self.strategy_line.setText(path) 104 | -------------------------------------------------------------------------------- /vnpy_scripttrader/engine.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import importlib 3 | import traceback 4 | from types import ModuleType 5 | from typing import Any 6 | from collections.abc import Callable 7 | from collections.abc import Sequence 8 | from pathlib import Path 9 | from datetime import datetime 10 | from threading import Thread 11 | 12 | from pandas import DataFrame 13 | 14 | from vnpy.event import Event, EventEngine 15 | from vnpy.trader.engine import BaseEngine, MainEngine, LogEngine 16 | from vnpy.trader.constant import Direction, Offset, OrderType, Interval 17 | from vnpy.trader.object import ( 18 | BaseData, 19 | OrderRequest, 20 | HistoryRequest, 21 | SubscribeRequest, 22 | TickData, 23 | OrderData, 24 | TradeData, 25 | PositionData, 26 | AccountData, 27 | ContractData, 28 | LogData, 29 | BarData, 30 | CancelRequest 31 | ) 32 | from vnpy.trader.datafeed import BaseDatafeed, get_datafeed 33 | 34 | 35 | APP_NAME = "ScriptTrader" 36 | 37 | EVENT_SCRIPT_LOG = "eScriptLog" 38 | 39 | 40 | class ScriptEngine(BaseEngine): 41 | """""" 42 | setting_filename: str = "script_trader_setting.json" 43 | 44 | def __init__(self, main_engine: MainEngine, event_engine: EventEngine) -> None: 45 | """""" 46 | super().__init__(main_engine, event_engine, APP_NAME) 47 | 48 | self.strategy_active: bool = False 49 | self.strategy_thread: Thread | None = None 50 | 51 | self.datafeed: BaseDatafeed = get_datafeed() 52 | 53 | log_engine: LogEngine = self.main_engine.get_engine("log") 54 | log_engine.register_log(EVENT_SCRIPT_LOG) 55 | 56 | def init(self) -> None: 57 | """启动策略引擎""" 58 | result: bool = self.datafeed.init() 59 | if result: 60 | self.write_log("数据服务初始化成功") 61 | 62 | def start_strategy(self, script_path: str) -> None: 63 | """运行策略线程中的策略方法""" 64 | if self.strategy_active: 65 | return 66 | self.strategy_active = True 67 | 68 | self.strategy_thread = Thread( 69 | target=self.run_strategy, args=(script_path,)) 70 | self.strategy_thread.start() 71 | 72 | self.write_log("策略交易脚本启动") 73 | 74 | def run_strategy(self, script_path: str) -> None: 75 | """加载策略脚本并调用run函数""" 76 | path: Path = Path(script_path) 77 | sys.path.append(str(path.parent)) 78 | 79 | script_name: str = path.parts[-1] 80 | module_name: str = script_name.replace(".py", "") 81 | 82 | try: 83 | module: ModuleType = importlib.import_module(module_name) 84 | importlib.reload(module) 85 | module.run(self) 86 | except Exception: 87 | msg: str = f"触发异常已停止\n{traceback.format_exc()}" 88 | self.write_log(msg) 89 | 90 | def stop_strategy(self) -> None: 91 | """停止运行中的策略""" 92 | if not self.strategy_active: 93 | return 94 | self.strategy_active = False 95 | 96 | if self.strategy_thread: 97 | self.strategy_thread.join() 98 | self.strategy_thread = None 99 | 100 | self.write_log("策略交易脚本停止") 101 | 102 | def connect_gateway(self, setting: dict, gateway_name: str) -> None: 103 | """""" 104 | self.main_engine.connect(setting, gateway_name) 105 | 106 | def send_order( 107 | self, 108 | vt_symbol: str, 109 | price: float, 110 | volume: float, 111 | direction: Direction, 112 | offset: Offset, 113 | order_type: OrderType 114 | ) -> str: 115 | """""" 116 | contract: ContractData | None = self.get_contract(vt_symbol) 117 | if not contract: 118 | return "" 119 | 120 | req: OrderRequest = OrderRequest( 121 | symbol=contract.symbol, 122 | exchange=contract.exchange, 123 | direction=direction, 124 | type=order_type, 125 | volume=volume, 126 | price=price, 127 | offset=offset, 128 | reference=APP_NAME 129 | ) 130 | 131 | vt_orderid: str = self.main_engine.send_order(req, contract.gateway_name) 132 | return vt_orderid 133 | 134 | def subscribe(self, vt_symbols: Sequence[str]) -> None: 135 | """""" 136 | for vt_symbol in vt_symbols: 137 | contract: ContractData | None = self.main_engine.get_contract(vt_symbol) 138 | if contract: 139 | req: SubscribeRequest = SubscribeRequest( 140 | symbol=contract.symbol, 141 | exchange=contract.exchange 142 | ) 143 | self.main_engine.subscribe(req, contract.gateway_name) 144 | 145 | def buy( 146 | self, 147 | vt_symbol: str, 148 | price: float, 149 | volume: float, 150 | order_type: OrderType = OrderType.LIMIT 151 | ) -> str: 152 | """""" 153 | return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.OPEN, order_type) 154 | 155 | def sell( 156 | self, 157 | vt_symbol: str, 158 | price: float, 159 | volume: float, 160 | order_type: OrderType = OrderType.LIMIT 161 | ) -> str: 162 | """""" 163 | return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.CLOSE, order_type) 164 | 165 | def short( 166 | self, 167 | vt_symbol: str, 168 | price: float, 169 | volume: float, 170 | order_type: OrderType = OrderType.LIMIT 171 | ) -> str: 172 | """""" 173 | return self.send_order(vt_symbol, price, volume, Direction.SHORT, Offset.OPEN, order_type) 174 | 175 | def cover( 176 | self, 177 | vt_symbol: str, 178 | price: float, 179 | volume: float, 180 | order_type: OrderType = OrderType.LIMIT 181 | ) -> str: 182 | """""" 183 | return self.send_order(vt_symbol, price, volume, Direction.LONG, Offset.CLOSE, order_type) 184 | 185 | def cancel_order(self, vt_orderid: str) -> None: 186 | """""" 187 | order: OrderData | None = self.get_order(vt_orderid) 188 | if not order: 189 | return 190 | 191 | req: CancelRequest = order.create_cancel_request() 192 | self.main_engine.cancel_order(req, order.gateway_name) 193 | 194 | def get_tick(self, vt_symbol: str, use_df: bool = False) -> TickData | None: 195 | """""" 196 | return get_data(self.main_engine.get_tick, arg=vt_symbol, use_df=use_df) 197 | 198 | def get_ticks(self, vt_symbols: Sequence[str], use_df: bool = False) -> Sequence[TickData] | DataFrame | None: 199 | """""" 200 | ticks: list = [] 201 | for vt_symbol in vt_symbols: 202 | tick: TickData | None = self.main_engine.get_tick(vt_symbol) 203 | ticks.append(tick) 204 | 205 | if not use_df: 206 | return ticks 207 | else: 208 | return to_df(ticks) 209 | 210 | def get_order(self, vt_orderid: str, use_df: bool = False) -> OrderData | None: 211 | """""" 212 | return get_data(self.main_engine.get_order, arg=vt_orderid, use_df=use_df) 213 | 214 | def get_orders(self, vt_orderids: Sequence[str], use_df: bool = False) -> Sequence[OrderData] | DataFrame | None: 215 | """""" 216 | orders: list = [] 217 | for vt_orderid in vt_orderids: 218 | order: OrderData | None = self.main_engine.get_order(vt_orderid) 219 | orders.append(order) 220 | 221 | if not use_df: 222 | return orders 223 | else: 224 | return to_df(orders) 225 | 226 | def get_trades(self, vt_orderid: str, use_df: bool = False) -> Sequence[TradeData] | DataFrame | None: 227 | """""" 228 | trades: list = [] 229 | all_trades: list[TradeData] = self.main_engine.get_all_trades() 230 | 231 | for trade in all_trades: 232 | if trade.vt_orderid == vt_orderid: 233 | trades.append(trade) 234 | 235 | if not use_df: 236 | return trades 237 | else: 238 | return to_df(trades) 239 | 240 | def get_all_active_orders(self, use_df: bool = False) -> Sequence[OrderData] | DataFrame | None: 241 | """""" 242 | return get_data(self.main_engine.get_all_active_orders, use_df=use_df) 243 | 244 | def get_contract(self, vt_symbol: str, use_df: bool = False) -> ContractData | None: 245 | """""" 246 | return get_data(self.main_engine.get_contract, arg=vt_symbol, use_df=use_df) 247 | 248 | def get_all_contracts(self, use_df: bool = False) -> Sequence[ContractData] | DataFrame | None: 249 | """""" 250 | return get_data(self.main_engine.get_all_contracts, use_df=use_df) 251 | 252 | def get_account(self, vt_accountid: str, use_df: bool = False) -> AccountData | None: 253 | """""" 254 | return get_data(self.main_engine.get_account, arg=vt_accountid, use_df=use_df) 255 | 256 | def get_all_accounts(self, use_df: bool = False) -> Sequence[AccountData] | DataFrame | None: 257 | """""" 258 | return get_data(self.main_engine.get_all_accounts, use_df=use_df) 259 | 260 | def get_position(self, vt_positionid: str, use_df: bool = False) -> PositionData | None: 261 | """""" 262 | return get_data(self.main_engine.get_position, arg=vt_positionid, use_df=use_df) 263 | 264 | def get_position_by_symbol(self, vt_symbol: str, direction: Direction, use_df: bool = False) -> PositionData | None: 265 | """""" 266 | contract: ContractData = self.main_engine.get_contract(vt_symbol) 267 | if not contract: 268 | return None 269 | 270 | vt_positionid: str = f"{contract.gateway_name}.{contract.vt_symbol}.{direction.value}" 271 | return get_data(self.main_engine.get_position, arg=vt_positionid, use_df=use_df) 272 | 273 | def get_all_positions(self, use_df: bool = False) -> Sequence[PositionData] | DataFrame | None: 274 | """""" 275 | return get_data(self.main_engine.get_all_positions, use_df=use_df) 276 | 277 | def get_bars( 278 | self, 279 | vt_symbol: str, 280 | start_date: str, 281 | interval: Interval, 282 | use_df: bool = False 283 | ) -> Sequence[BarData]: 284 | """""" 285 | contract: ContractData | None = self.main_engine.get_contract(vt_symbol) 286 | if not contract: 287 | return [] 288 | 289 | start: datetime = datetime.strptime(start_date, "%Y%m%d") 290 | end: datetime = datetime.now() 291 | 292 | req: HistoryRequest = HistoryRequest( 293 | symbol=contract.symbol, 294 | exchange=contract.exchange, 295 | start=start, 296 | end=end, 297 | interval=interval 298 | ) 299 | 300 | bars: Sequence[BarData] | DataFrame = get_data(self.datafeed.query_bar_history, arg=req, use_df=use_df) 301 | return bars 302 | 303 | def write_log(self, msg: str) -> None: 304 | """""" 305 | log: LogData = LogData(msg=msg, gateway_name=APP_NAME) 306 | print(f"{log.time}\t{log.msg}") 307 | 308 | event: Event = Event(EVENT_SCRIPT_LOG, log) 309 | self.event_engine.put(event) 310 | 311 | def send_email(self, msg: str) -> None: 312 | """""" 313 | subject: str = "脚本策略引擎通知" 314 | self.main_engine.send_email(subject, msg) 315 | 316 | 317 | def to_df(data_list: Sequence[BaseData]) -> DataFrame | None: 318 | """""" 319 | if not data_list: 320 | return None 321 | 322 | dict_list: list = [data.__dict__ for data in data_list if data] 323 | return DataFrame(dict_list) 324 | 325 | 326 | def get_data(func: Callable, arg: Any = None, use_df: bool = False) -> Any: 327 | """""" 328 | if not arg: 329 | data = func() 330 | else: 331 | data = func(arg) 332 | 333 | if not use_df: 334 | return data 335 | elif data is None: 336 | return data 337 | else: 338 | if not isinstance(data, list): 339 | data = [data] 340 | return to_df(data) 341 | --------------------------------------------------------------------------------