Merge pull request #2193 from vnpy/dev-portfolio-manager

Dev portfolio manager
This commit is contained in:
vn.py 2019-11-14 00:51:54 +09:00 committed by GitHub
commit 1351496280
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 803 additions and 10 deletions

View File

@ -33,15 +33,16 @@ from vnpy.gateway.bitmex import BitmexGateway
# from vnpy.gateway.gateios import GateiosGateway # from vnpy.gateway.gateios import GateiosGateway
from vnpy.gateway.bybit import BybitGateway from vnpy.gateway.bybit import BybitGateway
from vnpy.app.cta_strategy import CtaStrategyApp # from vnpy.app.cta_strategy import CtaStrategyApp
# from vnpy.app.csv_loader import CsvLoaderApp # from vnpy.app.csv_loader import CsvLoaderApp
# from vnpy.app.algo_trading import AlgoTradingApp # from vnpy.app.algo_trading import AlgoTradingApp
from vnpy.app.cta_backtester import CtaBacktesterApp # from vnpy.app.cta_backtester import CtaBacktesterApp
from vnpy.app.data_recorder import DataRecorderApp # from vnpy.app.data_recorder import DataRecorderApp
# from vnpy.app.risk_manager import RiskManagerApp # from vnpy.app.risk_manager import RiskManagerApp
from vnpy.app.script_trader import ScriptTraderApp # from vnpy.app.script_trader import ScriptTraderApp
from vnpy.app.rpc_service import RpcServiceApp # from vnpy.app.rpc_service import RpcServiceApp
from vnpy.app.spread_trading import SpreadTradingApp # from vnpy.app.spread_trading import SpreadTradingApp
from vnpy.app.portfolio_manager import PortfolioManagerApp
def main(): def main():
@ -81,15 +82,16 @@ def main():
# main_engine.add_gateway(GateiosGateway) # main_engine.add_gateway(GateiosGateway)
main_engine.add_gateway(BybitGateway) main_engine.add_gateway(BybitGateway)
main_engine.add_app(CtaStrategyApp) # main_engine.add_app(CtaStrategyApp)
main_engine.add_app(CtaBacktesterApp) # main_engine.add_app(CtaBacktesterApp)
# main_engine.add_app(CsvLoaderApp) # main_engine.add_app(CsvLoaderApp)
# main_engine.add_app(AlgoTradingApp) # main_engine.add_app(AlgoTradingApp)
main_engine.add_app(DataRecorderApp) # main_engine.add_app(DataRecorderApp)
# main_engine.add_app(RiskManagerApp) # main_engine.add_app(RiskManagerApp)
# main_engine.add_app(ScriptTraderApp) # main_engine.add_app(ScriptTraderApp)
# main_engine.add_app(RpcServiceApp) # main_engine.add_app(RpcServiceApp)
main_engine.add_app(SpreadTradingApp) # main_engine.add_app(SpreadTradingApp)
main_engine.add_app(PortfolioManagerApp)
main_window = MainWindow(main_engine, event_engine) main_window = MainWindow(main_engine, event_engine)
main_window.showMaximized() main_window.showMaximized()

View File

