From a259b2eb826f24a938cb5e64a1085296f04f72aa Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Mon, 27 May 2019 16:43:14 +0800 Subject: [PATCH 1/8] Create engine.py --- vnpy/app/risk_manager/engine.py | 198 ++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 vnpy/app/risk_manager/engine.py diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py new file mode 100644 index 00000000..878f4ba4 --- /dev/null +++ b/vnpy/app/risk_manager/engine.py @@ -0,0 +1,198 @@ +# encoding: UTF-8 + +""" +本文件中实现了风控引擎,用于提供一系列常用的风控功能: +1. 委托流控(单位时间内最大允许发出的委托数量) +2. 总成交限制(每日总成交数量限制) +3. 单笔委托的委托数量控制 +""" +import platform +from vnpy.trader.object import LogData, OrderRequest +from vnpy.event import Event, EventEngine, EVENT_TIMER +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_LOG +from vnpy.trader.constant import Status +from vnpy.trader.utility import load_json, save_json + + +class RmEngine(BaseEngine): + """风控引擎""" + setting_filename = "risk_manager_setting.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + self.main_engine = main_engine + self.event_engine = event_engine + main_engine.rmEngine = self + + self.active = False + self.order_flow_count = 0 # 单位时间内委托计数 + self.order_flow_limit = 50 # 委托限制 + self.order_flow_clear = 1 # 计数清空时间(秒) + self.order_flow_timer = 0 # 计数清空时间计时 + self.order_size_limit = 100 # 单笔委托最大限制 + self.trade_count = 0 # 当日成交合约数量统计 + self.trade_limit = 1000 # 当日成交合约数量限制 + self.order_cancel_limit = 10 # 撤单总次数限制 + self.order_cancel_count = {} # 单一合约对应撤单次数的字典 + self.active_order_limit = 20 # 活动合约最大限制 + + self.load_setting() + self.registerEvent() + + def load_setting(self): + """""" + setting = load_json(self.setting_filename) + + self.active = setting["active"] + self.order_flow_limit = setting["order_flow_limit"] + self.order_flow_clear = setting["order_flow_clear"] + self.order_size_limit = setting["order_size_limit"] + self.trade_limit = setting["trade_limit"] + self.active_order_limit = setting["active_order_limit"] + self.order_cancel_limit = setting["order_cancel_limit"] + + def save_setting(self): + """""" + setting = { + "active": self.active, + "order_flow_limit": self.order_flow_limit, + "order_flow_clear": self.order_flow_clear, + "order_size_limit": self.order_size_limit, + "trade_limit": self.trade_limit, + "active_order_limit": self.active_order_limit, + "order_cancel_limit": self.order_cancel_limit, + } + + save_json(self.setting_filename, setting) + + def register_event(self): + """""" + self.event_engine.register(EVENT_TRADE, self.update_trade) + self.event_engine.register(EVENT_TIMER, self.update_timer) + self.event_engine.register(EVENT_ORDER, self.update_order) + + def update_order(self, event): + """""" + order = event.dict_["data"] + if order.status != Status.CANCELLED: + return + + if order.symbol not in self.order_cancel_count: + self.order_cancel_count[order.symbol] = 1 + else: + self.order_cancel_count[order.symbol] += 1 + + def update_trade(self, event): + """""" + trade = event.dict_["data"] + self.trade_count += trade.volume + + def update_timer(self, event): + """""" + self.order_flow_timer += 1 + + if self.order_flow_timer >= self.order_flow_clear: + self.order_flow_count = 0 + self.order_flow_timer = 0 + + def write_risk_log(self, content): + """""" + if platform.uname() == "Windows": + import winsound + winsound.PlaySound("SystemHand", winsound.SND_ASYNC) + + log = LogData() + log.log_content = content + log.gateway_name = self.name + event = Event(type_=EVENT_LOG) + event.dict_["data"] = log + self.event_engine.put(event) + + def checkRisk(self, req: OrderRequest, gateway_name): + """""" + if not self.active: + return True + + # 检查委托数量 + if req.volume <= 0: + self.write_risk_log(f"委托数量必须大于0") + return False + + if req.volume > self.order_size_limit: + self.write_risk_log(f"单笔委托数量{req.volume},超过限制{self.order_size_limit}") + return False + + # 检查成交合约量 + if self.trade_count >= self.trade_limit: + self.write_risk_log(f"今日总成交合约数量{self.trade_count},超过限制{self.trade_limit}") + return False + + # 检查流控 + if self.order_flow_count >= self.order_flow_limit: + self.write_risk_log(f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}") + return False + + # 检查总活动合约 + active_order_count = len(self.main_engine.get_all_active_orders()) + if active_order_count >= self.active_order_limit: + self.write_risk_log(f"当前活动委托数量{active_order_count},超过限制{self.active_order_limit}") + return False + + # 检查撤单次数 + if req.symbol in self.order_cancel_count and self.order_cancel_count[req.symbol] >= self.order_cancel_limit: + self.write_risk_log(f"当日{req.symbol}撤单次数{self.order_cancel_count[req.symbol]},超过限制{self.order_cancel_limit}") + return False + + # 对于通过风控的委托,增加流控计数 + self.order_flow_count += 1 + + return True + + def clear_order_flow_count(self): + """""" + self.order_flow_count = 0 + self.write_risk_log(f"清空流控计数") + + def clear_trade_count(self): + """""" + self.trade_count = 0 + self.write_risk_log(f"清空总成交计数") + + def set_order_flow_limit(self, n): + """""" + self.order_flow_limit = n + + def set_order_flow_clear(self, n): + """""" + self.order_flow_clear = n + + def set_order_size_limit(self, n): + """""" + self.order_size_limit = n + + def set_trade_limit(self, n): + """""" + self.trade_limit = n + + def set_active_order_limit(self, n): + """""" + self.active_order_limit = n + + def set_order_cancel_limit(self, n): + """""" + self.order_cancel_limit = n + + def switch_engine_status(self): + """""" + self.active = not self.active + + if self.active: + self.write_risk_log(f"风险管理功能启动") + else: + self.write_risk_log(f"风险管理功能停止") + + def stop(self): + """""" + self.save_setting() + From 2e619685e7645bec86c77d2e8c98b34399fdb714 Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Tue, 28 May 2019 14:07:19 +0800 Subject: [PATCH 2/8] Update engine.py --- vnpy/app/risk_manager/engine.py | 110 ++++++++++++++------------------ 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py index 878f4ba4..981baf12 100644 --- a/vnpy/app/risk_manager/engine.py +++ b/vnpy/app/risk_manager/engine.py @@ -1,13 +1,6 @@ -# encoding: UTF-8 - -""" -本文件中实现了风控引擎,用于提供一系列常用的风控功能: -1. 委托流控(单位时间内最大允许发出的委托数量) -2. 总成交限制(每日总成交数量限制) -3. 单笔委托的委托数量控制 -""" -import platform -from vnpy.trader.object import LogData, OrderRequest +"""""" +from collections import defaultdict +from vnpy.trader.object import OrderRequest from vnpy.event import Event, EventEngine, EVENT_TIMER from vnpy.trader.engine import BaseEngine, MainEngine from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_LOG @@ -15,7 +8,10 @@ from vnpy.trader.constant import Status from vnpy.trader.utility import load_json, save_json -class RmEngine(BaseEngine): +APP_NAME = "RiskManager" + + +class RiskManagerEngine(BaseEngine): """风控引擎""" setting_filename = "risk_manager_setting.json" @@ -26,16 +22,16 @@ class RmEngine(BaseEngine): main_engine.rmEngine = self self.active = False - self.order_flow_count = 0 # 单位时间内委托计数 - self.order_flow_limit = 50 # 委托限制 - self.order_flow_clear = 1 # 计数清空时间(秒) - self.order_flow_timer = 0 # 计数清空时间计时 - self.order_size_limit = 100 # 单笔委托最大限制 - self.trade_count = 0 # 当日成交合约数量统计 - self.trade_limit = 1000 # 当日成交合约数量限制 - self.order_cancel_limit = 10 # 撤单总次数限制 - self.order_cancel_count = {} # 单一合约对应撤单次数的字典 - self.active_order_limit = 20 # 活动合约最大限制 + self.order_flow_count = 0 + self.order_flow_limit = 50 + self.order_flow_clear = 1 + self.order_flow_timer = 0 + self.order_size_limit = 100 + self.trade_count = 0 + self.trade_limit = 1000 + self.order_cancel_limit = 10 + self.order_cancel_counts = defaultdict(int) + self.active_order_limit = 20 self.load_setting() self.registerEvent() @@ -68,27 +64,23 @@ class RmEngine(BaseEngine): def register_event(self): """""" - self.event_engine.register(EVENT_TRADE, self.update_trade) - self.event_engine.register(EVENT_TIMER, self.update_timer) - self.event_engine.register(EVENT_ORDER, self.update_order) + self.event_engine.register(EVENT_TRADE, self.process_trade_event) + self.event_engine.register(EVENT_TIMER, self.process_timer_event) + self.event_engine.register(EVENT_ORDER, self.process_order_event) - def update_order(self, event): + def process_order_event(self, event: Event): """""" - order = event.dict_["data"] + order = event.data if order.status != Status.CANCELLED: - return - - if order.symbol not in self.order_cancel_count: - self.order_cancel_count[order.symbol] = 1 - else: - self.order_cancel_count[order.symbol] += 1 + return + self.order_cancel_counts[order.symbol] += 1 - def update_trade(self, event): + def process_trade_event(self, event: Event): """""" - trade = event.dict_["data"] + trade = event.data self.trade_count += trade.volume - def update_timer(self, event): + def process_timer_event(self, event: Event): """""" self.order_flow_timer += 1 @@ -96,68 +88,62 @@ class RmEngine(BaseEngine): self.order_flow_count = 0 self.order_flow_timer = 0 - def write_risk_log(self, content): - """""" - if platform.uname() == "Windows": - import winsound - winsound.PlaySound("SystemHand", winsound.SND_ASYNC) - - log = LogData() - log.log_content = content - log.gateway_name = self.name - event = Event(type_=EVENT_LOG) - event.dict_["data"] = log + def write_risk_log(self, msg: str): + """""" + event = Event( + EVENT_LOG, + msg + ) self.event_engine.put(event) - - def checkRisk(self, req: OrderRequest, gateway_name): + + def check_risk(self, req: OrderRequest, gateway_name: str): """""" if not self.active: return True - # 检查委托数量 + # Check order volume if req.volume <= 0: - self.write_risk_log(f"委托数量必须大于0") + self.write_risk_log("委托数量必须大于0") return False if req.volume > self.order_size_limit: self.write_risk_log(f"单笔委托数量{req.volume},超过限制{self.order_size_limit}") return False - # 检查成交合约量 + # Check trade volume if self.trade_count >= self.trade_limit: self.write_risk_log(f"今日总成交合约数量{self.trade_count},超过限制{self.trade_limit}") return False - # 检查流控 + # Check flow count if self.order_flow_count >= self.order_flow_limit: self.write_risk_log(f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}") return False - # 检查总活动合约 + # Check all active orders active_order_count = len(self.main_engine.get_all_active_orders()) if active_order_count >= self.active_order_limit: self.write_risk_log(f"当前活动委托数量{active_order_count},超过限制{self.active_order_limit}") return False - # 检查撤单次数 - if req.symbol in self.order_cancel_count and self.order_cancel_count[req.symbol] >= self.order_cancel_limit: - self.write_risk_log(f"当日{req.symbol}撤单次数{self.order_cancel_count[req.symbol]},超过限制{self.order_cancel_limit}") + # Check order cancel counts + if req.symbol in self.order_cancel_counts and self.order_cancel_counts[req.symbol] >= self.order_cancel_limit: + self.write_risk_log(f"当日{req.symbol}撤单次数{self.order_cancel_counts[req.symbol]},超过限制{self.order_cancel_limit}") return False - # 对于通过风控的委托,增加流控计数 + # Add flow count if pass all checks self.order_flow_count += 1 - return True def clear_order_flow_count(self): """""" self.order_flow_count = 0 - self.write_risk_log(f"清空流控计数") + self.write_risk_log("清空流控计数") def clear_trade_count(self): """""" self.trade_count = 0 - self.write_risk_log(f"清空总成交计数") + self.write_risk_log("清空总成交计数") def set_order_flow_limit(self, n): """""" @@ -188,9 +174,9 @@ class RmEngine(BaseEngine): self.active = not self.active if self.active: - self.write_risk_log(f"风险管理功能启动") + self.write_risk_log("风险管理功能启动") else: - self.write_risk_log(f"风险管理功能停止") + self.write_risk_log("风险管理功能停止") def stop(self): """""" From d76e842a735ef378cbb93f113aa5565f9b9ca6a3 Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Thu, 30 May 2019 15:09:45 +0800 Subject: [PATCH 3/8] Create __init__.py --- vnpy/app/risk_manager/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 vnpy/app/risk_manager/__init__.py diff --git a/vnpy/app/risk_manager/__init__.py b/vnpy/app/risk_manager/__init__.py new file mode 100644 index 00000000..3c51a1e7 --- /dev/null +++ b/vnpy/app/risk_manager/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path +from vnpy.trader.app import BaseApp +from .engine import RiskManagerEngine, APP_NAME + + +class RiskManagerApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "风险控制" + engine_class = RiskManagerEngine + widget_name = "RiskManager" + icon_name = "risk_manager.ico" From a5dff8e948a4e744dd50be12b36ffcaf952d28f7 Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Thu, 30 May 2019 15:11:17 +0800 Subject: [PATCH 4/8] Update engine.py --- vnpy/app/risk_manager/engine.py | 55 ++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py index 981baf12..84fa1c9c 100644 --- a/vnpy/app/risk_manager/engine.py +++ b/vnpy/app/risk_manager/engine.py @@ -1,6 +1,7 @@ """""" + from collections import defaultdict -from vnpy.trader.object import OrderRequest +from vnpy.trader.object import OrderRequest, LogData from vnpy.event import Event, EventEngine, EVENT_TIMER from vnpy.trader.engine import BaseEngine, MainEngine from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_LOG @@ -12,15 +13,16 @@ APP_NAME = "RiskManager" class RiskManagerEngine(BaseEngine): - """风控引擎""" + """""" setting_filename = "risk_manager_setting.json" def __init__(self, main_engine: MainEngine, event_engine: EventEngine): """""" + super().__init__(main_engine, event_engine, APP_NAME) + self.main_engine = main_engine self.event_engine = event_engine - main_engine.rmEngine = self - + self.active = False self.order_flow_count = 0 self.order_flow_limit = 50 @@ -31,22 +33,34 @@ class RiskManagerEngine(BaseEngine): self.trade_limit = 1000 self.order_cancel_limit = 10 self.order_cancel_counts = defaultdict(int) - self.active_order_limit = 20 + self.active_order_limit = 20 + # Patch send order function of MainEngine + self._send_order = self.main_engine.send_order + self.main_engine.send_order = self.send_order + self.load_setting() - self.registerEvent() + self.register_event() + + def send_order(self, req: OrderRequest, gateway_name: str): + """""" + result = self.check_risk(req, gateway_name) + if not result: + return "" + + return self._send_order(req, gateway_name) def load_setting(self): """""" setting = load_json(self.setting_filename) - self.active = setting["active"] - self.order_flow_limit = setting["order_flow_limit"] - self.order_flow_clear = setting["order_flow_clear"] - self.order_size_limit = setting["order_size_limit"] - self.trade_limit = setting["trade_limit"] - self.active_order_limit = setting["active_order_limit"] - self.order_cancel_limit = setting["order_cancel_limit"] + self.active = setting.get("active", self.active) + self.order_flow_limit = setting.get("order_flow_limit", self.order_flow_count) + self.order_flow_clear = setting.get("order_flow_clear", self.order_flow_clear) + self.order_size_limit = setting.get("order_size_limit", self.order_size_limit) + self.trade_limit = setting.get("trade_limit", self.trade_limit) + self.active_order_limit = setting.get("active_order_limit", self.active_order_limit) + self.order_cancel_limit = setting.get("order_cancel_limit", self.order_cancel_limit) def save_setting(self): """""" @@ -89,11 +103,9 @@ class RiskManagerEngine(BaseEngine): self.order_flow_timer = 0 def write_risk_log(self, msg: str): - """""" - event = Event( - EVENT_LOG, - msg - ) + """""" + log = LogData(msg=msg, gateway_name="RiskManager") + event = Event(type=EVENT_LOG, data=log) self.event_engine.put(event) def check_risk(self, req: OrderRequest, gateway_name: str): @@ -117,17 +129,17 @@ class RiskManagerEngine(BaseEngine): # Check flow count if self.order_flow_count >= self.order_flow_limit: - self.write_risk_log(f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}") + self.write_risk_log(f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}次") return False # Check all active orders active_order_count = len(self.main_engine.get_all_active_orders()) if active_order_count >= self.active_order_limit: - self.write_risk_log(f"当前活动委托数量{active_order_count},超过限制{self.active_order_limit}") + self.write_risk_log(f"当前活动委托次数{active_order_count},超过限制{self.active_order_limit}") return False # Check order cancel counts - if req.symbol in self.order_cancel_counts and self.order_cancel_counts[req.symbol] >= self.order_cancel_limit: + if req.symbol in self.order_cancel_counts and self.order_cancel_counts[req.symbol] >= self.order_cancel_limit: self.write_risk_log(f"当日{req.symbol}撤单次数{self.order_cancel_counts[req.symbol]},超过限制{self.order_cancel_limit}") return False @@ -181,4 +193,3 @@ class RiskManagerEngine(BaseEngine): def stop(self): """""" self.save_setting() - From b219f44c754aafb959217ecf4199c8dc23a41ab1 Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Thu, 30 May 2019 15:11:22 +0800 Subject: [PATCH 5/8] Create __init__.py --- vnpy/app/risk_manager/ui/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 vnpy/app/risk_manager/ui/__init__.py diff --git a/vnpy/app/risk_manager/ui/__init__.py b/vnpy/app/risk_manager/ui/__init__.py new file mode 100644 index 00000000..358dbf33 --- /dev/null +++ b/vnpy/app/risk_manager/ui/__init__.py @@ -0,0 +1 @@ +from .widget import RiskManager From 3730bb95ad0f9b0acaa7960fd6b6ab28e2b64afa Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Thu, 30 May 2019 15:11:26 +0800 Subject: [PATCH 6/8] Create risk_manager.ico --- vnpy/app/risk_manager/ui/risk_manager.ico | Bin 0 -> 67646 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 vnpy/app/risk_manager/ui/risk_manager.ico diff --git a/vnpy/app/risk_manager/ui/risk_manager.ico b/vnpy/app/risk_manager/ui/risk_manager.ico new file mode 100644 index 0000000000000000000000000000000000000000..4b376934c3f7d350ca3577567c6e2a39ef2812ed GIT binary patch literal 67646 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 I4|O^Je^F4ot^fc4 literal 0 HcmV?d00001 From 1f7a112302bfa460ed9470574e35743e21fbb2bc Mon Sep 17 00:00:00 2001 From: 1122455801 Date: Thu, 30 May 2019 15:11:32 +0800 Subject: [PATCH 7/8] Create widget.py --- vnpy/app/risk_manager/ui/widget.py | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 vnpy/app/risk_manager/ui/widget.py diff --git a/vnpy/app/risk_manager/ui/widget.py b/vnpy/app/risk_manager/ui/widget.py new file mode 100644 index 00000000..d4baaa18 --- /dev/null +++ b/vnpy/app/risk_manager/ui/widget.py @@ -0,0 +1,116 @@ +from vnpy.event import EventEngine +from vnpy.trader.engine import MainEngine +from vnpy.trader.ui import QtWidgets +from ..engine import APP_NAME + + +class RiskManager(QtWidgets.QWidget): + """""" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.risk_manager_engine = main_engine.get_engine(APP_NAME) + + self.init_ui() + self.update_engine_status() + + def init_ui(self): + """""" + self.setWindowTitle("风险控制") + + # SpinBox + self.order_flow_limit = RiskManagerSpinBox(0) + self.order_flow_clear = RiskManagerSpinBox(0) + self.order_size_limit = RiskManagerSpinBox(0) + self.trade_limit = RiskManagerSpinBox(0) + self.active_order_limit = RiskManagerSpinBox(0) + self.order_cancel_limit = RiskManagerSpinBox(0) + + # Button + self.switch_button = QtWidgets.QPushButton("风控模块未启动") + clear_order_count_button = QtWidgets.QPushButton("清空流控计数") + clear_trade_count_button = QtWidgets.QPushButton("清空总成交计数") + save_setting_button = QtWidgets.QPushButton("保存设置") + + # Grid layout + Label = QtWidgets.QLabel + grid = QtWidgets.QGridLayout() + grid.addWidget(Label("工作状态"), 0, 0) + grid.addWidget(self.switch_button, 0, 1) + grid.addWidget(Label("流控上限"), 1, 0) + grid.addWidget(self.order_flow_limit, 1, 1) + grid.addWidget(Label("流控清空(秒)"), 2, 0) + grid.addWidget(self.order_flow_clear, 2, 1) + grid.addWidget(Label("单笔委托上限"), 3, 0) + grid.addWidget(self.order_size_limit, 3, 1) + grid.addWidget(Label("总成交上限"), 4, 0) + grid.addWidget(self.trade_limit, 4, 1) + grid.addWidget(Label("活动订单上限"), 5, 0) + grid.addWidget(self.active_order_limit, 5, 1) + grid.addWidget(Label("单合约撤单上限"), 6, 0) + grid.addWidget(self.order_cancel_limit, 6, 1) + + # Horizontal box layout + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(clear_order_count_button) + hbox.addWidget(clear_trade_count_button) + hbox.addWidget(save_setting_button) + + # Vertical box layout + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(grid) + vbox.addLayout(hbox) + self.setLayout(vbox) + + # Connect signal to SpinBox + self.order_flow_limit.valueChanged.connect(self.risk_manager_engine.set_order_flow_limit) + self.order_flow_clear.valueChanged.connect(self.risk_manager_engine.set_order_flow_clear) + self.order_size_limit.valueChanged.connect(self.risk_manager_engine.set_order_size_limit) + self.trade_limit.valueChanged.connect(self.risk_manager_engine.set_trade_limit) + self.active_order_limit.valueChanged.connect(self.risk_manager_engine.set_active_order_limit) + self.order_cancel_limit.valueChanged.connect(self.risk_manager_engine.set_order_cancel_limit) + + # Connect signal to button + self.switch_button.clicked.connect(self.switch_engine_status) + clear_order_count_button.clicked.connect(self.risk_manager_engine.clear_order_flow_count) + clear_trade_count_button.clicked.connect(self.risk_manager_engine.clear_trade_count) + save_setting_button.clicked.connect(self.risk_manager_engine.save_setting) + + # Set Fix Size + self.setFixedSize(self.sizeHint()) + + def switch_engine_status(self): + """""" + self.risk_manager_engine.switch_engine_status() + self.update_engine_status() + + def update_engine_status(self): + """""" + if self.risk_manager_engine.active: + self.switch_button.setText("风控模块运行中") + self.order_flow_limit.setValue(self.risk_manager_engine.order_flow_limit) + self.order_flow_clear.setValue(self.risk_manager_engine.order_flow_clear) + self.order_size_limit.setValue(self.risk_manager_engine.order_size_limit) + self.trade_limit.setValue(self.risk_manager_engine.trade_limit) + self.active_order_limit.setValue(self.risk_manager_engine.active_order_limit) + self.order_cancel_limit.setValue(self.risk_manager_engine.order_cancel_limit) + else: + self.switch_button.setText("风控模块未启动") + + self.risk_manager_engine.save_setting() + + +class RiskManagerSpinBox(QtWidgets.QSpinBox): + """""" + + def __init__(self, value): + """""" + super(RiskManagerSpinBox, self).__init__() + + self.setMinimum(0) + self.setMaximum(1000000) + self.setValue(value) From 41bcf7b605a6a1f18b0567ae0f6d2692dd376a52 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Sun, 2 Jun 2019 14:51:15 +0800 Subject: [PATCH 8/8] [Mod] change RiskManager to use QDialog --- tests/trader/run.py | 2 + vnpy/app/risk_manager/__init__.py | 4 +- vnpy/app/risk_manager/engine.py | 169 ++++++++++------------ vnpy/app/risk_manager/ui/risk_manager.ico | Bin 67646 -> 0 bytes vnpy/app/risk_manager/ui/rm.ico | Bin 0 -> 59966 bytes vnpy/app/risk_manager/ui/widget.py | 149 +++++++++---------- vnpy/trader/ui/mainwindow.py | 4 +- 7 files changed, 152 insertions(+), 176 deletions(-) delete mode 100644 vnpy/app/risk_manager/ui/risk_manager.ico create mode 100644 vnpy/app/risk_manager/ui/rm.ico diff --git a/tests/trader/run.py b/tests/trader/run.py index b06b4af4..cf698f0e 100644 --- a/tests/trader/run.py +++ b/tests/trader/run.py @@ -25,6 +25,7 @@ from vnpy.app.csv_loader import CsvLoaderApp from vnpy.app.algo_trading import AlgoTradingApp from vnpy.app.cta_backtester import CtaBacktesterApp from vnpy.app.data_recorder import DataRecorderApp +from vnpy.app.risk_manager import RiskManagerApp def main(): @@ -55,6 +56,7 @@ def main(): main_engine.add_app(CsvLoaderApp) main_engine.add_app(AlgoTradingApp) main_engine.add_app(DataRecorderApp) + main_engine.add_app(RiskManagerApp) main_window = MainWindow(main_engine, event_engine) main_window.showMaximized() diff --git a/vnpy/app/risk_manager/__init__.py b/vnpy/app/risk_manager/__init__.py index 3c51a1e7..0b867f06 100644 --- a/vnpy/app/risk_manager/__init__.py +++ b/vnpy/app/risk_manager/__init__.py @@ -8,7 +8,7 @@ class RiskManagerApp(BaseApp): app_name = APP_NAME app_module = __module__ app_path = Path(__file__).parent - display_name = "风险控制" + display_name = "交易风控" engine_class = RiskManagerEngine widget_name = "RiskManager" - icon_name = "risk_manager.ico" + icon_name = "rm.ico" diff --git a/vnpy/app/risk_manager/engine.py b/vnpy/app/risk_manager/engine.py index 84fa1c9c..35ab4a74 100644 --- a/vnpy/app/risk_manager/engine.py +++ b/vnpy/app/risk_manager/engine.py @@ -15,32 +15,42 @@ APP_NAME = "RiskManager" class RiskManagerEngine(BaseEngine): """""" setting_filename = "risk_manager_setting.json" - + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): """""" super().__init__(main_engine, event_engine, APP_NAME) - + self.main_engine = main_engine self.event_engine = event_engine - + self.active = False - self.order_flow_count = 0 - self.order_flow_limit = 50 - self.order_flow_clear = 1 - self.order_flow_timer = 0 - self.order_size_limit = 100 - self.trade_count = 0 - self.trade_limit = 1000 - self.order_cancel_limit = 10 - self.order_cancel_counts = defaultdict(int) - self.active_order_limit = 20 - - # Patch send order function of MainEngine - self._send_order = self.main_engine.send_order - self.main_engine.send_order = self.send_order + + self.order_flow_count = 0 + self.order_flow_limit = 50 + + self.order_flow_clear = 1 + self.order_flow_timer = 0 + + self.order_size_limit = 100 + + self.trade_count = 0 + self.trade_limit = 1000 + + self.order_cancel_limit = 500 + self.order_cancel_counts = defaultdict(int) + + self.active_order_limit = 50 self.load_setting() self.register_event() + self.patch_send_order() + + def patch_send_order(self): + """ + Patch send order function of MainEngine. + """ + self._send_order = self.main_engine.send_order + self.main_engine.send_order = self.send_order def send_order(self, req: OrderRequest, gateway_name: str): """""" @@ -49,20 +59,23 @@ class RiskManagerEngine(BaseEngine): return "" return self._send_order(req, gateway_name) - - def load_setting(self): - """""" - setting = load_json(self.setting_filename) - self.active = setting.get("active", self.active) - self.order_flow_limit = setting.get("order_flow_limit", self.order_flow_count) - self.order_flow_clear = setting.get("order_flow_clear", self.order_flow_clear) - self.order_size_limit = setting.get("order_size_limit", self.order_size_limit) - self.trade_limit = setting.get("trade_limit", self.trade_limit) - self.active_order_limit = setting.get("active_order_limit", self.active_order_limit) - self.order_cancel_limit = setting.get("order_cancel_limit", self.order_cancel_limit) - - def save_setting(self): + def update_setting(self, setting: dict): + """""" + self.active = setting["active"] + self.order_flow_limit = setting["order_flow_limit"] + self.order_flow_clear = setting["order_flow_clear"] + self.order_size_limit = setting["order_size_limit"] + self.trade_limit = setting["trade_limit"] + self.active_order_limit = setting["active_order_limit"] + self.order_cancel_limit = setting["order_cancel_limit"] + + if self.active: + self.write_log("交易风控功能启动") + else: + self.write_log("交易风控功能停止") + + def get_setting(self): """""" setting = { "active": self.active, @@ -73,27 +86,39 @@ class RiskManagerEngine(BaseEngine): "active_order_limit": self.active_order_limit, "order_cancel_limit": self.order_cancel_limit, } + return setting + def load_setting(self): + """""" + setting = load_json(self.setting_filename) + if not setting: + return + + self.update_setting(setting) + + def save_setting(self): + """""" + setting = self.get_setting() save_json(self.setting_filename, setting) - + def register_event(self): """""" self.event_engine.register(EVENT_TRADE, self.process_trade_event) self.event_engine.register(EVENT_TIMER, self.process_timer_event) self.event_engine.register(EVENT_ORDER, self.process_order_event) - + def process_order_event(self, event: Event): """""" order = event.data if order.status != Status.CANCELLED: - return + return self.order_cancel_counts[order.symbol] += 1 - + def process_trade_event(self, event: Event): """""" trade = event.data self.trade_count += trade.volume - + def process_timer_event(self, event: Event): """""" self.order_flow_timer += 1 @@ -101,9 +126,9 @@ class RiskManagerEngine(BaseEngine): if self.order_flow_timer >= self.order_flow_clear: self.order_flow_count = 0 self.order_flow_timer = 0 - - def write_risk_log(self, msg: str): - """""" + + def write_log(self, msg: str): + """""" log = LogData(msg=msg, gateway_name="RiskManager") event = Event(type=EVENT_LOG, data=log) self.event_engine.put(event) @@ -115,81 +140,39 @@ class RiskManagerEngine(BaseEngine): # Check order volume if req.volume <= 0: - self.write_risk_log("委托数量必须大于0") + self.write_log("委托数量必须大于0") return False - + if req.volume > self.order_size_limit: - self.write_risk_log(f"单笔委托数量{req.volume},超过限制{self.order_size_limit}") + self.write_log( + f"单笔委托数量{req.volume},超过限制{self.order_size_limit}") return False # Check trade volume if self.trade_count >= self.trade_limit: - self.write_risk_log(f"今日总成交合约数量{self.trade_count},超过限制{self.trade_limit}") + self.write_log( + f"今日总成交合约数量{self.trade_count},超过限制{self.trade_limit}") return False # Check flow count if self.order_flow_count >= self.order_flow_limit: - self.write_risk_log(f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}次") + self.write_log( + f"委托流数量{self.order_flow_count},超过限制每{self.order_flow_clear}秒{self.order_flow_limit}次") return False # Check all active orders active_order_count = len(self.main_engine.get_all_active_orders()) if active_order_count >= self.active_order_limit: - self.write_risk_log(f"当前活动委托次数{active_order_count},超过限制{self.active_order_limit}") + self.write_log( + f"当前活动委托次数{active_order_count},超过限制{self.active_order_limit}") return False # Check order cancel counts - if req.symbol in self.order_cancel_counts and self.order_cancel_counts[req.symbol] >= self.order_cancel_limit: - self.write_risk_log(f"当日{req.symbol}撤单次数{self.order_cancel_counts[req.symbol]},超过限制{self.order_cancel_limit}") + if req.symbol in self.order_cancel_counts and self.order_cancel_counts[req.symbol] >= self.order_cancel_limit: + self.write_log( + f"当日{req.symbol}撤单次数{self.order_cancel_counts[req.symbol]},超过限制{self.order_cancel_limit}") return False # Add flow count if pass all checks self.order_flow_count += 1 return True - - def clear_order_flow_count(self): - """""" - self.order_flow_count = 0 - self.write_risk_log("清空流控计数") - - def clear_trade_count(self): - """""" - self.trade_count = 0 - self.write_risk_log("清空总成交计数") - - def set_order_flow_limit(self, n): - """""" - self.order_flow_limit = n - - def set_order_flow_clear(self, n): - """""" - self.order_flow_clear = n - - def set_order_size_limit(self, n): - """""" - self.order_size_limit = n - - def set_trade_limit(self, n): - """""" - self.trade_limit = n - - def set_active_order_limit(self, n): - """""" - self.active_order_limit = n - - def set_order_cancel_limit(self, n): - """""" - self.order_cancel_limit = n - - def switch_engine_status(self): - """""" - self.active = not self.active - - if self.active: - self.write_risk_log("风险管理功能启动") - else: - self.write_risk_log("风险管理功能停止") - - def stop(self): - """""" - self.save_setting() diff --git a/vnpy/app/risk_manager/ui/risk_manager.ico b/vnpy/app/risk_manager/ui/risk_manager.ico deleted file mode 100644 index 4b376934c3f7d350ca3577567c6e2a39ef2812ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67646 zcmeI5eT-d28OCodhGL-#LJ^xCmk*Yw}RDR6W9-;06|fj58#U&xu3N>rESkn$|B1pE~!p10Da z)5k}!X%PGbydN9^f*-W$tr^ayP(BBIA3O&-t?5MD+m5Xdfh)mLAoxKGKge=K`5oXk zun|mDhi-rGb!=Y=E&~T$BebBT5{KcXVQf%79xMeujrUf5rfbIg!G%CEE$wq9L>{A) zHV@v3Hxeh0h_RN4pZ{*%`n%k4ntO<%Qb$8Py}AE;a`pwFq_C;1PM zJ`UR9uMD5n+)_W#{cOs~7@@KhZ`=JOulpn2&%FxDpyx#TpnJyO0$uBt+6V0Zk)H|f z2AY$d2ybQkj^?T7gHrl{-7oT|gGWHwG@NXIUWAitL8-0@=uhg?UMS_w*OR~92lD5GHK458<<&3EZGR1D{W|smAN4o3C+Qs0r$AmDmAxL#8)^RM*L}dU zRr4CZ0#(tfsDA3cKyzfy`+#Mm&Re&DqByBqy_z3L=ZIX40hadi)5YL5P&J*3?C%5c z`4$kz0oQ!M*hQ*6vl~E>Tui#|z9+Xu3oI;j++O(9TyZ~0=Z9SJ0c?tWl2rcb{M_CZ zPY*>)*6oOv{;Q6U{(U&|zz4Kuw7~G~g8s&qB)x(30GRanEUJz^Z2RQe;b_yI1JT}* zC?bi|JO8&Ujt51xn`6CNJ5KixblV58#f*K${TFiVXCXc${Xd2NTlXQp_4@Ji-smr0 zw1=eY=K*Xm+G{QMK2V5edDdGJ{k4xsd&y${yA=aW`=lQPF7C-b$RCaVuMqeZ0(1^g zdY510U;Wa4!fBu@K497E#xsmA_afUzf}o#jI7gjax$ z#sJeEsn#aK9uaw3ubIDe4?x>L)!JtOH@}yTO6hhOPsJo&!w%q}PC= zpR*Bst3BV+GbrNupn}K0&NKG_ul4b8{kIk0o6zKJP)H0g^^%?lypH`^|NSa!znTO5 zI#BvC|K|Ui`GcNERJ8t!Xk#~M*1q%anf2e7ng7??@9qO5q$80>>%Zf1d@s68l@H|o zQk@BIA|1~Lr;nNSpKX4x|Bl7+vuLAdqU9~W(O%aVzb}LSTea|Ky*yk0A4`8dXQWs9 z3wgehI(tZ;17o%C{QGA7msEf8|G7k~+~ha)kKZ`LpzwCx z&-g^pf5Q6D5BfVF`*m$rRQ$h)x_6jy=AfwYe+Js?SyVxP zw>cQFt!Aoc~8?-O<7~fD`3BExZ^mm(szoF4_(kri5qP=|JD&Y0seO2K6 z{~#L0za3D3{^wHnX5eiMsDS?8L!UTa=cT{uN$Nb7^y#3#v;MExe08E#p7I<0^|znD z4Ej5(|0`(ot^)O!A6y^wcUJ$^Xfs3l=lxQh3C<_o3cS1qDEayS2S%s7^;dmK-SRK_ zSo*~eHrg1iibzcxq_e;V;FS-QRR6CSU5e1$ zuG6$bs`bDhf}nqQIneVsG#2ByY-?3i-n2z}0eBU7;{zoe|F>Ft6eW2>y;ggNk^gzn zzia-#5$+3_?`;sgNx|}g3(0#E14=gjZ-aZChdUPgZC6Oz)tb;9z>9N0iS*ZbKi=mf z9lG&Sy9v%D-2j~TffDJz)bQU8?M<7;22$OFi~y&7pd|Y1e*bvTmAG%TS88k^)&B5D zLD0Wg4z|PbWuWWY8$U3%kj@28UkjFC{NE18y6#yH>+`N~Ee_O8~x&Qm0+S_%p(O#)*FUvOF4`@Bwxfo!6|8vdueX06- z2E2X)K)(s$qRrT(zv){7;&HFk#g}a#`AdL&);Uh)154KJh@KqY7yW0~{%FOn^??1E&M0<9;(Av}~S6Uh~w>-VShgQYemMv} zaA>}!zp>IjdvCRu57_pT*PcP`Pj~WMfL*S{fG4nJ4oLYk4=AcqirMpbY1Pee`1$_z=+jZ`#+A%i4DLY;MSFU$OR< zuK+&!f#$hZgO7uTYkSIVb5o@+h#w1@FSr0`-;=JFyBWg|rETrc({E&73yuMHU#cYI zr23lWjk5NZod&)D{s@M_q3F?x&nU+0xAE=(x{jR=g6@;@fYC2!c}&V$OSlN=IV6g) zTRt^zlLUjRL~T+gl_1TO-`i=E(gum?!nA)wzk z)Nfq=6RZNi1~-FGgSnswG+e_|PPH$t$D!u&7_c>Hl2=UDv+Ly-iqU5Pt*^cp%m><2 zD{XbJq50@j!3p3)7xwFL zt=3~c5P#ijVIV$SuVA!Tt0kXLs!J`NROL92+oL|9o5?sF!1dta40YfBkq|t`|ocvCGS%$TBxJ zxS`w|MV9$-2O7#f4c&2Mf*B8pU z^O5Cy+;S5=2W@>gK5n^*|788Gp-fE5mb3ocP{#kW;~_hK8p@2f$hJRQ=5PbynQ}ay z2kI5XWs3mlXdr~{H*_ctxmE|_vekheecm#-H&b4wvekjOST{H}$1)%`$1)%`$BG!q z86Y*FceK8=fn`a#f3(L*^=Si1^=XAkb*Ut#I%9EC8_9qwOuwE~&{IDicgZS7aAqI2 I4|O^Je^F4ot^fc4 diff --git a/vnpy/app/risk_manager/ui/rm.ico b/vnpy/app/risk_manager/ui/rm.ico new file mode 100644 index 0000000000000000000000000000000000000000..751b82558e4cd567162afefd3e5e3255c8003487 GIT binary patch literal 59966 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