From c0d15ad9cbacef31d701ac934726482b70b70ce2 Mon Sep 17 00:00:00 2001 From: "vn.py" Date: Wed, 13 Nov 2019 14:34:26 +0800 Subject: [PATCH] [Add] new portfolio manager app --- vnpy/app/portfolio_manager/__init__.py | 16 + vnpy/app/portfolio_manager/engine.py | 289 +++++++ vnpy/app/portfolio_manager/ui/__init__.py | 1 + vnpy/app/portfolio_manager/ui/portfolio.ico | Bin 0 -> 67646 bytes vnpy/app/portfolio_manager/ui/widget.py | 845 ++++++++++++++++++++ 5 files changed, 1151 insertions(+) create mode 100644 vnpy/app/portfolio_manager/__init__.py create mode 100644 vnpy/app/portfolio_manager/engine.py create mode 100644 vnpy/app/portfolio_manager/ui/__init__.py create mode 100644 vnpy/app/portfolio_manager/ui/portfolio.ico create mode 100644 vnpy/app/portfolio_manager/ui/widget.py diff --git a/vnpy/app/portfolio_manager/__init__.py b/vnpy/app/portfolio_manager/__init__.py new file mode 100644 index 00000000..e6b84909 --- /dev/null +++ b/vnpy/app/portfolio_manager/__init__.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from vnpy.trader.app import BaseApp + +from .engine import PortfolioEngine, APP_NAME + + +class DataRecorderApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "投资组合" + engine_class = PortfolioEngine + widget_name = "PortfolioManager" + icon_name = "portfolio.ico" diff --git a/vnpy/app/portfolio_manager/engine.py b/vnpy/app/portfolio_manager/engine.py new file mode 100644 index 00000000..556b2ea8 --- /dev/null +++ b/vnpy/app/portfolio_manager/engine.py @@ -0,0 +1,289 @@ +from datetime import datetime +from typing import Dict, List, Set +from collections import defaultdict + +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.event import EVENT_TRADE, EVENT_ORDER, EVENT_TICK +from vnpy.trader.constant import Direction, Offset, OrderType +from vnpy.trader.object import ( + OrderRequest, CancelRequest, SubscribeRequest, + OrderData, TradeData, TickData +) +from vnpy.trader.utility import load_json, save_json + + +APP_NAME = "PortfolioManager" + +EVENT_PORTFOLIO_UPDATE = "ePortfioUpdate" + + +class PortfolioEngine(BaseEngine): + """""" + setting_filename = "portfolio_manager_setting.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.strategies: Dict[str, PortfolioStrategy] = {} + self.symbol_strategy_map: Dict[str, List] = defaultdict(list) + self.order_strategy_map: Dict[str, PortfolioStrategy] = {} + self.active_orders: Set[str] = set() + + self.register_event() + self.load_setting() + + def load_setting(self): + """""" + setting: dict = load_json(self.setting_filename) + + for d in setting.values(): + self.add_strategy( + d["name"], + d["vt_symbol"], + d["size"], + d["net_pos"], + d["open_price"], + d["last_price"], + d["create_time"], + d["note_text"], + ) + + def save_setting(self): + """""" + setting: dict = {} + + for strategy in self.strategies: + setting[strategy.name] = { + "name": strategy.name, + "vt_symbol": strategy.vt_symbol, + "size": strategy.size, + "net_pos": strategy.net_pos, + "open_price": strategy.open_price, + "last_price": strategy.last_price, + "create_time": strategy.create_time, + "note_text": strategy.note_text, + } + + save_json(self.setting_filename, setting) + + def register_event(self): + """""" + self.event_engine.register(EVENT_ORDER, self.process_order_event) + self.event_engine.register(EVENT_TRADE, self.process_trade_event) + self.event_engine.register(EVENT_TICK, self.process_tick_event) + + def process_order_event(self, event: Event): + """""" + order: OrderData = event.data + + if order.vt_orderid not in self.active_orders: + return + + if not order.is_active(): + self.active_orders.remove(order.vt_orderid) + + def process_trade_event(self, event: Event): + """""" + trade: TradeData = event.data + + strategy: PortfolioStrategy = self.order_strategy_map.get( + trade.vt_orderid) + if strategy: + strategy.update_trade( + trade.trade_direction, + trade.trade_volume, + trade.trade_price + ) + + self.put_strategy_event(strategy.name) + + def process_tick_event(self, event: Event): + """""" + tick: TickData = event.data + + strategies: List = self.symbol_strategy_map[tick.vt_symbol] + for strategy in strategies: + strategy.update_price(tick.last_price) + + self.put_strategy_event(strategy.name) + + def add_strategy( + self, + name: str, + vt_symbol: str, + size: int, + net_pos: int = 0, + open_price: float = 0, + last_price: float = 0, + create_time: str = "", + note_text: str = "" + ): + """""" + if name in self.strategies: + return False + + strategy = PortfolioStrategy( + name, + vt_symbol, + size, + net_pos, + open_price, + last_price, + create_time, + note_text + ) + + self.strategies[strategy.name] = strategy + self.symbol_strategy_map[strategy.vt_symbol].append(strategy) + + self.subscribe_data(vt_symbol) + + def remove_strategy(self, name: str): + """""" + if name not in self.strategies: + return False + + strategy = self.strategies.pop(name) + self.symbol_strategy_map[strategy.vt_symbol].remove(strategy) + + return True + + def subscribe_data(self, vt_symbol: str): + """""" + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return + + req = SubscribeRequest( + symbol=contract.symbol, + exchange=contract.exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + + def send_order( + self, + name: str, + price: float, + volume: int, + direction: Direction, + offset: Offset = Offset.NONE + ): + """""" + strategy = self.strategies[name] + vt_symbol = strategy.vt_symbol + + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return False + + req = OrderRequest( + symbol=contract.symbol, + exchange=contract.exchange, + direction=direction, + type=OrderType.LIMIT, + volume=volume, + price=price, + offset=offset + ) + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + + self.order_strategy_map[vt_orderid] = strategy + self.active_orders.add(vt_orderid) + + return True + + def cancel_order(self, vt_orderid: str): + """""" + if vt_orderid not in self.order_strategy_map: + return False + + order = self.main_engine.get_order(vt_orderid) + + req = CancelRequest( + orderid=order.orderid, + symbol=order.symbol, + exchange=order.exchange + ) + self.main_engine.cancel_order(req, order.gateway_name) + + return True + + def cancel_all(self, name: str): + """""" + for vt_orderid in self.active_orders: + strategy = self.order_symbol_map[vt_orderid] + if strategy.name == name: + self.cancel_order(vt_orderid) + + def put_strategy_event(self, name: str): + """""" + strategy = self.strategies[name] + event = Event(EVENT_PORTFOLIO_UPDATE, strategy) + self.event_engine.put(event) + + +class PortfolioStrategy: + """""" + + def __init__( + self, + name: str, + vt_symbol: str, + size: int, + net_pos: int, + open_price: float, + last_price: float, + create_time: str, + note_text: str + ): + """""" + self.name: str = name + self.vt_symbol: str = vt_symbol + self.size: int = size + + self.net_pos: int = net_pos + self.open_price: float = open_price + self.last_price: float = last_price + + self.pos_pnl: float = 0 + self.realized_pnl: float = 0 + + self.create_time: str = "" + if create_time: + self.create_time = create_time + else: + self.create_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + self.note_text: str = note_text + + self.calculate_pnl() + + def calculate_pnl(self): + """""" + self.pos_pnl = (self.last_price - self.open_price) * \ + self.net_pos * self.size + + def update_trade( + self, + trade_direction: Direction, + trade_volume: int, + trade_price: float + ): + """""" + if trade_direction == Direction.LONG: + self.net_pos += trade_volume + else: + self.net_pos -= trade_volume + + self.calculate_pnl() + + def update_price(self, last_price: float): + """""" + self.last_price = last_price + self.calculate_pnl() + + def update_note(self, note_text: str): + """""" + self.note_text = note_text diff --git a/vnpy/app/portfolio_manager/ui/__init__.py b/vnpy/app/portfolio_manager/ui/__init__.py new file mode 100644 index 00000000..aed1a423 --- /dev/null +++ b/vnpy/app/portfolio_manager/ui/__init__.py @@ -0,0 +1 @@ +from .widget import PortfolioManager diff --git a/vnpy/app/portfolio_manager/ui/portfolio.ico b/vnpy/app/portfolio_manager/ui/portfolio.ico new file mode 100644 index 0000000000000000000000000000000000000000..09f632d8d2d2f91982bd5789ab22dd32115f7d68 GIT binary patch literal 67646 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