@ -0,0 +1,16 @@
from pathlib import Path
from vnpy.trader.app import BaseApp
from .engine import PortfolioEngine, APP_NAME
class PortfolioManagerApp(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"

View File

@ -0,0 +1,381 @@
from datetime import datetime
from typing import Dict, List, Set
from collections import defaultdict
from copy import copy
from vnpy.event import Event, EventEngine
from vnpy.trader.engine import BaseEngine, MainEngine
from vnpy.trader.event import (
EVENT_TRADE, EVENT_ORDER, EVENT_TICK, EVENT_CONTRACT, EVENT_TIMER
)
from vnpy.trader.constant import Direction, Offset, OrderType
from vnpy.trader.object import (
OrderRequest, CancelRequest, SubscribeRequest,
OrderData, TradeData, TickData, ContractData
)
from vnpy.trader.utility import load_json, save_json
APP_NAME = "PortfolioManager"
EVENT_PORTFOLIO_UPDATE = "ePortfioUpdate"
EVENT_PORTFOLIO_ORDER = "ePortfioOrder"
EVENT_PORTFOLIO_TRADE = "ePortfioTrade"
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.inited = False
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()
def init_engine(self):
""""""
if self.inited:
return
self.inited = True
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.values():
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)
self.event_engine.register(EVENT_CONTRACT, self.process_contract_event)
self.event_engine.register(EVENT_TIMER, self.process_timer_event)
def process_timer_event(self, event: Event):
""""""
if self.inited:
self.save_setting()
def process_contract_event(self, event: Event):
""""""
contract: ContractData = event.data
if contract.vt_symbol in self.symbol_strategy_map:
self.subscribe_data(contract.vt_symbol)
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)
strategy: PortfolioStrategy = self.order_strategy_map[order.vt_orderid]
strategy_order = copy(order)
strategy_order.gateway_name = strategy.name
event = Event(EVENT_PORTFOLIO_ORDER, strategy_order)
self.event_engine.put(event)
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.direction,
trade.volume,
trade.price
)
self.put_strategy_event(strategy.name)
strategy_trade = copy(trade)
strategy_trade.gateway_name = strategy.name
event = Event(EVENT_PORTFOLIO_TRADE, strategy_trade)
self.event_engine.put(event)
self.save_setting()
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 = 0,
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
if not size:
contract = self.main_engine.get_contract(vt_symbol)
if not contract:
return False
size = contract.size
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.save_setting()
self.subscribe_data(vt_symbol)
self.put_strategy_event(name)
return True
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_strategy_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)
def stop(self):
""""""
self.save_setting()
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
):
""""""
old_cost = self.net_pos * self.open_price
if trade_direction == Direction.LONG:
new_pos = self.net_pos + trade_volume
# Open new long position
if self.net_pos >= 0:
new_cost = old_cost + trade_volume * trade_price
self.open_price = new_cost / new_pos
# Close short position
else:
close_volume = min(trade_volume, abs(self.net_pos))
realized_pnl = (trade_price - self.open_price) * \
close_volume * (-1)
self.realized_pnl += realized_pnl
if new_pos > 0:
self.open_price = trade_price
# Update net pos
self.net_pos = new_pos
else:
new_pos = self.net_pos - trade_volume
# Open new short position
if self.net_pos <= 0:
new_cost = old_cost - trade_volume * trade_price
self.open_price = new_cost / new_pos
# Close long position
else:
close_volume = min(trade_volume, abs(self.net_pos))
realized_pnl = (trade_price - self.open_price) * close_volume
self.realized_pnl += realized_pnl
if new_pos < 0:
self.open_price = trade_price
# Update net pos
self.net_pos = new_pos
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

View File

