Merge pull request #2172 from vnpy/dev-editor

Dev editor
This commit is contained in:
vn.py 2019-11-01 10:40:04 +08:00 committed by GitHub
commit 523ff044c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 519 additions and 5 deletions

View File

@ -19,3 +19,4 @@ ibapi
deap deap
pyzmq pyzmq
wmi wmi
QScintilla

View File

@ -4,6 +4,7 @@ import traceback
from datetime import datetime from datetime import datetime
from threading import Thread from threading import Thread
from pathlib import Path from pathlib import Path
from inspect import getfile
from vnpy.event import Event, EventEngine from vnpy.event import Event, EventEngine
from vnpy.trader.engine import BaseEngine, MainEngine from vnpy.trader.engine import BaseEngine, MainEngine
@ -116,6 +117,12 @@ class BacktesterEngine(BaseEngine):
msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}" msg = f"策略文件{module_name}加载失败,触发异常:\n{traceback.format_exc()}"
self.write_log(msg) self.write_log(msg)
def reload_strategy_class(self):
""""""
self.classes.clear()
self.load_strategy_class()
self.write_log("策略文件重载刷新完成")
def get_strategy_class_names(self): def get_strategy_class_names(self):
"""""" """"""
return list(self.classes.keys()) return list(self.classes.keys())
@ -425,3 +432,9 @@ class BacktesterEngine(BaseEngine):
def get_history_data(self): def get_history_data(self):
"""""" """"""
return self.backtesting_engine.history_data return self.backtesting_engine.history_data
def get_strategy_class_file(self, class_name: str):
""""""
strategy_class = self.classes[class_name]
file_path = getfile(strategy_class)
return file_path

View File

@ -13,6 +13,7 @@ from vnpy.trader.constant import Interval, Direction
from vnpy.trader.engine import MainEngine from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import QtCore, QtWidgets, QtGui from vnpy.trader.ui import QtCore, QtWidgets, QtGui
from vnpy.trader.ui.widget import BaseMonitor, BaseCell, DirectionCell, EnumCell from vnpy.trader.ui.widget import BaseMonitor, BaseCell, DirectionCell, EnumCell
from vnpy.trader.ui.editor import CodeEditor
from vnpy.event import Event, EventEngine from vnpy.event import Event, EventEngine
from vnpy.chart import ChartWidget, CandleItem, VolumeItem from vnpy.chart import ChartWidget, CandleItem, VolumeItem
@ -117,6 +118,12 @@ class BacktesterManager(QtWidgets.QWidget):
self.candle_button.clicked.connect(self.show_candle_chart) self.candle_button.clicked.connect(self.show_candle_chart)
self.candle_button.setEnabled(False) self.candle_button.setEnabled(False)
edit_button = QtWidgets.QPushButton("代码编辑")
edit_button.clicked.connect(self.edit_strategy_code)
reload_button = QtWidgets.QPushButton("策略重载")
reload_button.clicked.connect(self.reload_strategy_class)
for button in [ for button in [
backtesting_button, backtesting_button,
optimization_button, optimization_button,
@ -125,7 +132,9 @@ class BacktesterManager(QtWidgets.QWidget):
self.order_button, self.order_button,
self.trade_button, self.trade_button,
self.daily_button, self.daily_button,
self.candle_button self.candle_button,
edit_button,
reload_button
]: ]:
button.setFixedHeight(button.sizeHint().height() * 2) button.setFixedHeight(button.sizeHint().height() * 2)
@ -142,18 +151,24 @@ class BacktesterManager(QtWidgets.QWidget):
form.addRow("回测资金", self.capital_line) form.addRow("回测资金", self.capital_line)
form.addRow("合约模式", self.inverse_combo) form.addRow("合约模式", self.inverse_combo)
result_grid = QtWidgets.QGridLayout()
result_grid.addWidget(self.trade_button, 0, 0)
result_grid.addWidget(self.order_button, 0, 1)
result_grid.addWidget(self.daily_button, 1, 0)
result_grid.addWidget(self.candle_button, 1, 1)
left_vbox = QtWidgets.QVBoxLayout() left_vbox = QtWidgets.QVBoxLayout()
left_vbox.addLayout(form) left_vbox.addLayout(form)
left_vbox.addWidget(backtesting_button) left_vbox.addWidget(backtesting_button)
left_vbox.addWidget(downloading_button) left_vbox.addWidget(downloading_button)
left_vbox.addStretch() left_vbox.addStretch()
left_vbox.addWidget(self.trade_button) left_vbox.addLayout(result_grid)
left_vbox.addWidget(self.order_button)
left_vbox.addWidget(self.daily_button)
left_vbox.addWidget(self.candle_button)
left_vbox.addStretch() left_vbox.addStretch()
left_vbox.addWidget(optimization_button) left_vbox.addWidget(optimization_button)
left_vbox.addWidget(self.result_button) left_vbox.addWidget(self.result_button)
left_vbox.addStretch()
left_vbox.addWidget(edit_button)
left_vbox.addWidget(reload_button)
# Result part # Result part
self.statistics_monitor = StatisticsMonitor() self.statistics_monitor = StatisticsMonitor()
@ -197,6 +212,9 @@ class BacktesterManager(QtWidgets.QWidget):
hbox.addWidget(self.chart) hbox.addWidget(self.chart)
self.setLayout(hbox) self.setLayout(hbox)
# Code Editor
self.editor = CodeEditor(self.main_engine, self.event_engine)
def register_event(self): def register_event(self):
"""""" """"""
self.signal_log.connect(self.process_log_event) self.signal_log.connect(self.process_log_event)
@ -403,6 +421,21 @@ class BacktesterManager(QtWidgets.QWidget):
self.candle_dialog.exec_() self.candle_dialog.exec_()
def edit_strategy_code(self):
""""""
class_name = self.class_combo.currentText()
file_path = self.backtester_engine.get_strategy_class_file(class_name)
self.editor.open_editor(file_path)
self.editor.show()
def reload_strategy_class(self):
""""""
self.backtester_engine.reload_strategy_class()
self.class_combo.clear()
self.init_strategy_settings()
def show(self): def show(self):
"""""" """"""
self.showMaximized() self.showMaximized()