@ -0,0 +1 @@
from .widget import PortfolioManager

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,393 @@
from vnpy.event import EventEngine
from vnpy.trader.engine import MainEngine
from vnpy.trader.constant import Direction, Offset
from vnpy.trader.ui import QtWidgets, QtGui
from vnpy.trader.ui.widget import (
BaseMonitor, BaseCell,
PnlCell, DirectionCell, EnumCell,
)
from ..engine import (
PortfolioEngine,
APP_NAME,
EVENT_PORTFOLIO_UPDATE,
EVENT_PORTFOLIO_TRADE,
EVENT_PORTFOLIO_ORDER
)
class PortfolioManager(QtWidgets.QWidget):
""""""
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super().__init__()
self.main_engine = main_engine
self.event_engine = event_engine
self.portfolio_engine = main_engine.get_engine(APP_NAME)
self.init_ui()
def init_ui(self):
""""""
self.setWindowTitle("投资组合")
strategy_monitor = PortfolioStrategyMonitor(
self.main_engine, self.event_engine)
order_monitor = PortfolioOrderMonitor(
self.main_engine, self.event_engine)
trade_monitor = PortfolioTradeMonitor(
self.main_engine, self.event_engine)
self.trading_widget = StrategyTradingWidget(self.portfolio_engine)
self.management_widget = StrategyManagementWidget(
self.portfolio_engine,
self.trading_widget,
strategy_monitor
)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.management_widget)
vbox.addWidget(self.create_group("策略", strategy_monitor))
vbox.addWidget(self.trading_widget)
vbox.addWidget(self.create_group("委托", order_monitor))
vbox.addWidget(self.create_group("成交", trade_monitor))
self.setLayout(vbox)
def show(self):
""""""
self.portfolio_engine.init_engine()
self.management_widget.update_combo()
self.showMaximized()
def create_group(self, title: str, widget: QtWidgets.QWidget):
""""""
group = QtWidgets.QGroupBox()
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(widget)
group.setLayout(vbox)
group.setTitle(title)
return group
class PortfolioStrategyMonitor(BaseMonitor):
"""
Monitor for portfolio strategy.
"""
event_type = EVENT_PORTFOLIO_UPDATE
data_key = "name"
sorting = False
headers = {
"name": {"display": "策略名称", "cell": BaseCell, "update": False},
"vt_symbol": {"display": "交易合约", "cell": BaseCell, "update": False},
"size": {"display": "合约乘数", "cell": BaseCell, "update": False},
"net_pos": {"display": "策略持仓", "cell": BaseCell, "update": True},
"open_price": {"display": "持仓价格", "cell": BaseCell, "update": True},
"last_price": {"display": "最新价格", "cell": BaseCell, "update": True},
"pos_pnl": {"display": "持仓盈亏", "cell": PnlCell, "update": True},
"realized_pnl": {"display": "平仓盈亏", "cell": PnlCell, "update": True},
"create_time": {"display": "创建时间", "cell": BaseCell, "update": False},
}
def remove_strategy(self, name: str):
""""""
if name not in self.cells:
return
row_cells = self.cells.pop(name)
row = self.row(row_cells["net_pos"])
self.removeRow(row)
class PortfolioTradeMonitor(BaseMonitor):
"""
Monitor for trade data.
"""
event_type = EVENT_PORTFOLIO_TRADE
data_key = ""
headers = {
"gateway_name": {"display": "策略名称", "cell": BaseCell, "update": False},
"tradeid": {"display": "成交号 ", "cell": BaseCell, "update": False},
"orderid": {"display": "委托号", "cell": BaseCell, "update": False},
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"direction": {"display": "方向", "cell": DirectionCell, "update": False},
"offset": {"display": "开平", "cell": EnumCell, "update": False},
"price": {"display": "价格", "cell": BaseCell, "update": False},
"volume": {"display": "数量", "cell": BaseCell, "update": False},
"time": {"display": "时间", "cell": BaseCell, "update": False},
}
class PortfolioOrderMonitor(BaseMonitor):
"""
Monitor for order data.
"""
event_type = EVENT_PORTFOLIO_ORDER
data_key = "vt_orderid"
sorting = True
headers = {
"gateway_name": {"display": "策略名称", "cell": BaseCell, "update": False},
"orderid": {"display": "委托号", "cell": BaseCell, "update": False},
"symbol": {"display": "代码", "cell": BaseCell, "update": False},
"exchange": {"display": "交易所", "cell": EnumCell, "update": False},
"type": {"display": "类型", "cell": EnumCell, "update": False},
"direction": {"display": "方向", "cell": DirectionCell, "update": False},
"offset": {"display": "开平", "cell": EnumCell, "update": False},
"price": {"display": "价格", "cell": BaseCell, "update": False},
"volume": {"display": "总数量", "cell": BaseCell, "update": True},
"traded": {"display": "已成交", "cell": BaseCell, "update": True},
"status": {"display": "状态", "cell": EnumCell, "update": True},
"time": {"display": "时间", "cell": BaseCell, "update": True},
}
def init_ui(self):
"""
Connect signal.
"""
super(PortfolioOrderMonitor, self).init_ui()
self.setToolTip("双击单元格撤单")
self.itemDoubleClicked.connect(self.cancel_order)
def cancel_order(self, cell):
"""
Cancel order if cell double clicked.
"""
order = cell.get_data()
req = order.create_cancel_request()
self.main_engine.cancel_order(req, order.gateway_name)
class StrategyTradingWidget(QtWidgets.QWidget):
""""""
def __init__(self, portfolio_engine: PortfolioEngine):
""""""
super().__init__()
self.portfolio_engine = portfolio_engine
self.init_ui()
self.update_combo()
def init_ui(self):
""""""
self.name_combo = QtWidgets.QComboBox()
self.direction_combo = QtWidgets.QComboBox()
self.direction_combo.addItems(
[Direction.LONG.value, Direction.SHORT.value])
self.offset_combo = QtWidgets.QComboBox()
self.offset_combo.addItems([offset.value for offset in Offset])
double_validator = QtGui.QDoubleValidator()
double_validator.setBottom(0)
self.price_line = QtWidgets.QLineEdit()
self.price_line.setValidator(double_validator)
self.volume_line = QtWidgets.QLineEdit()
self.volume_line.setValidator(double_validator)
for w in [
self.name_combo,
self.price_line,
self.volume_line,
self.direction_combo,
self.offset_combo
]:
w.setFixedWidth(150)
send_button = QtWidgets.QPushButton("委托")
send_button.clicked.connect(self.send_order)
send_button.setFixedWidth(70)
cancel_button = QtWidgets.QPushButton("全撤")
cancel_button.clicked.connect(self.cancel_all)
cancel_button.setFixedWidth(70)
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(QtWidgets.QLabel("策略名称"))
hbox.addWidget(self.name_combo)
hbox.addWidget(QtWidgets.QLabel("方向"))
hbox.addWidget(self.direction_combo)
hbox.addWidget(QtWidgets.QLabel("开平"))
hbox.addWidget(self.offset_combo)
hbox.addWidget(QtWidgets.QLabel("价格"))
hbox.addWidget(self.price_line)
hbox.addWidget(QtWidgets.QLabel("数量"))
hbox.addWidget(self.volume_line)
hbox.addWidget(send_button)
hbox.addWidget(cancel_button)
hbox.addStretch()
self.setLayout(hbox)
def send_order(self):
""""""
name = self.name_combo.currentText()
price_text = self.price_line.text()
volume_text = self.volume_line.text()
if not price_text or not volume_text:
return
price = float(price_text)
volume = float(volume_text)
direction = Direction(self.direction_combo.currentText())
offset = Offset(self.offset_combo.currentText())
self.portfolio_engine.send_order(
name,
price,
volume,
direction,
offset
)
def cancel_all(self):
""""""
name = self.name_combo.currentText()
self.portfolio_engine.cancel_all(name)
def update_combo(self):
""""""
strategy_names = list(self.portfolio_engine.strategies.keys())
self.name_combo.clear()
self.name_combo.addItems(strategy_names)
class StrategyManagementWidget(QtWidgets.QWidget):
""""""
def __init__(
self,
portfolio_engine: PortfolioEngine,
trading_widget: StrategyTradingWidget,
strategy_monitor: PortfolioStrategyMonitor
):
""""""
super().__init__()
self.portfolio_engine = portfolio_engine
self.trading_widget = trading_widget
self.strategy_monitor = strategy_monitor
self.init_ui()
self.update_combo()
def init_ui(self):
""""""
self.name_line = QtWidgets.QLineEdit()
self.symbol_line = QtWidgets.QLineEdit()
self.remove_combo = QtWidgets.QComboBox()
for w in [
self.name_line,
self.symbol_line,
self.remove_combo
]:
w.setFixedWidth(150)
add_button = QtWidgets.QPushButton("创建策略")
add_button.clicked.connect(self.add_strategy)
remove_button = QtWidgets.QPushButton("移除策略")
remove_button.clicked.connect(self.remove_strategy)
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(QtWidgets.QLabel("策略名称"))
hbox.addWidget(self.name_line)
hbox.addWidget(QtWidgets.QLabel("交易合约"))
hbox.addWidget(self.symbol_line)
hbox.addWidget(add_button)
hbox.addStretch()
hbox.addWidget(self.remove_combo)
hbox.addWidget(remove_button)
self.setLayout(hbox)
def add_strategy(self):
""""""
name = self.name_line.text()
vt_symbol = self.symbol_line.text()
if not name or not vt_symbol:
QtWidgets.QMessageBox.information(
self,
"提示",
"请输入策略名称和交易合约",
QtWidgets.QMessageBox.Ok
)
result = self.portfolio_engine.add_strategy(name, vt_symbol)
if result:
QtWidgets.QMessageBox.information(
self,
"提示",
"策略创建成功",
QtWidgets.QMessageBox.Ok
)
self.update_combo()
else:
QtWidgets.QMessageBox.warning(
self,
"提示",
"策略创建失败,存在重名或找不到合约",
QtWidgets.QMessageBox.Ok
)
def remove_strategy(self):
""""""
name = self.remove_combo.currentText()
if not name:
return
result = self.portfolio_engine.remove_strategy(name)
if result:
QtWidgets.QMessageBox.information(
self,
"提示",
"策略移除成功",
QtWidgets.QMessageBox.Ok
)
self.update_combo()
self.strategy_monitor.remove_strategy(name)
else:
QtWidgets.QMessageBox.warning(
self,
"提示",
"策略移除失败,不存在该策略",
QtWidgets.QMessageBox.Ok
)
def update_combo(self):
""""""
strategy_names = list(self.portfolio_engine.strategies.keys())
self.remove_combo.clear()
self.remove_combo.addItems(strategy_names)
self.trading_widget.update_combo()