454
vnpy/trader/ui/editor.py Normal file
View File

@ -0,0 +1,454 @@
from typing import Callable, Dict
from pathlib import Path
from PyQt5 import QtWidgets, Qsci, QtGui
class CodeEditor(QtWidgets.QMainWindow):
""""""
NEW_FILE_NAME = "Untitled"
_instance = None
def __new__(cls, *args, **kwargs):
""""""
if not cls._instance:
cls._instance = QtWidgets.QMainWindow.__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self, main_engine=None, event_engine=None):
""""""
super().__init__()
self.new_file_count = 0
self.editor_path_map: Dict[Qsci.QsciScintilla, str] = {}
self.init_ui()
def init_ui(self):
""""""
self.setWindowTitle("策略编辑器")
self.init_menu()
self.init_central()
def init_central(self):
""""""
self.tab = QtWidgets.QTabWidget()
self.tab.currentChanged.connect(self.update_path_label)
self.path_label = QtWidgets.QLabel()
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.tab)
vbox.addWidget(self.path_label)
widget = QtWidgets.QWidget()
widget.setLayout(vbox)
self.setCentralWidget(widget)
def init_menu(self):
""""""
bar = self.menuBar()
file_menu = bar.addMenu("文件")
self.add_menu_action(file_menu, "新建文件", self.new_file, "Ctrl+N")
self.add_menu_action(file_menu, "打开文件", self.open_file, "Ctrl+O")
self.add_menu_action(file_menu, "关闭文件", self.close_editor, "Ctrl+W")
file_menu.addSeparator()
self.add_menu_action(file_menu, "保存", self.save_file, "Ctrl+S")
self.add_menu_action(
file_menu,
"另存为",
self.save_file_as,
"Ctrl+Shift+S"
)
file_menu.addSeparator()
self.add_menu_action(file_menu, "退出", self.close)
edit_menu = bar.addMenu("编辑")
self.add_menu_action(edit_menu, "撤销", self.undo, "Ctrl+Z")
self.add_menu_action(edit_menu, "恢复", self.redo, "Ctrl+Y")
edit_menu.addSeparator()
self.add_menu_action(edit_menu, "复制", self.copy, "Ctrl+C")
self.add_menu_action(edit_menu, "粘贴", self.paste, "Ctrl+P")
self.add_menu_action(edit_menu, "剪切", self.cut, "Ctrl+X")
edit_menu.addSeparator()
self.add_menu_action(edit_menu, "查找", self.find, "Ctrl+F")
self.add_menu_action(edit_menu, "替换", self.replace, "Ctrl+H")
def add_menu_action(
self,
menu: QtWidgets.QMenu,
action_name: str,
func: Callable,
shortcut: str = "",
):
""""""
action = QtWidgets.QAction(action_name, self)
action.triggered.connect(func)
menu.addAction(action)
if shortcut:
sequence = QtGui.QKeySequence(shortcut)
action.setShortcut(sequence)
def new_editor(self):
""""""
# Create editor object
editor = Qsci.QsciScintilla()
# Set editor font
font = QtGui.QFont()
font.setFamily('Consolas')
font.setFixedPitch(True)
font.setPointSize(10)
editor.setFont(font)
editor.setMarginsFont(font)
# Set margin for line numbers
font_metrics = QtGui.QFontMetrics(font)
editor.setMarginWidth(0, font_metrics.width("00000") + 6)
editor.setMarginLineNumbers(0, True)
editor.setMarginsBackgroundColor(QtGui.QColor("#cccccc"))
# Set brace matching
editor.setBraceMatching(Qsci.QsciScintilla.SloppyBraceMatch)
# Hide horizontal scroll bar
editor.SendScintilla(Qsci.QsciScintilla.SCI_SETHSCROLLBAR, 0)
# Set current line color
editor.setCaretLineVisible(True)
editor.setCaretLineBackgroundColor(QtGui.QColor("#ffe4e4"))
# Set Python lexer
lexer = Qsci.QsciLexerPython()
lexer.setDefaultFont(font)
editor.setLexer(lexer)
# Add minimum editor size
editor.setMinimumSize(600, 450)
# Enable auto complete
editor.setAutoCompletionSource(Qsci.QsciScintilla.AcsAll)
editor.setAutoCompletionThreshold(2)
editor.setAutoCompletionCaseSensitivity(False)
editor.setAutoCompletionReplaceWord(False)
# Use space indentation
editor.setIndentationsUseTabs(False)
editor.setTabWidth(4)
editor.setIndentationGuides(True)
# Enable folding
editor.setFolding(True)
return editor
def open_editor(self, file_path: str = ""):
""""""
# Show editor tab if file already opened
if file_path:
file_path = str(Path(file_path))
for editor, path in self.editor_path_map.items():
if file_path == path:
editor.show()
return
# Otherwise create new editor
editor = self.new_editor()
if file_path:
buf = open(file_path, encoding="UTF8").read()
editor.setText(buf)
file_name = Path(file_path).name
self.editor_path_map[editor] = file_path
else:
self.new_file_count += 1
file_name = f"{self.NEW_FILE_NAME}-{self.new_file_count}"
self.editor_path_map[editor] = file_name
i = self.tab.addTab(editor, file_name)
self.tab.setCurrentIndex(i)
self.path_label.setText(file_path)
def close_editor(self):
""""""
i = self.tab.currentIndex()
# Close editor if last file closed
if not i:
self.close()
# Otherwise only close current tab
else:
self.save_file()
editor = self.get_active_editor()
self.editor_path_map.pop(editor)
self.tab.removeTab(i)
def open_file(self):
""""""
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "打开文件", "", "Python(*.py)")
if file_path:
self.open_editor(file_path)
def new_file(self):
""""""
self.open_editor("")
def save_file(self):
""""""
editor = self.get_active_editor()
file_path = self.editor_path_map[editor]
if self.NEW_FILE_NAME in file_path:
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存", "", "Python(*.py)")
self.save_editor_text(editor, file_path)
def save_file_as(self):
""""""
editor = self.get_active_editor()
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存", "", "Python(*.py)")
self.save_editor_text(editor, file_path)
def save_editor_text(self, editor: Qsci.QsciScintilla, file_path: str):
""""""
if file_path:
self.editor_path_map[editor] = file_path
i = self.tab.currentIndex()
file_name = Path(file_path).name
self.tab.setTabText(i, file_name)
with open(file_path, "w", encoding="UTF8") as f:
f.write(editor.text())
self.update_path_label()
def copy(self):
""""""
self.get_active_editor().copy()
def paste(self):
""""""
self.get_active_editor().paste()
def undo(self):
""""""
self.get_active_editor().undo()
def redo(self):
""""""
self.get_active_editor().redo()
def cut(self):
""""""
self.get_active_editor().cut()
def find(self):
""""""
dialog = FindDialog(
self.get_active_editor(),
False
)
dialog.exec_()
def replace(self):
""""""
dialog = FindDialog(
self.get_active_editor(),
True
)
dialog.exec_()
def get_active_editor(self):
""""""
return self.tab.currentWidget()
def closeEvent(self, event):
""""""
for editor, path in self.editor_path_map.items():
i = QtWidgets.QMessageBox.question(
self,
"退出保存",
f"是否要保存{path}",
QtWidgets.QMessageBox.Save | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel,
QtWidgets.QMessageBox.Save
)
if i == QtWidgets.QMessageBox.Save:
if self.NEW_FILE_NAME in path:
path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "保存", "", "Python(*.py)")
if path:
self.save_editor_text(editor, path)
elif i == QtWidgets.QMessageBox.Cancel:
break
event.accept()
def show(self):
""""""
if not self.tab.count():
self.open_editor()
self.showMaximized()
def update_path_label(self):
""""""
editor = self.get_active_editor()
path = self.editor_path_map[editor]
self.path_label.setText(path)
class FindDialog(QtWidgets.QDialog):
""""""
def __init__(
self,
editor: Qsci.QsciScintilla,
show_replace: bool = False
):
""""""
super().__init__()
self.editor: Qsci.QsciScintilla = editor
self.show_replace: bool = show_replace
self.new_task: bool = True
self.init_ui()
def init_ui(self):
""""""
find_label = QtWidgets.QLabel("查找")
replace_label = QtWidgets.QLabel("替换")
selected_text = self.editor.selectedText()
self.find_line = QtWidgets.QLineEdit(selected_text)
self.find_line.textChanged.connect(self.reset_task)
self.replace_line = QtWidgets.QLineEdit()
self.case_check = QtWidgets.QCheckBox("大小写")
self.case_check.setChecked(True)
self.case_check.stateChanged.connect(self.reset_task)
self.whole_check = QtWidgets.QCheckBox("全词匹配")
self.whole_check.stateChanged.connect(self.reset_task)
self.selection_check = QtWidgets.QCheckBox("选中区域")
self.selection_check.stateChanged.connect(self.reset_task)
find_button = QtWidgets.QPushButton("查找")
find_button.clicked.connect(self.find_text)
self.replace_button = QtWidgets.QPushButton("替换")
self.replace_button.clicked.connect(self.replace_text)
self.replace_button.setEnabled(False)
check_hbox = QtWidgets.QHBoxLayout()
check_hbox.addWidget(self.case_check)
check_hbox.addStretch()
check_hbox.addWidget(self.whole_check)
check_hbox.addStretch()
check_hbox.addWidget(self.selection_check)
check_hbox.addStretch()
button_hbox = QtWidgets.QHBoxLayout()
button_hbox.addWidget(find_button)
button_hbox.addWidget(self.replace_button)
form = QtWidgets.QFormLayout()
form.addRow(find_label, self.find_line)
form.addRow(replace_label, self.replace_line)
form.addRow(check_hbox)
form.addRow(button_hbox)
self.setLayout(form)
if self.show_replace:
self.setWindowTitle("替换")
else:
self.setWindowTitle("查找")
replace_label.setVisible(False)
self.replace_line.setVisible(False)
self.replace_button.setVisible(False)
def find_text(self):
""""""
if not self.new_task:
result = self.editor.findNext()
if result:
self.new_task = False
self.replace_button.setEnabled(True)
return
else:
self.new_task = True
self.editor.cancelFind()
if not self.selection_check.isChecked():
result = self.editor.findFirst(
self.find_line.text(),
False,
self.case_check.isChecked(),
self.whole_check.isChecked(),
False,
line=1,
index=1
)
else:
result = self.editor.findFirstInSelection(
self.find_line.text(),
False,
self.case_check.isChecked(),
self.whole_check.isChecked(),
False
)
if result:
self.new_task = False
self.replace_button.setEnabled(True)
else:
self.new_task = True
def replace_text(self):
""""""
new_text = self.replace_line.text()
self.editor.replace(new_text)
self.editor.findNext()
def reset_task(self):
""""""
self.new_task = True
self.replace_button.setEnabled(False)
if __name__ == "__main__":
from vnpy.trader.ui import create_qapp
app = create_qapp()
editor = CodeEditor()
editor.show()
app.exec_()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -24,6 +24,7 @@ from .widget import (
AboutDialog, AboutDialog,
GlobalDialog GlobalDialog
) )
from .editor import CodeEditor
from ..engine import MainEngine from ..engine import MainEngine
from ..utility import get_icon_path, TRADER_DIR from ..utility import get_icon_path, TRADER_DIR
@ -138,6 +139,18 @@ class MainWindow(QtWidgets.QMainWindow):
partial(self.open_widget, ContractManager, "contract") partial(self.open_widget, ContractManager, "contract")
) )
self.add_menu_action(
help_menu,
"代码编辑",
"editor.ico",
partial(self.open_widget, CodeEditor, "editor")
)
self.add_toolbar_action(
"代码编辑",
"editor.ico",
partial(self.open_widget, CodeEditor, "editor")
)
self.add_menu_action( self.add_menu_action(
help_menu, "还原窗口", "restore.ico", self.restore_window_setting help_menu, "还原窗口", "restore.ico", self.restore_window_setting
) )