diff --git a/Q_n_A.md b/Q_n_A.md index eeb1a9d8..87da4c34 100644 --- a/Q_n_A.md +++ b/Q_n_A.md @@ -35,7 +35,13 @@ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config --set show_channel_urls yes - conda install -c quantopian ta-lib=0.4.9 + + wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz + tar -xf ta-lib-0.4.0-src.tar.gz + cd ta-lib + ./configure --prefix=/usr + make -j + sudo make install 若出现libta_lib.so.0 cannot open shared object file no such file or directory 解决: @@ -117,3 +123,13 @@ ../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin make –j4 make install + +12. pip 增加国内源 + + + 创建/home/trade/.pip目录 + 创建pip.conf文件,内容: + [global] + index-url=http://pypi.douban.com/simple + [install] + trusted-host=pypi.douban.com diff --git a/centos_setup.md b/centos_setup.md index ae19cd98..c61eb307 100644 --- a/centos_setup.md +++ b/centos_setup.md @@ -9,9 +9,8 @@ conda create -name py37=python3.7 wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz tar xvf ta-lib-0.4.0-src.tar.gz - cd ta-lib - ./autogen.sh - ./configure + cd ta-lib + ./configure --prefix=/usr make 下面指令用用root权限运行,会把编译结果放在/usr/local/lib下 @@ -19,26 +18,28 @@ conda create -name py37=python3.7 pip install ta-lib - 错误: - + 错误: ImportError: libta_lib.so.0: cannot open shared object file: No such file or directory - 解决: - - sudo find / -name libta_lib.so.0 - - /home/ai/eco-ta/ta-lib/src/.libs/libta_lib.so.0 - - /usr/local/lib/libta_lib.so.0 - - vi /etc/profile - - 添加 - - export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib - + 解决: + sudo find / -name libta_lib.so.0 + /home/ai/eco-ta/ta-lib/src/.libs/libta_lib.so.0 + /usr/local/lib/libta_lib.so.0 + vi /etc/profile + 添加 + export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib source /etc/profile - + + 错误: + autogen.sh:行3: aclocal: 未找到命令 + 解决: + sudo yum -y install automake + + 错误: + configure.in:13: error: possibly undefined macro: AC_PROG_LIBTOOL + 解决: + sudo yum install libtool + 单独编译ctp接口 一般直接使用提供的vnctptd.so 和 vnctpmd.so 就可以了。 diff --git a/install.sh b/install.sh index 227b7196..0abfb3fc 100644 --- a/install.sh +++ b/install.sh @@ -21,6 +21,7 @@ function install-ta-lib() make install popd } + function ta-lib-exists() { ta-lib-config --libs > /dev/null diff --git a/requirements.txt b/requirements.txt index df41c201..2eccd11a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ websocket-client peewee mongoengine numpy -pandas>=0.24.2 +pandas matplotlib seaborn futu-api diff --git a/run_celery.sh b/run_celery.sh new file mode 100644 index 00000000..77ed6253 --- /dev/null +++ b/run_celery.sh @@ -0,0 +1 @@ +celery -A vnpy.task.celery_app worker --max-tasks-per-child 1 -l debug > tests/celery/worker.log 2>tests/celery/worker-error.log & diff --git a/vnpy/app/cta_strategy_pro/__init__.py b/vnpy/app/cta_strategy_pro/__init__.py index 32b57c6a..728588b9 100644 --- a/vnpy/app/cta_strategy_pro/__init__.py +++ b/vnpy/app/cta_strategy_pro/__init__.py @@ -9,6 +9,7 @@ from .template import ( Direction, Offset, Status, + Color, TickData, BarData, TradeData, diff --git a/vnpy/app/cta_strategy_pro/template.py b/vnpy/app/cta_strategy_pro/template.py index 2092ebb1..786f72de 100644 --- a/vnpy/app/cta_strategy_pro/template.py +++ b/vnpy/app/cta_strategy_pro/template.py @@ -11,7 +11,7 @@ from copy import copy from typing import Any, Callable from logging import INFO, ERROR from datetime import datetime -from vnpy.trader.constant import Interval, Direction, Offset, Status, OrderType +from vnpy.trader.constant import Interval, Direction, Offset, Status, OrderType, Color from vnpy.trader.object import BarData, TickData, OrderData, TradeData from vnpy.trader.utility import virtual, append_data, extract_vt_symbol, get_underlying_symbol @@ -1421,11 +1421,11 @@ class CtaProFutureTemplate(CtaProTemplate): self.write_log(u'{} 订单信息:{}'.format(order.vt_orderid, old_order)) old_order['traded'] = order.traded # order_time = old_order['order_time'] - order_symbol = copy(old_order['symbol']) + order_vt_symbol = copy(old_order['vt_symbol']) order_volume = old_order['volume'] - old_order['traded'] if order_volume <= 0: msg = u'{} {}{}重新平仓数量为{},不再平仓' \ - .format(self.strategy_name, order.vt_orderid, order_symbol, order_volume) + .format(self.strategy_name, order.vt_orderid, order_vt_symbol, order_volume) self.write_error(msg) self.send_wechat(msg) self.write_log(u'活动订单移除:{}'.format(order.vt_orderid)) @@ -1434,11 +1434,11 @@ class CtaProFutureTemplate(CtaProTemplate): order_price = old_order['price'] order_type = old_order.get('order_type', OrderType.LIMIT) - order_retry = old_order['retry'] + order_retry = old_order.get('retry', 0) grid = old_order.get('grid', None) if order_retry > 20: msg = u'{} 平仓撤单 {}/{}手, 重试平仓次数{}>20' \ - .format(self.strategy_name, order_symbol, order_volume, order_retry) + .format(self.strategy_name, order_vt_symbol, order_volume, order_retry) self.write_error(msg) self.send_wechat(msg) if grid: @@ -1458,25 +1458,25 @@ class CtaProFutureTemplate(CtaProTemplate): if old_order['direction'] == Direction.LONG and order_type == OrderType.FAK: self.write_log(u'FAK模式,需要重新发送cover委托.grid:{}'.format(grid.__dict__)) # 更新委托平仓价 - cover_tick = self.tick_dict.get(order_symbol, self.cur_mi_tick) + cover_tick = self.tick_dict.get(order_vt_symbol, self.cur_mi_tick) cover_price = max(cover_tick.ask_price_1, cover_tick.last_price, order_price) + self.price_tick # 不能超过涨停价 if cover_tick.limit_up > 0 and cover_price > cover_tick.limit_up: cover_price = cover_tick.limit_up - if self.is_upper_limit(order_symbol): - self.write_log(u'{}涨停,不做cover'.format(order_symbol)) + if self.is_upper_limit(order_vt_symbol): + self.write_log(u'{}涨停,不做cover'.format(order_vt_symbol)) return # 发送委托 vt_orderids = self.cover(price=cover_price, volume=order_volume, - vt_symbol=order_symbol, + vt_symbol=order_vt_symbol, order_type=OrderType.FAK, order_time=self.cur_datetime, grid=grid) if not vt_orderids: - self.write_error(u'重新提交{} {}手平空单{}失败'.format(order_symbol, order_volume, cover_price)) + self.write_error(u'重新提交{} {}手平空单{}失败'.format(order_vt_symbol, order_volume, cover_price)) return for vt_orderid in vt_orderids: @@ -1485,31 +1485,31 @@ class CtaProFutureTemplate(CtaProTemplate): self.gt.save() self.write_log(u'移除活动订单:{}'.format(order.vt_orderid)) - self.active_orders.pop(order.vt_orderi, None) + self.active_orders.pop(order.vt_orderid, None) elif old_order['direction'] == Direction.SHORT and order_type == OrderType.FAK: self.write_log(u'FAK模式,需要重新发送sell委托.grid:{}'.format(grid.__dict__)) - sell_tick = self.tick_dict.get(order_symbol, self.cur_mi_tick) + sell_tick = self.tick_dict.get(order_vt_symbol, self.cur_mi_tick) sell_price = min(sell_tick.bid_price_1, sell_tick.last_price, order_price) - self.price_tick # 不能超过跌停价 if sell_tick.limit_down > 0 and sell_price < sell_tick.limit_down: sell_price = sell_tick.limit_down - if self.is_lower_limit(order_symbol): - self.write_log(u'{}涨停,不做sell'.format(order_symbol)) + if self.is_lower_limit(order_vt_symbol): + self.write_log(u'{}涨停,不做sell'.format(order_vt_symbol)) return # 发送委托 vt_orderids = self.sell(price=sell_price, volume=order_volume, - vt_symbol=order_symbol, + vt_symbol=order_vt_symbol, order_type=OrderType.FAK, order_time=self.cur_datetime, grid=grid) if not vt_orderids: - self.write_error(u'重新提交{} {}手平多单{}失败'.format(order_symbol, order_volume, sell_price)) + self.write_error(u'重新提交{} {}手平多单{}失败'.format(order_vt_symbol, order_volume, sell_price)) return for vt_orderid in vt_orderids: @@ -1568,11 +1568,11 @@ class CtaProFutureTemplate(CtaProTemplate): over_seconds = (dt - order_time).total_seconds() # 只处理未成交的限价委托单 - if order_status in [Status.NOTTRADED] and (order_type == OrderType.LIMIT or '.SPD' in order_vt_symbol): + if order_status in [Status.NOTTRADED,Status.SUBMITTING] and (order_type == OrderType.LIMIT or '.SPD' in order_vt_symbol): if over_seconds > self.cancel_seconds or force: # 超过设置的时间还未成交 self.write_log(u'超时{}秒未成交,取消委托单:vt_orderid:{},order:{}' .format(over_seconds, vt_orderid, order_info)) - order_info.update({'status': Status.CANCELING}) + order_info.update({'status': Status.CANCELLING}) self.active_orders.update({vt_orderid: order_info}) ret = self.cancel_order(str(vt_orderid)) if not ret: diff --git a/vnpy/app/data_manager/__init__.py b/vnpy/app/data_manager/__init__.py new file mode 100644 index 00000000..d410a758 --- /dev/null +++ b/vnpy/app/data_manager/__init__.py @@ -0,0 +1,16 @@ +from pathlib import Path + +from vnpy.trader.app import BaseApp +from .engine import APP_NAME, ManagerEngine + + +class DataManagerApp(BaseApp): + """""" + + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "数据管理" + engine_class = ManagerEngine + widget_name = "ManagerWidget" + icon_name = "manager.ico" diff --git a/vnpy/app/data_manager/engine.py b/vnpy/app/data_manager/engine.py new file mode 100644 index 00000000..a6d4bb4d --- /dev/null +++ b/vnpy/app/data_manager/engine.py @@ -0,0 +1,166 @@ +import csv +from datetime import datetime +from typing import List, Dict, Tuple + +from vnpy.trader.engine import BaseEngine, MainEngine, EventEngine +from vnpy.trader.constant import Interval, Exchange +from vnpy.trader.object import BarData +from vnpy.trader.database import database_manager + + +APP_NAME = "DataManager" + + +class ManagerEngine(BaseEngine): + """""" + + def __init__( + self, + main_engine: MainEngine, + event_engine: EventEngine, + ): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + def import_data_from_csv( + self, + file_path: str, + symbol: str, + exchange: Exchange, + interval: Interval, + datetime_head: str, + open_head: str, + high_head: str, + low_head: str, + close_head: str, + volume_head: str, + open_interest_head: str, + datetime_format: str + ) -> Tuple: + """""" + with open(file_path, "rt") as f: + buf = [line.replace("\0", "") for line in f] + + reader = csv.DictReader(buf, delimiter=",") + + bars = [] + start = None + count = 0 + + for item in reader: + if datetime_format: + dt = datetime.strptime(item[datetime_head], datetime_format) + else: + dt = datetime.fromisoformat(item[datetime_head]) + + open_interest = item.get(open_interest_head, 0) + + bar = BarData( + symbol=symbol, + exchange=exchange, + datetime=dt, + interval=interval, + volume=item[volume_head], + open_price=item[open_head], + high_price=item[high_head], + low_price=item[low_head], + close_price=item[close_head], + open_interest=open_interest, + gateway_name="DB", + ) + + bars.append(bar) + + # do some statistics + count += 1 + if not start: + start = bar.datetime + + # insert into database + database_manager.save_bar_data(bars) + + end = bar.datetime + return start, end, count + + def output_data_to_csv( + self, + file_path: str, + symbol: str, + exchange: Exchange, + interval: Interval, + start: datetime, + end: datetime + ) -> bool: + """""" + bars = self.load_bar_data(symbol, exchange, interval, start, end) + + fieldnames = [ + "symbol", + "exchange", + "datetime", + "open", + "high", + "low", + "close", + "volume", + "open_interest" + ] + + try: + with open(file_path, "w") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames, lineterminator="\n") + writer.writeheader() + + for bar in bars: + d = { + "symbol": bar.symbol, + "exchange": bar.exchange.value, + "datetime": bar.datetime.strftime("%Y-%m-%d %H:%M:%S"), + "open": bar.open_price, + "high": bar.high_price, + "low": bar.low_price, + "close": bar.close_price, + "volume": bar.volume, + "open_interest": bar.open_interest, + } + writer.writerow(d) + + return True + except PermissionError: + return False + + def get_bar_data_available(self) -> List[Dict]: + """""" + data = database_manager.get_bar_data_statistics() + + for d in data: + oldest_bar = database_manager.get_oldest_bar_data( + d["symbol"], Exchange(d["exchange"]), Interval(d["interval"]) + ) + d["start"] = oldest_bar.datetime + + newest_bar = database_manager.get_newest_bar_data( + d["symbol"], Exchange(d["exchange"]), Interval(d["interval"]) + ) + d["end"] = newest_bar.datetime + + return data + + def load_bar_data( + self, + symbol: str, + exchange: Exchange, + interval: Interval, + start: datetime, + end: datetime + ) -> List[BarData]: + """""" + bars = database_manager.load_bar_data( + symbol, + exchange, + interval, + start, + end + ) + + return bars diff --git a/vnpy/app/data_manager/ui/__init__.py b/vnpy/app/data_manager/ui/__init__.py new file mode 100644 index 00000000..3dfb4634 --- /dev/null +++ b/vnpy/app/data_manager/ui/__init__.py @@ -0,0 +1 @@ +from .widget import ManagerWidget \ No newline at end of file diff --git a/vnpy/app/data_manager/ui/manager.ico b/vnpy/app/data_manager/ui/manager.ico new file mode 100644 index 00000000..6ad2845e Binary files /dev/null and b/vnpy/app/data_manager/ui/manager.ico differ diff --git a/vnpy/app/data_manager/ui/widget.py b/vnpy/app/data_manager/ui/widget.py new file mode 100644 index 00000000..c7d7f839 --- /dev/null +++ b/vnpy/app/data_manager/ui/widget.py @@ -0,0 +1,422 @@ +from typing import Tuple, Dict +from functools import partial +from datetime import datetime + +from vnpy.trader.ui import QtWidgets, QtCore +from vnpy.trader.engine import MainEngine, EventEngine +from vnpy.trader.constant import Interval, Exchange + +from ..engine import APP_NAME, ManagerEngine + + +class ManagerWidget(QtWidgets.QWidget): + """""" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.engine: ManagerEngine = main_engine.get_engine(APP_NAME) + + self.tree_items: Dict[Tuple, QtWidgets.QTreeWidgetItem] = {} + + self.init_ui() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("数据管理") + + self.init_tree() + self.init_table() + + refresh_button = QtWidgets.QPushButton("刷新") + refresh_button.clicked.connect(self.refresh_tree) + + import_button = QtWidgets.QPushButton("导入数据") + import_button.clicked.connect(self.import_data) + + hbox1 = QtWidgets.QHBoxLayout() + hbox1.addWidget(refresh_button) + hbox1.addStretch() + hbox1.addWidget(import_button) + + hbox2 = QtWidgets.QHBoxLayout() + hbox2.addWidget(self.tree) + hbox2.addWidget(self.table) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox1) + vbox.addLayout(hbox2) + + self.setLayout(vbox) + + def init_tree(self) -> None: + """""" + labels = [ + "数据", + "本地代码", + "代码", + "交易所", + "数据量", + "开始时间", + "结束时间", + "", + "" + ] + + self.tree = QtWidgets.QTreeWidget() + self.tree.setColumnCount(len(labels)) + self.tree.setHeaderLabels(labels) + + root = QtWidgets.QTreeWidgetItem(self.tree) + root.setText(0, "K线数据") + root.setExpanded(True) + + self.minute_child = QtWidgets.QTreeWidgetItem() + self.minute_child.setText(0, "分钟线") + root.addChild(self.minute_child) + + self.hour_child = QtWidgets.QTreeWidgetItem() + self.hour_child.setText(0, "小时线") + root.addChild(self.hour_child) + + self.daily_child = QtWidgets.QTreeWidgetItem() + self.daily_child.setText(0, "日线") + root.addChild(self.daily_child) + + def init_table(self) -> None: + """""" + labels = [ + "时间", + "开盘价", + "最高价", + "最低价", + "收盘价", + "成交量", + "持仓量" + ] + + self.table = QtWidgets.QTableWidget() + self.table.setColumnCount(len(labels)) + self.table.setHorizontalHeaderLabels(labels) + self.table.verticalHeader().setVisible(False) + self.table.horizontalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.ResizeToContents + ) + + def refresh_tree(self) -> None: + """""" + data = self.engine.get_bar_data_available() + + for d in data: + key = (d["symbol"], d["exchange"], d["interval"]) + item = self.tree_items.get(key, None) + + if not item: + item = QtWidgets.QTreeWidgetItem() + self.tree_items[key] = item + + item.setText(1, ".".join([d["symbol"], d["exchange"]])) + item.setText(2, d["symbol"]) + item.setText(3, d["exchange"]) + + if d["interval"] == Interval.MINUTE.value: + self.minute_child.addChild(item) + elif d["interval"] == Interval.HOUR.value: + self.hour_child.addChild(item) + else: + self.daily_child.addChild(item) + + output_button = QtWidgets.QPushButton("导出") + output_func = partial( + self.output_data, + d["symbol"], + Exchange(d["exchange"]), + Interval(d["interval"]), + d["start"], + d["end"] + ) + output_button.clicked.connect(output_func) + + show_button = QtWidgets.QPushButton("查看") + show_func = partial( + self.show_data, + d["symbol"], + Exchange(d["exchange"]), + Interval(d["interval"]), + d["start"], + d["end"] + ) + show_button.clicked.connect(show_func) + + self.tree.setItemWidget(item, 7, show_button) + self.tree.setItemWidget(item, 8, output_button) + + item.setText(4, str(d["count"])) + item.setText(5, d["start"].strftime("%Y-%m-%d %H:%M:%S")) + item.setText(6, d["end"].strftime("%Y-%m-%d %H:%M:%S")) + + self.minute_child.setExpanded(True) + self.hour_child.setExpanded(True) + self.daily_child.setExpanded(True) + + def import_data(self) -> None: + """""" + dialog = ImportDialog() + n = dialog.exec_() + if n != dialog.Accepted: + return + + file_path = dialog.file_edit.text() + symbol = dialog.symbol_edit.text() + exchange = dialog.exchange_combo.currentData() + interval = dialog.interval_combo.currentData() + datetime_head = dialog.datetime_edit.text() + open_head = dialog.open_edit.text() + low_head = dialog.low_edit.text() + high_head = dialog.high_edit.text() + close_head = dialog.close_edit.text() + volume_head = dialog.volume_edit.text() + open_interest_head = dialog.open_interest_edit.text() + datetime_format = dialog.format_edit.text() + + start, end, count = self.engine.import_data_from_csv( + file_path, + symbol, + exchange, + interval, + datetime_head, + open_head, + high_head, + low_head, + close_head, + volume_head, + open_interest_head, + datetime_format + ) + + msg = f"\ + CSV载入成功\n\ + 代码:{symbol}\n\ + 交易所:{exchange.value}\n\ + 周期:{interval.value}\n\ + 起始:{start}\n\ + 结束:{end}\n\ + 总数量:{count}\n\ + " + QtWidgets.QMessageBox.information(self, "载入成功!", msg) + + def output_data( + self, + symbol: str, + exchange: Exchange, + interval: Interval, + start: datetime, + end: datetime + ) -> None: + """""" + # Get output date range + dialog = DateRangeDialog(start, end) + n = dialog.exec_() + if n != dialog.Accepted: + return + start, end = dialog.get_date_range() + + # Get output file path + path, _ = QtWidgets.QFileDialog.getSaveFileName( + self, + "导出数据", + "", + "CSV(*.csv)" + ) + if not path: + return + + result = self.engine.output_data_to_csv( + path, + symbol, + exchange, + interval, + start, + end + ) + + if not result: + QtWidgets.QMessageBox.warning( + self, + "导出失败!", + "该文件已在其他程序中打开,请关闭相关程序后再尝试导出数据。" + ) + + def show_data( + self, + symbol: str, + exchange: Exchange, + interval: Interval, + start: datetime, + end: datetime + ) -> None: + """""" + # Get output date range + dialog = DateRangeDialog(start, end) + n = dialog.exec_() + if n != dialog.Accepted: + return + start, end = dialog.get_date_range() + + bars = self.engine.load_bar_data( + symbol, + exchange, + interval, + start, + end + ) + + self.table.setRowCount(0) + self.table.setRowCount(len(bars)) + + for row, bar in enumerate(bars): + self.table.setItem(row, 0, DataCell(bar.datetime.strftime("%Y-%m-%d %H:%M:%S"))) + self.table.setItem(row, 1, DataCell(str(bar.open_price))) + self.table.setItem(row, 2, DataCell(str(bar.high_price))) + self.table.setItem(row, 3, DataCell(str(bar.low_price))) + self.table.setItem(row, 4, DataCell(str(bar.close_price))) + self.table.setItem(row, 5, DataCell(str(bar.volume))) + self.table.setItem(row, 6, DataCell(str(bar.open_interest))) + + def show(self) -> None: + """""" + self.showMaximized() + + +class DataCell(QtWidgets.QTableWidgetItem): + """""" + + def __init__(self, text: str = ""): + super().__init__(text) + + self.setTextAlignment(QtCore.Qt.AlignCenter) + + +class DateRangeDialog(QtWidgets.QDialog): + """""" + + def __init__(self, start: datetime, end: datetime, parent=None): + """""" + super().__init__(parent) + + self.setWindowTitle("选择数据区间") + + self.start_edit = QtWidgets.QDateEdit( + QtCore.QDate( + start.year, + start.month, + start.day + ) + ) + self.end_edit = QtWidgets.QDateEdit( + QtCore.QDate( + end.year, + end.month, + end.day + ) + ) + + button = QtWidgets.QPushButton("确定") + button.clicked.connect(self.accept) + + form = QtWidgets.QFormLayout() + form.addRow("开始时间", self.start_edit) + form.addRow("结束时间", self.end_edit) + form.addRow(button) + + self.setLayout(form) + + def get_date_range(self) -> Tuple[datetime, datetime]: + """""" + start = self.start_edit.date().toPyDate() + end = self.end_edit.date().toPyDate() + return start, end + + +class ImportDialog(QtWidgets.QDialog): + """""" + + def __init__(self, parent=None): + """""" + super().__init__() + + self.setWindowTitle("从CSV文件导入数据") + self.setFixedWidth(300) + + self.setWindowFlags( + (self.windowFlags() | QtCore.Qt.CustomizeWindowHint) + & ~QtCore.Qt.WindowMaximizeButtonHint) + + file_button = QtWidgets.QPushButton("选择文件") + file_button.clicked.connect(self.select_file) + + load_button = QtWidgets.QPushButton("确定") + load_button.clicked.connect(self.accept) + + self.file_edit = QtWidgets.QLineEdit() + self.symbol_edit = QtWidgets.QLineEdit() + + self.exchange_combo = QtWidgets.QComboBox() + for i in Exchange: + self.exchange_combo.addItem(str(i.name), i) + + self.interval_combo = QtWidgets.QComboBox() + for i in Interval: + self.interval_combo.addItem(str(i.name), i) + + self.datetime_edit = QtWidgets.QLineEdit("datetime") + self.open_edit = QtWidgets.QLineEdit("open") + self.high_edit = QtWidgets.QLineEdit("high") + self.low_edit = QtWidgets.QLineEdit("low") + self.close_edit = QtWidgets.QLineEdit("close") + self.volume_edit = QtWidgets.QLineEdit("volume") + self.open_interest_edit = QtWidgets.QLineEdit("open_interest") + + self.format_edit = QtWidgets.QLineEdit("%Y-%m-%d %H:%M:%S") + + info_label = QtWidgets.QLabel("合约信息") + info_label.setAlignment(QtCore.Qt.AlignCenter) + + head_label = QtWidgets.QLabel("表头信息") + head_label.setAlignment(QtCore.Qt.AlignCenter) + + format_label = QtWidgets.QLabel("格式信息") + format_label.setAlignment(QtCore.Qt.AlignCenter) + + form = QtWidgets.QFormLayout() + form.addRow(file_button, self.file_edit) + form.addRow(QtWidgets.QLabel()) + form.addRow(info_label) + form.addRow("代码", self.symbol_edit) + form.addRow("交易所", self.exchange_combo) + form.addRow("周期", self.interval_combo) + form.addRow(QtWidgets.QLabel()) + form.addRow(head_label) + form.addRow("时间戳", self.datetime_edit) + form.addRow("开盘价", self.open_edit) + form.addRow("最高价", self.high_edit) + form.addRow("最低价", self.low_edit) + form.addRow("收盘价", self.close_edit) + form.addRow("成交量", self.volume_edit) + form.addRow("持仓量", self.open_interest_edit) + form.addRow(QtWidgets.QLabel()) + form.addRow(format_label) + form.addRow("时间格式", self.format_edit) + form.addRow(QtWidgets.QLabel()) + form.addRow(load_button) + + self.setLayout(form) + + def select_file(self): + """""" + result: str = QtWidgets.QFileDialog.getOpenFileName( + self, filter="CSV (*.csv)") + filename = result[0] + if filename: + self.file_edit.setText(filename) diff --git a/vnpy/app/excel_rtd/__init__.py b/vnpy/app/excel_rtd/__init__.py new file mode 100644 index 00000000..bb8df0a1 --- /dev/null +++ b/vnpy/app/excel_rtd/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path +from vnpy.trader.app import BaseApp +from .engine import RtdEngine, APP_NAME + + +class ExcelRtdApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "Excel RTD" + engine_class = RtdEngine + widget_name = "RtdManager" + icon_name = "rtd.ico" diff --git a/vnpy/app/excel_rtd/engine.py b/vnpy/app/excel_rtd/engine.py new file mode 100644 index 00000000..eb916b32 --- /dev/null +++ b/vnpy/app/excel_rtd/engine.py @@ -0,0 +1,80 @@ +"""""" + +from typing import Set + +from vnpy.event import Event, EventEngine +from vnpy.rpc import RpcServer +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.object import TickData, LogData, SubscribeRequest +from vnpy.trader.event import EVENT_TICK + + +APP_NAME = "ExcelRtd" + +EVENT_RTD_LOG = "eRtdLog" + +REP_ADDRESS = "tcp://*:9001" +PUB_ADDRESS = "tcp://*:9002" + + +class RtdEngine(BaseEngine): + """ + The engine for managing RTD objects and data update. + """ + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.server: RpcServer = RpcServer() + self.server.register(self.subscribe) + self.server.register(self.write_log) + self.server.start(REP_ADDRESS, PUB_ADDRESS) + + self.subscribed: Set[str] = set() + + self.register_event() + + def register_event(self) -> None: + """ + Register event handler. + """ + self.event_engine.register(EVENT_TICK, self.process_tick_event) + + def process_tick_event(self, event: Event) -> None: + """ + Process tick event and update related RTD value. + """ + tick: TickData = event.data + self.server.publish("tick", tick) + + def write_log(self, msg: str) -> None: + """ + Output RTD related log message. + """ + log = LogData(msg=msg, gateway_name=APP_NAME) + event = Event(EVENT_RTD_LOG, log) + self.event_engine.put(event) + + def subscribe(self, vt_symbol: str) -> None: + """ + Subscribe tick data update. + """ + contract = self.main_engine.get_contract(vt_symbol) + if not contract: + return + + if vt_symbol in self.subscribed: + return + self.subscribed.add(vt_symbol) + + req = SubscribeRequest( + contract.symbol, + contract.exchange + ) + self.main_engine.subscribe(req, contract.gateway_name) + + def close(self): + """""" + self.server.stop() + self.server.join() diff --git a/vnpy/app/excel_rtd/ui/__init__.py b/vnpy/app/excel_rtd/ui/__init__.py new file mode 100644 index 00000000..a51486a8 --- /dev/null +++ b/vnpy/app/excel_rtd/ui/__init__.py @@ -0,0 +1 @@ +from .widget import RtdManager diff --git a/vnpy/app/excel_rtd/ui/rtd.ico b/vnpy/app/excel_rtd/ui/rtd.ico new file mode 100644 index 00000000..f6c9cbfd Binary files /dev/null and b/vnpy/app/excel_rtd/ui/rtd.ico differ diff --git a/vnpy/app/excel_rtd/ui/widget.py b/vnpy/app/excel_rtd/ui/widget.py new file mode 100644 index 00000000..b7aa655b --- /dev/null +++ b/vnpy/app/excel_rtd/ui/widget.py @@ -0,0 +1,79 @@ +from pathlib import Path + +from vnpy.event import EventEngine, Event +from vnpy.trader.engine import MainEngine +from vnpy.trader.ui import QtWidgets, QtCore +from vnpy.trader.object import LogData +from ..engine import APP_NAME, EVENT_RTD_LOG + + +class RtdManager(QtWidgets.QWidget): + """""" + signal_log = QtCore.pyqtSignal(Event) + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.rm_engine = main_engine.get_engine(APP_NAME) + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """ + Init widget ui components. + """ + self.setWindowTitle("Excel RTD") + self.resize(600, 600) + + module_path = Path(__file__).parent.parent + client_path = module_path.joinpath("vnpy_rtd.py") + self.client_line = QtWidgets.QLineEdit(str(client_path)) + self.client_line.setReadOnly(True) + + copy_button = QtWidgets.QPushButton("复制") + copy_button.clicked.connect(self.copy_client_path) + + self.log_monitor = QtWidgets.QTextEdit() + self.log_monitor.setReadOnly(True) + + self.port_label = QtWidgets.QLabel( + "使用Socket端口:请求回应9001、广播推送9002" + ) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(self.client_line) + hbox.addWidget(copy_button) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox) + vbox.addWidget(self.log_monitor) + vbox.addWidget(self.port_label) + self.setLayout(vbox) + + def register_event(self) -> None: + """ + Register event handler. + """ + self.signal_log.connect(self.process_log_event) + + self.event_engine.register(EVENT_RTD_LOG, self.signal_log.emit) + + def process_log_event(self, event: Event) -> None: + """ + Show log message in monitor. + """ + log: LogData = event.data + + msg = f"{log.time}: {log.msg}" + self.log_monitor.append(msg) + + def copy_client_path(self) -> None: + """ + Copy path of client python file to clipboard. + """ + self.client_line.selectAll() + self.client_line.copy() diff --git a/vnpy/app/excel_rtd/vnpy_rtd.py b/vnpy/app/excel_rtd/vnpy_rtd.py new file mode 100644 index 00000000..5fd398a0 --- /dev/null +++ b/vnpy/app/excel_rtd/vnpy_rtd.py @@ -0,0 +1,114 @@ +"""""" + +from typing import Dict, Set, Any +from collections import defaultdict + +from pyxll import RTD, xl_func + +from vnpy.rpc import RpcClient +from vnpy.trader.object import TickData + + +REQ_ADDRESS = "tcp://localhost:9001" +SUB_ADDRESS = "tcp://localhost:9002" + + +rtd_client: "RtdClient" = None + + +class ObjectRtd(RTD): + """ + RTD proxy for object in Python. + """ + + def __init__(self, engine: "RtdClient", name: str, field: str): + """Constructor""" + super().__init__(value=0) + + self.engine = engine + self.name = name + self.field = field + + def connect(self) -> None: + """ + Callback when excel cell rtd is connected. + """ + self.engine.add_rtd(self) + + def disconnect(self) -> None: + """ + Callback when excel cell rtd is disconncted. + """ + self.engine.remove_rtd(self) + + def update(self, data: Any) -> None: + """ + Update value in excel cell. + """ + new_value = getattr(data, self.field, "N/A") + + if new_value != self.value: + self.value = new_value + + +class RtdClient(RpcClient): + """ + The engine for managing RTD objects and data update. + """ + + def __init__(self): + """""" + super().__init__() + + self.rtds: Dict[str, Set[ObjectRtd]] = defaultdict(set) + + global rtd_client + rtd_client = self + + def callback(self, topic: str, data: Any) -> None: + """""" + tick: TickData = data + buf = self.rtds[tick.vt_symbol] + + for rtd in buf: + rtd.update(tick) + + def add_rtd(self, rtd: ObjectRtd) -> None: + """ + Add a new RTD into the engine.. + """ + buf = self.rtds[rtd.name] + buf.add(rtd) + self.write_log(f"新增RTD连接:{rtd.name} {rtd.field}") + + # Auto subscribe tick data + self.subscribe(rtd.name) + + def remove_rtd(self, rtd: ObjectRtd) -> None: + """ + Remove an existing RTD from the engine. + """ + buf = self.rtds[self.name] + if self in buf: + buf.remove(rtd) + self.write_log(f"移除RTD连接:{rtd.name} {rtd.field}") + + +def init_client() -> None: + """Initialize vnpy rtd client""" + global rtd_client + rtd_client = RtdClient() + rtd_client.subscribe_topic("") + rtd_client.start(REQ_ADDRESS, SUB_ADDRESS) + + +@xl_func("string vt_symbol, string field: rtd") +def rtd_tick_data(vt_symbol: str, field: str) -> ObjectRtd: + """ + Return the streaming value of the tick data field. + """ + if not rtd_client: + init_client() + + rtd = ObjectRtd(rtd_client, vt_symbol, field) + return rtd diff --git a/vnpy/app/option_master/__init__.py b/vnpy/app/option_master/__init__.py new file mode 100644 index 00000000..dd881ee3 --- /dev/null +++ b/vnpy/app/option_master/__init__.py @@ -0,0 +1,14 @@ +from pathlib import Path +from vnpy.trader.app import BaseApp +from .engine import OptionEngine, APP_NAME + + +class OptionMasterApp(BaseApp): + """""" + app_name = APP_NAME + app_module = __module__ + app_path = Path(__file__).parent + display_name = "期权交易" + engine_class = OptionEngine + widget_name = "OptionManager" + icon_name = "option.ico" diff --git a/vnpy/app/option_master/algo.py b/vnpy/app/option_master/algo.py new file mode 100644 index 00000000..b59563fd --- /dev/null +++ b/vnpy/app/option_master/algo.py @@ -0,0 +1,342 @@ +from typing import TYPE_CHECKING, Set + +from vnpy.trader.object import TickData, OrderData, TradeData +from vnpy.trader.constant import Direction, Offset +from vnpy.trader.utility import round_to + +from .base import OptionData + +if TYPE_CHECKING: + from .engine import OptionAlgoEngine + + +class ElectronicEyeAlgo: + + def __init__( + self, + algo_engine: "OptionAlgoEngine", + option: OptionData + ): + """""" + self.algo_engine = algo_engine + self.option = option + self.underlying = option.underlying + self.pricetick = option.pricetick + self.vt_symbol = option.vt_symbol + + # Parameters + self.pricing_active: bool = False + self.trading_active: bool = False + + self.price_spread: float = 0.0 + self.volatility_spread: float = 0.0 + + self.long_allowed = False + self.short_allowed = False + + self.max_pos: int = 0 + self.target_pos: int = 0 + self.max_order_size: int = 0 + + # Variables + self.long_active_orderids: Set[str] = set() + self.short_active_orderids: Set[str] = set() + + self.algo_spread: float = 0.0 + self.ref_price: float = 0.0 + self.algo_bid_price: float = 0.0 + self.algo_ask_price: float = 0.0 + self.pricing_impv: float = 0.0 + + def start_pricing(self, params: dict) -> bool: + """""" + if self.pricing_active: + return False + + self.price_spread = params["price_spread"] + self.volatility_spread = params["volatility_spread"] + + self.pricing_active = True + self.put_status_event() + self.calculate_price() + self.write_log("启动定价") + + return True + + def stop_pricing(self) -> bool: + """""" + if not self.pricing_active: + return False + + if self.trading_active: + return False + + self.pricing_active = False + + # Clear parameters + self.algo_spread = 0.0 + self.ref_price = 0.0 + self.algo_bid_price = 0.0 + self.algo_ask_price = 0.0 + self.pricing_impv = 0.0 + + self.put_status_event() + self.put_pricing_event() + self.write_log("停止定价") + + return True + + def start_trading(self, params: dict) -> bool: + """""" + if self.trading_active: + return False + + if not self.pricing_active: + self.write_log("请先启动定价") + return False + + self.long_allowed = params["long_allowed"] + self.short_allowed = params["short_allowed"] + self.max_pos = params["max_pos"] + self.target_pos = params["target_pos"] + self.max_order_size = params["max_order_size"] + + if not self.max_order_size: + self.write_log("请先设置最大委托数量") + return False + + self.trading_active = True + + self.put_trading_event() + self.put_status_event() + self.write_log("启动交易") + + return True + + def stop_trading(self) -> bool: + """""" + if not self.trading_active: + return False + + self.trading_active = False + + self.cancel_long() + self.cancel_short() + + self.put_status_event() + self.put_trading_event() + self.write_log("停止交易") + + return True + + def on_underlying_tick(self, tick: TickData) -> None: + """""" + if self.pricing_active: + self.calculate_price() + + if self.trading_active: + self.do_trading() + + def on_option_tick(self, tick: TickData) -> None: + """""" + if self.trading_active: + self.do_trading() + + def on_order(self, order: OrderData) -> None: + """""" + if not order.is_active(): + if order.vt_orderid in self.long_active_orderids: + self.long_active_orderids.remove(order.vt_orderid) + elif order.vt_orderid in self.short_active_orderids: + self.short_active_orderids.remove(order.vt_orderid) + + def on_trade(self, trade: TradeData) -> None: + """""" + self.write_log(f"委托成交,{trade.direction} {trade.offset} {trade.volume}@{trade.price}") + + def on_timer(self) -> None: + """""" + if self.long_active_orderids: + self.cancel_long() + + if self.short_active_orderids: + self.cancel_short() + + def send_order( + self, + direction: Direction, + offset: Offset, + price: float, + volume: int + ) -> str: + """""" + vt_orderid = self.algo_engine.send_order( + self, + self.vt_symbol, + direction, + offset, + price, + volume + ) + + self.write_log(f"发出委托,{direction} {offset} {volume}@{price}") + + return vt_orderid + + def buy(self, price: float, volume: int) -> None: + """""" + vt_orderid = self.send_order(Direction.LONG, Offset.OPEN, price, volume) + self.long_active_orderids.add(vt_orderid) + + def sell(self, price: float, volume: int) -> None: + """""" + self.send_order(Direction.SHORT, Offset.CLOSE, price, volume) + + def short(self, price: float, volume: int) -> None: + """""" + vt_orderid = self.send_order(Direction.SHORT, Offset.OPEN, price, volume) + self.short_active_orderids.add(vt_orderid) + + def cover(self, price: float, volume: int) -> None: + """""" + self.send_order(Direction.LONG, Offset.CLOSE, price, volume) + + def send_long(self, price: float, volume: int) -> None: + """""" + option = self.option + + if not option.short_pos: + self.buy(price, volume) + elif option.short_pos >= volume: + self.cover(price, volume) + else: + self.cover(price, option.short_pos) + self.buy(price, volume - option.short_pos) + + def send_short(self, price: float, volume: int) -> None: + """""" + option = self.option + + if not option.long_pos: + self.short(price, volume) + elif option.long_pos >= volume: + self.sell(price, volume) + else: + self.sell(price, option.long_pos) + self.short(price, volume - option.long_pos) + + self.order_ask_price = price + self.order_ask_volume = volume + + def cancel_order(self, vt_orderid: str) -> None: + """""" + self.algo_engine.cancel_order(vt_orderid) + + def cancel_long(self) -> None: + """""" + for vt_orderid in self.long_active_orderids: + self.cancel_order(vt_orderid) + + def cancel_short(self) -> None: + """""" + for vt_orderid in self.short_active_orderids: + self.cancel_order(vt_orderid) + + def check_long_finished(self) -> bool: + """""" + if not self.long_active_orderids: + return True + + return False + + def check_short_finished(self) -> bool: + """""" + if not self.short_active_orderids: + return True + + return False + + def calculate_price(self) -> None: + """""" + option = self.option + + # Get ref price + self.pricing_impv = option.pricing_impv + ref_price = option.calculate_ref_price() + self.ref_price = round_to(ref_price, self.pricetick) + + # Calculate spread + algo_spread = max( + self.price_spread, + self.volatility_spread * option.theo_vega + ) + half_spread = algo_spread / 2 + + # Calculate bid/ask + self.algo_bid_price = round_to(ref_price - half_spread, self.pricetick) + self.algo_ask_price = round_to(ref_price + half_spread, self.pricetick) + self.algo_spread = round_to(algo_spread, self.pricetick) + + self.put_pricing_event() + + def do_trading(self) -> None: + """""" + if self.long_allowed and self.check_long_finished(): + self.snipe_long() + + if self.short_allowed and self.check_short_finished(): + self.snipe_short() + + def snipe_long(self) -> None: + """""" + option = self.option + tick = option.tick + + # Calculate volume left to trade + pos_up_limit = self.target_pos + self.max_pos + volume_left = pos_up_limit - option.net_pos + + # Check price + if volume_left > 0 and tick.ask_price_1 <= self.algo_bid_price: + volume = min( + volume_left, + tick.ask_volume_1, + self.max_order_size + ) + + self.send_long(self.algo_bid_price, volume) + + def snipe_short(self) -> None: + """""" + option = self.option + tick = option.tick + + # Calculate volume left to trade + pos_down_limit = self.target_pos - self.max_pos + volume_left = option.net_pos - pos_down_limit + + # Check price + if volume_left > 0 and tick.bid_price_1 >= self.algo_ask_price: + volume = min( + volume_left, + tick.bid_volume_1, + self.max_order_size + ) + + self.send_short(self.algo_ask_price, volume) + + def put_pricing_event(self) -> None: + """""" + self.algo_engine.put_algo_pricing_event(self) + + def put_trading_event(self) -> None: + """""" + self.algo_engine.put_algo_trading_event(self) + + def put_status_event(self) -> None: + """""" + self.algo_engine.put_algo_status_event(self) + + def write_log(self, msg: str) -> None: + """""" + self.algo_engine.write_algo_log(self, msg) diff --git a/vnpy/app/option_master/base.py b/vnpy/app/option_master/base.py new file mode 100644 index 00000000..ad0307b7 --- /dev/null +++ b/vnpy/app/option_master/base.py @@ -0,0 +1,594 @@ +from datetime import datetime +from typing import Dict, List, Callable +from types import ModuleType + +from vnpy.trader.object import ContractData, TickData, TradeData +from vnpy.trader.constant import Exchange, OptionType, Direction, Offset +from vnpy.trader.converter import PositionHolding + +from .time import calculate_days_to_expiry, ANNUAL_DAYS + + +APP_NAME = "OptionMaster" + +EVENT_OPTION_NEW_PORTFOLIO = "eOptionNewPortfolio" +EVENT_OPTION_ALGO_PRICING = "eOptionAlgoPricing" +EVENT_OPTION_ALGO_TRADING = "eOptionAlgoTrading" +EVENT_OPTION_ALGO_STATUS = "eOptionAlgoStatus" +EVENT_OPTION_ALGO_LOG = "eOptionAlgoLog" + + +CHAIN_UNDERLYING_MAP = { + "510050_O.SSE": "510050", + "510300_O.SSE": "510300", + "159919_O.SSE": "159919", + "IO.CFFEX": "IF", + "HO.CFFEX": "IH", + "i_o.DCE": "i", + "m_o.DCE": "m", + "c_o.DCE": "c", + "cu_o.SHFE": "cu", + "ru_o.SHFE": "ru", + "SR.CZCE": "SR", + "CF.CZCE": "CF", + "TA.CZCE": "TA", +} + + +class InstrumentData: + """""" + + def __init__(self, contract: ContractData): + """""" + self.symbol: str = contract.symbol + self.exchange: Exchange = contract.exchange + self.vt_symbol: str = contract.vt_symbol + + self.pricetick: float = contract.pricetick + self.min_volume: float = contract.min_volume + self.size: int = contract.size + + self.long_pos: int = 0 + self.short_pos: int = 0 + self.net_pos: int = 0 + self.mid_price: float = 0 + + self.tick: TickData = None + self.portfolio: PortfolioData = None + + def calculate_net_pos(self) -> None: + """""" + self.net_pos = self.long_pos - self.short_pos + + def update_tick(self, tick: TickData) -> None: + """""" + self.tick = tick + self.mid_price = (tick.bid_price_1 + tick.ask_price_1) / 2 + + def update_trade(self, trade: TradeData) -> None: + """""" + if trade.direction == Direction.LONG: + if trade.offset == Offset.OPEN: + self.long_pos += trade.volume + else: + self.short_pos -= trade.volume + else: + if trade.offset == Offset.OPEN: + self.short_pos += trade.volume + else: + self.long_pos -= trade.volume + self.calculate_net_pos() + + def update_holding(self, holding: PositionHolding) -> None: + """""" + self.long_pos = holding.long_pos + self.short_pos = holding.short_pos + self.calculate_net_pos() + + def set_portfolio(self, portfolio: "PortfolioData") -> None: + """""" + self.portfolio = portfolio + + +class OptionData(InstrumentData): + """""" + + def __init__(self, contract: ContractData): + """""" + super().__init__(contract) + + # Option contract features + self.strike_price: float = contract.option_strike + self.chain_index: str = contract.option_index + + self.option_type: int = 0 + if contract.option_type == OptionType.CALL: + self.option_type = 1 + else: + self.option_type = -1 + + self.option_expiry: datetime = contract.option_expiry + self.days_to_expiry: int = calculate_days_to_expiry( + contract.option_expiry + ) + self.time_to_expiry: float = self.days_to_expiry / ANNUAL_DAYS + + self.interest_rate: float = 0 + + # Option portfolio related + self.underlying: UnderlyingData = None + self.chain: ChainData = None + self.underlying_adjustment: float = 0 + + # Pricing model + self.calculate_price: Callable = None + self.calculate_greeks: Callable = None + self.calculate_impv: Callable = None + + # Implied volatility + self.bid_impv: float = 0 + self.ask_impv: float = 0 + self.mid_impv: float = 0 + self.pricing_impv: float = 0 + + # Greeks related + self.theo_delta: float = 0 + self.theo_gamma: float = 0 + self.theo_theta: float = 0 + self.theo_vega: float = 0 + + self.pos_value: float = 0 + self.pos_delta: float = 0 + self.pos_gamma: float = 0 + self.pos_theta: float = 0 + self.pos_vega: float = 0 + + def calculate_option_impv(self) -> None: + """""" + if not self.tick: + return + + underlying_price = self.underlying.mid_price + if not underlying_price: + return + underlying_price += self.underlying_adjustment + + self.ask_impv = self.calculate_impv( + self.tick.ask_price_1, + underlying_price, + self.strike_price, + self.interest_rate, + self.time_to_expiry, + self.option_type + ) + + self.bid_impv = self.calculate_impv( + self.tick.bid_price_1, + underlying_price, + self.strike_price, + self.interest_rate, + self.time_to_expiry, + self.option_type + ) + + self.mid_impv = (self.ask_impv + self.bid_impv) / 2 + + def calculate_theo_greeks(self) -> None: + """""" + if not self.underlying: + return + + underlying_price = self.underlying.mid_price + if not underlying_price or not self.mid_impv: + return + underlying_price += self.underlying_adjustment + + price, delta, gamma, theta, vega = self.calculate_greeks( + underlying_price, + self.strike_price, + self.interest_rate, + self.time_to_expiry, + self.mid_impv, + self.option_type + ) + + self.theo_delta = delta * self.size + self.theo_gamma = gamma * self.size + self.theo_theta = theta * self.size + self.theo_vega = vega * self.size + + def calculate_pos_greeks(self) -> None: + """""" + if self.tick: + self.pos_value = self.tick.last_price * self.size * self.net_pos + + self.pos_delta = self.theo_delta * self.net_pos + self.pos_gamma = self.theo_gamma * self.net_pos + self.pos_theta = self.theo_theta * self.net_pos + self.pos_vega = self.theo_vega * self.net_pos + + def calculate_ref_price(self) -> float: + """""" + underlying_price = self.underlying.mid_price + underlying_price += self.underlying_adjustment + + ref_price = self.calculate_price( + underlying_price, + self.strike_price, + self.interest_rate, + self.time_to_expiry, + self.pricing_impv, + self.option_type + ) + + return ref_price + + def update_tick(self, tick: TickData) -> None: + """""" + super().update_tick(tick) + self.calculate_option_impv() + + def update_trade(self, trade: TradeData) -> None: + """""" + super().update_trade(trade) + self.calculate_pos_greeks() + + def update_underlying_tick(self, underlying_adjustment: float) -> None: + """""" + self.underlying_adjustment = underlying_adjustment + + self.calculate_option_impv() + self.calculate_theo_greeks() + self.calculate_pos_greeks() + + def set_chain(self, chain: "ChainData") -> None: + """""" + self.chain = chain + + def set_underlying(self, underlying: "UnderlyingData") -> None: + """""" + self.underlying = underlying + + def set_interest_rate(self, interest_rate: float) -> None: + """""" + self.interest_rate = interest_rate + + def set_pricing_model(self, pricing_model: ModuleType) -> None: + """""" + self.calculate_greeks = pricing_model.calculate_greeks + self.calculate_impv = pricing_model.calculate_impv + self.calculate_price = pricing_model.calculate_price + + +class UnderlyingData(InstrumentData): + """""" + + def __init__(self, contract: ContractData): + """""" + super().__init__(contract) + + self.theo_delta: float = 0 + self.pos_delta: float = 0 + self.chains: Dict[str: ChainData] = {} + + def add_chain(self, chain: "ChainData") -> None: + """""" + self.chains[chain.chain_symbol] = chain + + def update_tick(self, tick: TickData) -> None: + """""" + super().update_tick(tick) + + self.theo_delta = self.size * self.mid_price / 100 + for chain in self.chains.values(): + chain.update_underlying_tick() + + self.calculate_pos_greeks() + + def update_trade(self, trade: TradeData) -> None: + """""" + super().update_trade(trade) + + self.calculate_pos_greeks() + + def calculate_pos_greeks(self) -> None: + """""" + self.pos_delta = self.theo_delta * self.net_pos + + +class ChainData: + """""" + + def __init__(self, chain_symbol: str): + """""" + self.chain_symbol: str = chain_symbol + + self.long_pos: int = 0 + self.short_pos: int = 0 + self.net_pos: int = 0 + + self.pos_value: float = 0 + self.pos_delta: float = 0 + self.pos_gamma: float = 0 + self.pos_theta: float = 0 + self.pos_vega: float = 0 + + self.underlying: UnderlyingData = None + + self.options: Dict[str, OptionData] = {} + self.calls: Dict[str, OptionData] = {} + self.puts: Dict[str, OptionData] = {} + + self.portfolio: PortfolioData = None + + self.indexes: List[float] = [] + self.atm_price: float = 0 + self.atm_index: str = "" + self.underlying_adjustment: float = 0 + self.days_to_expiry: int = 0 + + def add_option(self, option: OptionData) -> None: + """""" + self.options[option.vt_symbol] = option + + if option.option_type > 0: + self.calls[option.chain_index] = option + else: + self.puts[option.chain_index] = option + + option.set_chain(self) + + if option.chain_index not in self.indexes: + self.indexes.append(option.chain_index) + self.indexes.sort() + + self.days_to_expiry = option.days_to_expiry + + def calculate_pos_greeks(self) -> None: + """""" + # Clear data + self.long_pos = 0 + self.short_pos = 0 + self.net_pos = 0 + self.pos_value = 0 + self.pos_delta = 0 + self.pos_gamma = 0 + self.pos_theta = 0 + self.pos_vega = 0 + + # Sum all value + for option in self.options.values(): + if option.net_pos: + self.long_pos += option.long_pos + self.short_pos += option.short_pos + self.pos_value += option.pos_value + self.pos_delta += option.pos_delta + self.pos_gamma += option.pos_gamma + self.pos_theta += option.pos_theta + self.pos_vega += option.pos_vega + + self.net_pos = self.long_pos - self.short_pos + + def update_tick(self, tick: TickData) -> None: + """""" + option = self.options[tick.vt_symbol] + option.update_tick(tick) + + def update_underlying_tick(self) -> None: + """""" + self.calculate_underlying_adjustment() + + for option in self.options.values(): + option.update_underlying_tick(self.underlying_adjustment) + + self.calculate_pos_greeks() + + def update_trade(self, trade: TradeData) -> None: + """""" + option = self.options[trade.vt_symbol] + + # Deduct old option pos greeks + self.long_pos -= option.long_pos + self.short_pos -= option.short_pos + self.pos_value -= option.pos_value + self.pos_delta -= option.pos_delta + self.pos_gamma -= option.pos_gamma + self.pos_theta -= option.pos_theta + self.pos_vega -= option.pos_vega + + # Calculate new option pos greeks + option.update_trade(trade) + + # Add new option pos greeks + self.long_pos += option.long_pos + self.short_pos += option.short_pos + self.pos_value += option.pos_value + self.pos_delta += option.pos_delta + self.pos_gamma += option.pos_gamma + self.pos_theta += option.pos_theta + self.pos_vega += option.pos_vega + + self.net_pos = self.long_pos - self.short_pos + + def set_underlying(self, underlying: "UnderlyingData") -> None: + """""" + underlying.add_chain(self) + self.underlying = underlying + + for option in self.options.values(): + option.set_underlying(underlying) + + def set_interest_rate(self, interest_rate: float) -> None: + """""" + for option in self.options.values(): + option.set_interest_rate(interest_rate) + + def set_pricing_model(self, pricing_model: ModuleType) -> None: + """""" + for option in self.options.values(): + option.set_pricing_model(pricing_model) + + def set_portfolio(self, portfolio: "PortfolioData") -> None: + """""" + for option in self.options: + option.set_portfolio(portfolio) + + def calculate_atm_price(self) -> None: + """""" + underlying_price = self.underlying.mid_price + + atm_distance = 0 + atm_price = 0 + atm_index = "" + + for call in self.calls.values(): + price_distance = abs(underlying_price - call.strike_price) + + if not atm_distance or price_distance < atm_distance: + atm_distance = price_distance + atm_price = call.strike_price + atm_index = call.chain_index + + self.atm_price = atm_price + self.atm_index = atm_index + + def calculate_underlying_adjustment(self) -> None: + """""" + if not self.atm_price: + return + + atm_call = self.calls[self.atm_index] + atm_put = self.puts[self.atm_index] + + synthetic_price = atm_call.mid_price - atm_put.mid_price + self.atm_price + self.underlying_adjustment = synthetic_price - self.underlying.mid_price + + +class PortfolioData: + + def __init__(self, name: str): + """""" + self.name: str = name + + self.long_pos: int = 0 + self.short_pos: int = 0 + self.net_pos: int = 0 + + self.pos_delta: float = 0 + self.pos_gamma: float = 0 + self.pos_theta: float = 0 + self.pos_vega: float = 0 + + # All instrument + self._options: Dict[str, OptionData] = {} + self._chains: Dict[str, ChainData] = {} + + # Active instrument + self.options: Dict[str, OptionData] = {} + self.chains: Dict[str, ChainData] = {} + self.underlyings: Dict[str, UnderlyingData] = {} + + def calculate_pos_greeks(self) -> None: + """""" + self.long_pos = 0 + self.short_pos = 0 + self.net_pos = 0 + + self.pos_value = 0 + self.pos_delta = 0 + self.pos_gamma = 0 + self.pos_theta = 0 + self.pos_vega = 0 + + for underlying in self.underlyings.values(): + self.pos_delta += underlying.pos_delta + + for chain in self.chains.values(): + self.long_pos += chain.long_pos + self.short_pos += chain.short_pos + self.pos_value += chain.pos_value + self.pos_delta += chain.pos_delta + self.pos_gamma += chain.pos_gamma + self.pos_theta += chain.pos_theta + self.pos_vega += chain.pos_vega + + self.net_pos = self.long_pos - self.short_pos + + def update_tick(self, tick: TickData) -> None: + """""" + if tick.vt_symbol in self.options: + option = self.options[tick.vt_symbol] + chain = option.chain + chain.update_tick(tick) + self.calculate_pos_greeks() + elif tick.vt_symbol in self.underlyings: + underlying = self.underlyings[tick.vt_symbol] + underlying.update_tick(tick) + self.calculate_pos_greeks() + + def update_trade(self, trade: TradeData) -> None: + """""" + if trade.vt_symbol in self.options: + option = self.options[trade.vt_symbol] + chain = option.chain + chain.update_trade(trade) + self.calculate_pos_greeks() + elif trade.vt_symbol in self.underlyings: + underlying = self.underlyings[trade.vt_symbol] + underlying.update_trade(trade) + self.calculate_pos_greeks() + + def set_interest_rate(self, interest_rate: float) -> None: + """""" + for chain in self.chains.values(): + chain.set_interest_rate(interest_rate) + + def set_pricing_model(self, pricing_model: ModuleType) -> None: + """""" + for chain in self.chains.values(): + chain.set_pricing_model(pricing_model) + + def set_chain_underlying(self, chain_symbol: str, contract: ContractData) -> None: + """""" + underlying = self.underlyings.get(contract.vt_symbol, None) + if not underlying: + underlying = UnderlyingData(contract) + underlying.set_portfolio(self) + self.underlyings[contract.vt_symbol] = underlying + + chain = self.get_chain(chain_symbol) + chain.set_underlying(underlying) + + # Add to active dict + self.chains[chain_symbol] = chain + + for option in chain.options.values(): + self.options[option.vt_symbol] = option + + def get_chain(self, chain_symbol: str) -> ChainData: + """""" + chain = self._chains.get(chain_symbol, None) + + if not chain: + chain = ChainData(chain_symbol) + chain.set_portfolio(self) + self._chains[chain_symbol] = chain + + return chain + + def add_option(self, contract: ContractData) -> None: + """""" + option = OptionData(contract) + option.set_portfolio(self) + self._options[contract.vt_symbol] = option + + exchange_name = contract.exchange.value + chain_symbol: str = f"{contract.option_underlying}.{exchange_name}" + + chain = self.get_chain(chain_symbol) + chain.add_option(option) + + def calculate_atm_price(self) -> None: + """""" + for chain in self.chains.values(): + chain.calculate_atm_price() diff --git a/vnpy/app/option_master/engine.py b/vnpy/app/option_master/engine.py new file mode 100644 index 00000000..a018a153 --- /dev/null +++ b/vnpy/app/option_master/engine.py @@ -0,0 +1,683 @@ +"""""" + +from typing import Dict, List, Set +from copy import copy +from collections import defaultdict + +from vnpy.trader.object import ( + LogData, ContractData, TickData, + OrderData, TradeData, + SubscribeRequest, OrderRequest +) +from vnpy.event import Event, EventEngine +from vnpy.trader.engine import BaseEngine, MainEngine +from vnpy.trader.event import ( + EVENT_TRADE, EVENT_TICK, EVENT_CONTRACT, + EVENT_TIMER, EVENT_ORDER, EVENT_POSITION +) +from vnpy.trader.constant import ( + Product, Offset, Direction, OrderType +) +from vnpy.trader.converter import OffsetConverter +from vnpy.trader.utility import round_to, save_json, load_json + +from .base import ( + APP_NAME, CHAIN_UNDERLYING_MAP, + EVENT_OPTION_NEW_PORTFOLIO, + EVENT_OPTION_ALGO_PRICING, EVENT_OPTION_ALGO_TRADING, + EVENT_OPTION_ALGO_STATUS, EVENT_OPTION_ALGO_LOG, + InstrumentData, PortfolioData +) +try: + from .pricing import black_76_cython as black_76 + from .pricing import binomial_tree_cython as binomial_tree + from .pricing import black_scholes_cython as black_scholes +except ImportError: + from .pricing import ( + black_76, binomial_tree, black_scholes + ) + print("Faile to import cython option pricing model, please rebuild with cython in cmd.") +from .algo import ElectronicEyeAlgo + + +PRICING_MODELS = { + "Black-76 欧式期货期权": black_76, + "Black-Scholes 欧式股票期权": black_scholes, + "二叉树 美式期货期权": binomial_tree +} + + +class OptionEngine(BaseEngine): + """""" + + setting_filename = "option_master_setting.json" + data_filename = "option_master_data.json" + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__(main_engine, event_engine, APP_NAME) + + self.portfolios: Dict[str, PortfolioData] = {} + self.instruments: Dict[str, InstrumentData] = {} + self.active_portfolios: Dict[str, PortfolioData] = {} + + self.timer_count: int = 0 + self.timer_trigger: int = 60 + + self.offset_converter: OffsetConverter = OffsetConverter(main_engine) + self.get_position_holding = self.offset_converter.get_position_holding + + self.hedge_engine: OptionHedgeEngine = OptionHedgeEngine(self) + self.algo_engine: OptionAlgoEngine = OptionAlgoEngine(self) + + self.setting: Dict = {} + + self.load_setting() + self.register_event() + + def close(self) -> None: + """""" + self.save_setting() + self.save_data() + + def load_setting(self) -> None: + """""" + self.setting = load_json(self.setting_filename) + + def save_setting(self) -> None: + """ + Save underlying adjustment. + """ + save_json(self.setting_filename, self.setting) + + def load_data(self) -> None: + """""" + data = load_json(self.data_filename) + + for portfolio in self.active_portfolios.values(): + portfolio_name = portfolio.name + + # Load underlying adjustment from setting + chain_adjustments = data.get("chain_adjustments", {}) + chain_adjustment_data = chain_adjustments.get(portfolio_name, {}) + + if chain_adjustment_data: + for chain in portfolio.chains.values(): + chain.underlying_adjustment = chain_adjustment_data.get( + chain.chain_symbol, 0 + ) + + # Load pricing impv from setting + pricing_impvs = data.get("pricing_impvs", {}) + pricing_impv_data = pricing_impvs.get(portfolio_name, {}) + + if pricing_impv_data: + for chain in portfolio.chains.values(): + for index in chain.indexes: + key = f"{chain.chain_symbol}_{index}" + pricing_impv = pricing_impv_data.get(key, 0) + + if pricing_impv: + call = chain.calls[index] + call.pricing_impv = pricing_impv + + put = chain.puts[index] + put.pricing_impv = pricing_impv + + def save_data(self) -> None: + """""" + chain_adjustments = {} + pricing_impvs = {} + + for portfolio in self.active_portfolios.values(): + chain_adjustment_data = {} + pricing_impv_data = {} + for chain in portfolio.chains.values(): + chain_adjustment_data[chain.chain_symbol] = chain.underlying_adjustment + + for call in chain.calls.values(): + key = f"{chain.chain_symbol}_{call.chain_index}" + pricing_impv_data[key] = call.pricing_impv + + chain_adjustments[portfolio.name] = chain_adjustment_data + pricing_impvs[portfolio.name] = pricing_impv_data + + data = { + "chain_adjustments": chain_adjustments, + "pricing_impvs": pricing_impvs + } + + save_json(self.data_filename, data) + + def register_event(self) -> None: + """""" + 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_ORDER, self.process_order_event) + self.event_engine.register(EVENT_TRADE, self.process_trade_event) + self.event_engine.register(EVENT_POSITION, self.process_position_event) + self.event_engine.register(EVENT_TIMER, self.process_timer_event) + + def process_tick_event(self, event: Event) -> None: + """""" + tick: TickData = event.data + + instrument = self.instruments.get(tick.vt_symbol, None) + if not instrument: + return + + portfolio = instrument.portfolio + if not portfolio: + return + + portfolio.update_tick(tick) + + def process_order_event(self, event: Event) -> None: + """""" + order: OrderData = event.data + self.offset_converter.update_order(order) + + def process_trade_event(self, event: Event) -> None: + """""" + trade: TradeData = event.data + self.offset_converter.update_trade(trade) + + instrument = self.instruments.get(trade.vt_symbol, None) + if not instrument: + return + + portfolio = instrument.portfolio + if not portfolio: + return + + portfolio.update_trade(trade) + + def process_contract_event(self, event: Event) -> None: + """""" + contract: ContractData = event.data + + if contract.product == Product.OPTION: + exchange_name = contract.exchange.value + portfolio_name = f"{contract.option_portfolio}.{exchange_name}" + if portfolio_name not in CHAIN_UNDERLYING_MAP: + return + + portfolio = self.get_portfolio(portfolio_name) + portfolio.add_option(contract) + + def process_position_event(self, event: Event) -> None: + """""" + position = event.data + self.offset_converter.update_position(position) + + def process_timer_event(self, event: Event) -> None: + """""" + self.timer_count += 1 + if self.timer_count < self.timer_trigger: + return + self.timer_count = 0 + + for portfolio in self.active_portfolios.values(): + portfolio.calculate_atm_price() + + def get_portfolio(self, portfolio_name: str) -> PortfolioData: + """""" + portfolio = self.portfolios.get(portfolio_name, None) + if not portfolio: + portfolio = PortfolioData(portfolio_name) + self.portfolios[portfolio_name] = portfolio + + event = Event(EVENT_OPTION_NEW_PORTFOLIO, portfolio_name) + self.event_engine.put(event) + + return portfolio + + def subscribe_data(self, vt_symbol: str) -> None: + """""" + contract = self.main_engine.get_contract(vt_symbol) + req = SubscribeRequest(contract.symbol, contract.exchange) + self.main_engine.subscribe(req, contract.gateway_name) + + def update_portfolio_setting( + self, + portfolio_name: str, + model_name: str, + interest_rate: float, + chain_underlying_map: Dict[str, str], + ) -> None: + """""" + portfolio = self.get_portfolio(portfolio_name) + + for chain_symbol, underlying_symbol in chain_underlying_map.items(): + contract = self.main_engine.get_contract(underlying_symbol) + portfolio.set_chain_underlying(chain_symbol, contract) + + portfolio.set_interest_rate(interest_rate) + + pricing_model = PRICING_MODELS[model_name] + portfolio.set_pricing_model(pricing_model) + + portfolio_settings = self.setting.setdefault("portfolio_settings", {}) + portfolio_settings[portfolio_name] = { + "model_name": model_name, + "interest_rate": interest_rate, + "chain_underlying_map": chain_underlying_map + } + self.save_setting() + + def get_portfolio_setting(self, portfolio_name: str) -> Dict: + """""" + portfolio_settings = self.setting.setdefault("portfolio_settings", {}) + return portfolio_settings.get(portfolio_name, {}) + + def init_portfolio(self, portfolio_name: str) -> bool: + """""" + # Add to active dict + if portfolio_name in self.active_portfolios: + return False + portfolio = self.get_portfolio(portfolio_name) + self.active_portfolios[portfolio_name] = portfolio + + # Subscribe market data + for underlying in portfolio.underlyings.values(): + self.instruments[underlying.vt_symbol] = underlying + self.subscribe_data(underlying.vt_symbol) + + for option in portfolio.options.values(): + # Ignore options with no underlying set + if not option.underlying: + continue + + self.instruments[option.vt_symbol] = option + self.subscribe_data(option.vt_symbol) + + # Update position volume + for instrument in self.instruments.values(): + holding = self.offset_converter.get_position_holding( + instrument.vt_symbol + ) + if holding: + instrument.update_holding(holding) + + portfolio.calculate_pos_greeks() + + # Load chain adjustment and pricing impv data + self.load_data() + + return True + + def get_portfolio_names(self) -> List[str]: + """""" + return list(self.portfolios.keys()) + + def get_underlying_symbols(self, portfolio_name: str) -> List[str]: + """""" + underlying_prefix = CHAIN_UNDERLYING_MAP[portfolio_name] + underlying_symbols = [] + + contracts = self.main_engine.get_all_contracts() + for contract in contracts: + if contract.product == Product.OPTION: + continue + + if contract.symbol.startswith(underlying_prefix): + underlying_symbols.append(contract.vt_symbol) + + underlying_symbols.sort() + + return underlying_symbols + + def get_instrument(self, vt_symbol: str) -> InstrumentData: + """""" + instrument = self.instruments[vt_symbol] + return instrument + + def set_timer_trigger(self, timer_trigger: int) -> None: + """""" + self.timer_trigger = timer_trigger + + +class OptionHedgeEngine: + """""" + + def __init__(self, option_engine: OptionEngine): + """""" + self.option_engine: OptionEngine = option_engine + self.main_engine: MainEngine = option_engine.main_engine + self.event_engine: EventEngine = option_engine.event_engine + + # Hedging parameters + self.portfolio_name: str = "" + self.vt_symbol: str = "" + self.timer_trigger = 5 + self.delta_target = 0 + self.delta_range = 0 + self.hedge_payup = 1 + + self.active: bool = False + self.active_orderids: Set[str] = set() + self.timer_count = 0 + + self.register_event() + + def register_event(self) -> None: + """""" + self.event_engine.register(EVENT_ORDER, self.process_order_event) + self.event_engine.register(EVENT_TIMER, self.process_timer_event) + + def process_order_event(self, event: Event) -> None: + """""" + order: OrderData = event.data + + if order.vt_orderid not in self.active_orderids: + return + + if not order.is_active(): + self.active_orderids.remove(order.vt_orderid) + + def process_timer_event(self, event: Event) -> None: + """""" + if not self.active: + return + + self.timer_count += 1 + if self.timer_count < self.timer_trigger: + return + self.timer_count = 0 + + self.run() + + def start( + self, + portfolio_name: str, + vt_symbol: str, + timer_trigger: int, + delta_target: int, + delta_range: int, + hedge_payup: int + ) -> None: + """""" + if self.active: + return + + self.portfolio_name = portfolio_name + self.vt_symbol = vt_symbol + self.timer_trigger = timer_trigger + self.delta_target = delta_target + self.delta_range = delta_range + self.hedge_payup = hedge_payup + + self.active = True + + def stop(self) -> None: + """""" + if not self.active: + return + + self.active = False + self.timer_count = 0 + + def run(self) -> None: + """""" + if not self.check_order_finished(): + self.cancel_all() + return + + delta_max = self.delta_target + self.delta_range + delta_min = self.delta_target - self.delta_range + + # Do nothing if portfolio delta is in the allowed range + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + if delta_min <= portfolio.pos_delta <= delta_max: + return + + # Calculate volume of contract to hedge + delta_to_hedge = self.delta_target - portfolio.pos_delta + instrument = self.option_engine.get_instrument(self.vt_symbol) + hedge_volume = delta_to_hedge / instrument.theo_delta + + # Send hedge orders + tick = self.main_engine.get_tick(self.vt_symbol) + contract = self.main_engine.get_contract(self.vt_symbol) + holding = self.option_engine.get_position_holding(self.vt_symbol) + + if hedge_volume > 0: + price = tick.ask_price_1 + contract.pricetick * self.hedge_payup + direction = Direction.LONG + + if holding: + available = holding.short_pos - holding.short_pos_frozen + else: + available = 0 + else: + price = tick.bid_price_1 - contract.pricetick * self.hedge_payup + direction = Direction.SHORT + + if holding: + available = holding.long_pos - holding.long_pos_frozen + else: + available = 0 + + order_volume = abs(hedge_volume) + + req = OrderRequest( + symbol=contract.symbol, + exchange=contract.exchange, + direction=direction, + type=OrderType.LIMIT, + volume=order_volume, + price=round_to(price, contract.pricetick), + ) + + # Close positon if opposite available is enough + if available > order_volume: + req.offset = Offset.CLOSE + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + self.active_orderids.add(vt_orderid) + # Open position if no oppsite available + elif not available: + req.offset = Offset.OPEN + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + self.active_orderids.add(vt_orderid) + # Else close all opposite available and open left volume + else: + close_req = copy(req) + close_req.offset = Offset.CLOSE + close_req.volume = available + close_orderid = self.main_engine.send_order(close_req, contract.gateway_name) + self.active_orderids.add(close_orderid) + + open_req = copy(req) + open_req.offset = Offset.OPEN + open_req.volume = order_volume - available + open_orderid = self.main_engine.send_order(open_req, contract.gateway_name) + self.active_orderids.add(open_orderid) + + def check_order_finished(self) -> None: + """""" + if self.active_orderids: + return False + else: + return True + + def cancel_all(self) -> None: + """""" + for vt_orderid in self.active_orderids: + order: OrderData = self.main_engine.get_order(vt_orderid) + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) + + +class OptionAlgoEngine: + + def __init__(self, option_engine: OptionEngine): + """""" + self.option_engine = option_engine + self.main_engine = option_engine.main_engine + self.event_engine = option_engine.event_engine + + self.algos: Dict[str, ElectronicEyeAlgo] = {} + self.active_algos: Dict[str, ElectronicEyeAlgo] = {} + + self.underlying_algo_map: Dict[str, ElectronicEyeAlgo] = defaultdict(list) + self.order_algo_map: Dict[str, ElectronicEyeAlgo] = {} + + self.register_event() + + def init_engine(self, portfolio_name: str) -> None: + """""" + if self.algos: + return + + portfolio = self.option_engine.get_portfolio(portfolio_name) + + for option in portfolio.options.values(): + algo = ElectronicEyeAlgo(self, option) + self.algos[option.vt_symbol] = algo + + def register_event(self) -> None: + """""" + 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_TIMER, self.process_timer_event) + + def process_underlying_tick_event(self, event: Event) -> None: + """""" + tick: TickData = event.data + + for algo in self.underlying_algo_map[tick.vt_symbol]: + algo.on_underlying_tick(algo) + + def process_option_tick_event(self, event: Event) -> None: + """""" + tick: TickData = event.data + + algo = self.algos[tick.vt_symbol] + algo.on_option_tick(algo) + + def process_order_event(self, event: Event) -> None: + """""" + order: OrderData = event.data + algo = self.order_algo_map.get(order.vt_orderid, None) + + if algo: + algo.on_order(order) + + def process_trade_event(self, event: Event) -> None: + """""" + trade: TradeData = event.data + algo = self.order_algo_map.get(trade.vt_orderid, None) + + if algo: + algo.on_trade(trade) + + def process_timer_event(self, event: Event) -> None: + """""" + for algo in self.active_algos.values(): + algo.on_timer() + + def start_algo_pricing(self, vt_symbol: str, params: dict) -> None: + """""" + algo = self.algos[vt_symbol] + + result = algo.start_pricing(params) + if not result: + return + + self.underlying_algo_map[algo.underlying.vt_symbol].append(algo) + + self.event_engine.register( + EVENT_TICK + algo.option.vt_symbol, + self.process_option_tick_event + ) + self.event_engine.register( + EVENT_TICK + algo.underlying.vt_symbol, + self.process_underlying_tick_event + ) + + def stop_algo_pricing(self, vt_symbol: str) -> None: + """""" + algo = self.algos[vt_symbol] + + result = algo.stop_pricing() + if not result: + return + + self.event_engine.unregister( + EVENT_TICK + vt_symbol, + self.process_option_tick_event + ) + + buf = self.underlying_algo_map[algo.underlying.vt_symbol] + buf.remove(algo) + + if not buf: + self.event_engine.unregister( + EVENT_TICK + algo.underlying.vt_symbol, + self.process_underlying_tick_event + ) + + def start_algo_trading(self, vt_symbol: str, params: dict) -> None: + """""" + algo = self.algos[vt_symbol] + algo.start_trading(params) + + def stop_algo_trading(self, vt_symbol: str) -> None: + """""" + algo = self.algos[vt_symbol] + algo.stop_trading() + + def send_order( + self, + algo: ElectronicEyeAlgo, + vt_symbol: str, + direction: Direction, + offset: Offset, + price: float, + volume: int + ) -> str: + """""" + contract = self.main_engine.get_contract(vt_symbol) + + req = OrderRequest( + contract.symbol, + contract.exchange, + direction, + OrderType.LIMIT, + volume, + price, + offset + ) + + vt_orderid = self.main_engine.send_order(req, contract.gateway_name) + self.order_algo_map[vt_orderid] = algo + + return vt_orderid + + def cancel_order(self, vt_orderid: str) -> None: + """""" + order = self.main_engine.get_order(vt_orderid) + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) + + def write_algo_log(self, algo: ElectronicEyeAlgo, msg: str) -> None: + """""" + msg = f"[{algo.vt_symbol}] {msg}" + log = LogData(APP_NAME, msg) + event = Event(EVENT_OPTION_ALGO_LOG, log) + self.event_engine.put(event) + + def put_algo_pricing_event(self, algo: ElectronicEyeAlgo) -> None: + """""" + event = Event(EVENT_OPTION_ALGO_PRICING, algo) + self.event_engine.put(event) + + def put_algo_trading_event(self, algo: ElectronicEyeAlgo) -> None: + """""" + event = Event(EVENT_OPTION_ALGO_TRADING, algo) + self.event_engine.put(event) + + def put_algo_status_event(self, algo: ElectronicEyeAlgo) -> None: + """""" + event = Event(EVENT_OPTION_ALGO_STATUS, algo) + self.event_engine.put(event) diff --git a/vnpy/app/option_master/pricing/__init__.py b/vnpy/app/option_master/pricing/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/vnpy/app/option_master/pricing/binomial_tree.py b/vnpy/app/option_master/pricing/binomial_tree.py new file mode 100644 index 00000000..d6697f12 --- /dev/null +++ b/vnpy/app/option_master/pricing/binomial_tree.py @@ -0,0 +1,251 @@ +from numpy import zeros, ndarray +from math import exp, sqrt +from typing import Tuple + + +DEFAULT_STEP = 15 + + +def generate_tree( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int +) -> Tuple[ndarray, ndarray]: + """Generate binomial tree for pricing American option.""" + dt = t / n + u = exp(v * sqrt(dt)) + d = 1 / u + a = 1 + tree_size = n + 1 + underlying_tree = zeros((tree_size, tree_size)) + option_tree = zeros((tree_size, tree_size)) + + # Calculate risk neutral probability + p = (a - d) / (u - d) + p1 = p / a + p2 = (1 - p) / a + + # Calculate underlying price tree + underlying_tree[0, 0] = f + + for i in range(1, n + 1): + underlying_tree[0, i] = underlying_tree[0, i - 1] * u + for j in range(1, n + 1): + underlying_tree[j, i] = underlying_tree[j - 1, i - 1] * d + + # Calculate option price tree + for j in range(n + 1): + option_tree[j, n] = max(0, cp * (underlying_tree[j, n] - k)) + + for i in range(n - 1, -1, -1): + for j in range(i + 1): + option_tree[j, i] = max( + (p1 * option_tree[j, i + 1] + p2 * option_tree[j + 1, i + 1]), + cp * (underlying_tree[j, i] - k) + ) + + # Return both trees + return option_tree, underlying_tree + + +def calculate_price( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option price""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + return option_tree[0, 0] + + +def calculate_delta( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option delta""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + option_price_change = option_tree[0, 1] - option_tree[1, 1] + underlying_price_change = underlying_tree[0, 1] - underlying_tree[1, 1] + return option_price_change / underlying_price_change + + +def calculate_gamma( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option gamma""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + + gamma_delta_1 = (option_tree[0, 2] - option_tree[1, 2]) / \ + (underlying_tree[0, 2] - underlying_tree[1, 2]) + gamma_delta_2 = (option_tree[1, 2] - option_tree[2, 2]) / \ + (underlying_tree[1, 2] - underlying_tree[2, 2]) + gamma = (gamma_delta_1 - gamma_delta_2) / \ + (0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2])) + + return gamma + + +def calculate_theta( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP, + annual_days: int = 240 +) -> float: + """Calcualte option theta""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + + dt = t / n + theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days) + + return theta + + +def calculate_vega( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option vega(%)""" + vega = calculate_original_vega(f, k, r, t, v, cp, n) / 100 + return vega + + +def calculate_original_vega( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option vega""" + price_1 = calculate_price(f, k, r, t, v, cp, n) + price_2 = calculate_price(f, k, r, t, v * 1.001, cp, n) + vega = (price_2 - price_1) / (v * 0.001) + return vega + + +def calculate_greeks( + f: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + n: int = DEFAULT_STEP, + annual_days: int = 240 +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + dt = t / n + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + option_tree_vega, underlying_tree_vega = generate_tree(f, k, r, t, v * 1.001, cp, n) + + # Price + price = option_tree[0, 0] + + # Delta + option_price_change = option_tree[0, 1] - option_tree[1, 1] + underlying_price_change = underlying_tree[0, 1] - underlying_tree[1, 1] + delta = option_price_change / underlying_price_change + + # Gamma + gamma_delta_1 = (option_tree[0, 2] - option_tree[1, 2]) / \ + (underlying_tree[0, 2] - underlying_tree[1, 2]) + gamma_delta_2 = (option_tree[1, 2] - option_tree[2, 2]) / \ + (underlying_tree[1, 2] - underlying_tree[2, 2]) + gamma = (gamma_delta_1 - gamma_delta_2) / \ + (0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2])) + + # Theta + theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days) + + # Vega + vega = (option_tree_vega[0, 0] - option_tree[0, 0]) / (0.001 * v * 100) + + return price, delta, gamma, theta, vega + + +def calculate_impv( + price: float, + f: float, + k: float, + r: float, + t: float, + cp: int, + n: int = DEFAULT_STEP +) -> float: + """Calculate option implied volatility""" + # Check option price must be position + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet = False + + if cp == 1 and price > (f - k): + meet = True + elif cp == -1 and price > (k - f): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p: float = calculate_price(f, k, r, t, v, cp, n) + vega: float = calculate_original_vega(f, k, r, t, v, cp, n) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/binomial_tree_cython.pyd b/vnpy/app/option_master/pricing/binomial_tree_cython.pyd new file mode 100644 index 00000000..dc3682f7 Binary files /dev/null and b/vnpy/app/option_master/pricing/binomial_tree_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/black_76.py b/vnpy/app/option_master/pricing/black_76.py new file mode 100644 index 00000000..3ed021c3 --- /dev/null +++ b/vnpy/app/option_master/pricing/black_76.py @@ -0,0 +1,217 @@ +from scipy import stats +from math import log, pow, sqrt, exp +from typing import Tuple + +cdf = stats.norm.cdf +pdf = stats.norm.pdf + + +def calculate_d1( + s: float, + k: float, + r: float, + t: float, + v: float +) -> float: + """Calculate option D1 value""" + d1: float = (log(s / k) + (0.5 * pow(v, 2)) * t) / (v * sqrt(t)) + return d1 + + +def calculate_price( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0 +) -> float: + """Calculate option price""" + # Return option space value if volatility not positive + if v <= 0: + return max(0, cp * (s - k)) + + if not d1: + d1: float = calculate_d1(s, k, r, r, v) + d2: float = d1 - v * sqrt(t) + + price: float = cp * (s * cdf(cp * d1) - k * cdf(cp * d2)) * exp(-r * t) + return price + + +def calculate_delta( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0 +) -> float: + """Calculate option delta""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + _delta: float = cp * exp(-r * t) * cdf(cp * d1) + delta: float = _delta * s * 0.01 + return delta + + +def calculate_gamma( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option gamma""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + _gamma: float = exp(-r * t) * pdf(d1) / (s * v * sqrt(t)) + gamma: float = _gamma * pow(s, 2) * 0.0001 + + return gamma + + +def calculate_theta( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0, + annual_days: int = 240 +) -> float: + """Calculate option theta""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + d2: float = d1 - v * sqrt(t) + + _theta = -s * exp(-r * t) * pdf(d1) * v / (2 * sqrt(t)) \ + + cp * r * s * exp(-r * t) * cdf(cp * d1) \ + - cp * r * k * exp(-r * t) * cdf(cp * d2) + theta = _theta / annual_days + + return theta + + +def calculate_vega( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option vega(%)""" + vega: float = calculate_original_vega(s, k, r, t, v, d1) / 100 + return vega + + +def calculate_original_vega( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option vega""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + vega: float = s * exp(-r * t) * pdf(d1) * sqrt(t) + + return vega + + +def calculate_greeks( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + annual_days: int = 240 +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + d1: float = calculate_d1(s, k, r, t, v) + price: float = calculate_price(s, k, r, t, v, cp, d1) + delta: float = calculate_delta(s, k, r, t, v, cp, d1) + gamma: float = calculate_gamma(s, k, r, t, v, d1) + theta: float = calculate_theta(s, k, r, t, v, cp, d1, annual_days) + vega: float = calculate_vega(s, k, r, t, v, d1) + return price, delta, gamma, theta, vega + + +def calculate_impv( + price: float, + s: float, + k: float, + r: float, + t: float, + cp: int +): + """Calculate option implied volatility""" + # Check option price must be positive + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet: bool = False + + if cp == 1 and (price > (s - k) * exp(-r * t)): + meet = True + elif cp == -1 and (price > k * exp(-r * t) - s): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v: float = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p: float = calculate_price(s, k, r, t, v, cp) + vega: float = calculate_original_vega(s, k, r, t, v, cp) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx: float = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/black_76_cython.pyd b/vnpy/app/option_master/pricing/black_76_cython.pyd new file mode 100644 index 00000000..85b0aad7 Binary files /dev/null and b/vnpy/app/option_master/pricing/black_76_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/black_scholes.py b/vnpy/app/option_master/pricing/black_scholes.py new file mode 100644 index 00000000..c3b6c979 --- /dev/null +++ b/vnpy/app/option_master/pricing/black_scholes.py @@ -0,0 +1,216 @@ +from scipy import stats +from math import log, pow, sqrt, exp +from typing import Tuple + +cdf = stats.norm.cdf +pdf = stats.norm.pdf + + +def calculate_d1( + s: float, + k: float, + r: float, + t: float, + v: float +) -> float: + """Calculate option D1 value""" + d1: float = (log(s / k) + (r + 0.5 * pow(v, 2)) * t) / (v * sqrt(t)) + return d1 + + +def calculate_price( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0 +) -> float: + """Calculate option price""" + # Return option space value if volatility not positive + if v <= 0: + return max(0, cp * (s - k)) + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + d2: float = d1 - v * sqrt(t) + + price: float = cp * (s * cdf(cp * d1) - k * cdf(cp * d2) * exp(-r * t)) + return price + + +def calculate_delta( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0 +) -> float: + """Calculate option delta""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + _delta: float = cp * cdf(cp * d1) + delta: float = _delta * s * 0.01 + return delta + + +def calculate_gamma( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option gamma""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + _gamma: float = pdf(d1) / (s * v * sqrt(t)) + gamma: float = _gamma * pow(s, 2) * 0.0001 + + return gamma + + +def calculate_theta( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + d1: float = 0.0, + annual_days: int = 240 +) -> float: + """Calculate option theta""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + d2: float = d1 - v * sqrt(t) + + _theta = -s * pdf(d1) * v / (2 * sqrt(t)) \ + - cp * r * k * exp(-r * t) * cdf(cp * d2) + theta = _theta / annual_days + + return theta + + +def calculate_vega( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option vega(%)""" + vega: float = calculate_original_vega(s, k, r, t, v, d1) / 100 + return vega + + +def calculate_original_vega( + s: float, + k: float, + r: float, + t: float, + v: float, + d1: float = 0.0 +) -> float: + """Calculate option vega""" + if v <= 0: + return 0 + + if not d1: + d1: float = calculate_d1(s, k, r, t, v) + + vega: float = s * pdf(d1) * sqrt(t) + + return vega + + +def calculate_greeks( + s: float, + k: float, + r: float, + t: float, + v: float, + cp: int, + annual_days: int = 240 +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + d1: float = calculate_d1(s, k, r, t, v) + price: float = calculate_price(s, k, r, t, v, cp, d1) + delta: float = calculate_delta(s, k, r, t, v, cp, d1) + gamma: float = calculate_gamma(s, k, r, t, v, d1) + theta: float = calculate_theta(s, k, r, t, v, cp, d1, annual_days) + vega: float = calculate_vega(s, k, r, t, v, d1) + return price, delta, gamma, theta, vega + + +def calculate_impv( + price: float, + s: float, + k: float, + r: float, + t: float, + cp: int +): + """Calculate option implied volatility""" + # Check option price must be positive + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet: bool = False + + if cp == 1 and (price > (s - k) * exp(-r * t)): + meet = True + elif cp == -1 and (price > k * exp(-r * t) - s): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v: float = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p: float = calculate_price(s, k, r, t, v, cp) + vega: float = calculate_original_vega(s, k, r, t, v, cp) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx: float = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/black_scholes_cython.pyd b/vnpy/app/option_master/pricing/black_scholes_cython.pyd new file mode 100644 index 00000000..a636dbb1 Binary files /dev/null and b/vnpy/app/option_master/pricing/black_scholes_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx new file mode 100644 index 00000000..9390d302 --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx @@ -0,0 +1,269 @@ +from typing import Tuple +import numpy as np + +cimport numpy as np +cimport cython + +cdef extern from "math.h" nogil: + double exp(double) + double sqrt(double) + double pow(double, double) + double fmax(double, double) + double fabs(double) + + +DEFAULT_STEP = 15 + + +cdef tuple generate_tree( + double f, + double k, + double r, + double t, + double v, + int cp, + int n +): + """Generate binomial tree for pricing American option.""" + cdef double dt = t / n + cdef double u = exp(v * sqrt(dt)) + cdef double d = 1 / u + cdef double a = 1 + + cdef int tree_size = n + 1 + cdef np.ndarray[np.double_t, ndim = 2] underlying_tree = np.zeros((tree_size, tree_size)) + cdef np.ndarray[np.double_t, ndim = 2] option_tree = np.zeros((tree_size, tree_size)) + + cdef int i, j + + # Calculate risk neutral probability + cdef double p = (a - d) / (u - d) + cdef double p1 = p / a + cdef double p2 = (1 - p) / a + + # Calculate underlying price tree + underlying_tree[0, 0] = f + + for i in range(1, n + 1): + underlying_tree[0, i] = underlying_tree[0, i - 1] * u + for j in range(1, n + 1): + underlying_tree[j, i] = underlying_tree[j - 1, i - 1] * d + + # Calculate option price tree + for j in range(n + 1): + option_tree[j, n] = max(0, cp * (underlying_tree[j, n] - k)) + + for i in range(n - 1, -1, -1): + for j in range(i + 1): + option_tree[j, i] = max( + (p1 * option_tree[j, i + 1] + p2 * option_tree[j + 1, i + 1]), + cp * (underlying_tree[j, i] - k) + ) + + # Return both trees + return option_tree, underlying_tree + + +def calculate_price( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP +) -> float: + """Calculate option price""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + return option_tree[0, 0] + + +def calculate_delta( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP +) -> float: + """Calculate option delta""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + option_price_change = option_tree[0, 1] - option_tree[1, 1] + underlying_price_change = underlying_tree[0, 1] - underlying_tree[1, 1] + return option_price_change / underlying_price_change + + +def calculate_gamma( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP +) -> float: + """Calculate option gamma""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + + gamma_delta_1 = (option_tree[0, 2] - option_tree[1, 2]) / \ + (underlying_tree[0, 2] - underlying_tree[1, 2]) + gamma_delta_2 = (option_tree[1, 2] - option_tree[2, 2]) / \ + (underlying_tree[1, 2] - underlying_tree[2, 2]) + gamma = (gamma_delta_1 - gamma_delta_2) / \ + (0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2])) + + return gamma + + +def calculate_theta( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP, + int annual_days = 240 +) -> float: + """Calcualte option theta""" + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + + dt = t / n + theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days) + + return theta + + +def calculate_vega( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP +) -> float: + """Calculate option vega(%)""" + vega = calculate_original_vega(f, k, r, t, v, cp, n) / 100 + return vega + + +cdef double calculate_original_vega( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP +): + """Calculate option vega""" + cdef double price_1 = calculate_price(f, k, r, t, v, cp, n) + cdef double price_2 = calculate_price(f, k, r, t, v * 1.001, cp, n) + cdef double vega = (price_2 - price_1) / (v * 0.001) + return vega + + +def calculate_greeks( + double f, + double k, + double r, + double t, + double v, + int cp, + int n = DEFAULT_STEP, + int annual_days = 240 +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + cdef double dt = t / n + cdef price, delta, gamma, vega, theta + cdef option_price_change, underlying_price_change + cdef gamma_delta_1, gamma_delta_2 + + option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n) + option_tree_vega, underlying_tree_vega = generate_tree(f, k, r, t, v * 1.001, cp, n) + + # Price + price = option_tree[0, 0] + + # Delta + option_price_change = option_tree[0, 1] - option_tree[1, 1] + underlying_price_change = underlying_tree[0, 1] - underlying_tree[1, 1] + delta = option_price_change / underlying_price_change + + # Gamma + gamma_delta_1 = (option_tree[0, 2] - option_tree[1, 2]) / \ + (underlying_tree[0, 2] - underlying_tree[1, 2]) + gamma_delta_2 = (option_tree[1, 2] - option_tree[2, 2]) / \ + (underlying_tree[1, 2] - underlying_tree[2, 2]) + gamma = (gamma_delta_1 - gamma_delta_2) / \ + (0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2])) + + # Theta + theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days) + + # Vega + vega = (option_tree_vega[0, 0] - option_tree[0, 0]) / (0.001 * v * 100) + + return price, delta, gamma, theta, vega + + +def calculate_impv( + double price, + double f, + double k, + double r, + double t, + int cp, + int n = DEFAULT_STEP +) -> float: + """Calculate option implied volatility""" + cdef double p, v, dx, vega + + # Check option price must be position + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet = False + + if cp == 1 and price > (f - k): + meet = True + elif cp == -1 and price > (k - f): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p = calculate_price(f, k, r, t, v, cp, n) + vega = calculate_original_vega(f, k, r, t, v, cp, n) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/setup.py b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/setup.py new file mode 100644 index 00000000..63512e01 --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/setup.py @@ -0,0 +1,9 @@ +from distutils.core import setup +from Cython.Build import cythonize +import numpy + +setup( + name='binomial_tree_cython', + ext_modules=cythonize("binomial_tree_cython.pyx"), + include_dirs=[numpy.get_include()] +) diff --git a/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx b/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx new file mode 100644 index 00000000..cd290cbb --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx @@ -0,0 +1,239 @@ +from typing import Tuple + +cdef extern from "math.h" nogil: + double exp(double) + double sqrt(double) + double pow(double, double) + double log(double) + double erf(double) + double fabs(double) + + +cdef double cdf(double x): + return 0.5 * (1 + erf(x / sqrt(2.0))) + + +cdef double pdf(double x): + # 1 / sqrt(2 * 3.1416) = 0.3989422804014327 + return exp(- pow(x, 2) * 0.5) * 0.3989422804014327 + + +cdef double calculate_d1(double s, double k, double r, double t, double v): + """Calculate option D1 value""" + return (log(s / k) + (0.5 * pow(v, 2)) * t) / (v * sqrt(t)) + + +def calculate_price( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0 +) -> float: + """Calculate option price""" + cdef double d2, price + + # Return option space value if volatility not positive + if v <= 0: + return max(0, cp * (s - k)) + + if not d1: + d1 = calculate_d1(s, k, r, r, v) + d2 = d1 - v * sqrt(t) + + price = cp * (s * cdf(cp * d1) - k * cdf(cp * d2)) * exp(-r * t) + return price + + +def calculate_delta( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0 +) -> float: + """Calculate option delta""" + cdef _delta, delta + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + _delta: float = cp * exp(-r * t) * cdf(cp * d1) + delta: float = _delta * s * 0.01 + return delta + + +def calculate_gamma( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option gamma""" + cdef _gamma, gamma + + if v <= 0 or s <= 0 or t<= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + _gamma = exp(-r * t) * pdf(d1) / (s * v * sqrt(t)) + gamma = _gamma * pow(s, 2) * 0.0001 + + return gamma + + +def calculate_theta( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0, + int annual_days = 240 +) -> float: + """Calculate option theta""" + cdef double d2, _theta, theta + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + d2: float = d1 - v * sqrt(t) + + _theta = -s * exp(-r * t) * pdf(d1) * v / (2 * sqrt(t)) \ + + cp * r * s * exp(-r * t) * cdf(cp * d1) \ + - cp * r * k * exp(-r * t) * cdf(cp * d2) + theta = _theta / annual_days + + return theta + + +def calculate_vega( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option vega(%)""" + vega: float = calculate_original_vega(s, k, r, t, v, d1) / 100 + return vega + + +def calculate_original_vega( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option vega""" + cdef double vega + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + vega: float = s * exp(-r * t) * pdf(d1) * sqrt(t) + + return vega + + +def calculate_greeks( + double s, + double k, + double r, + double t, + double v, + int cp, + int annual_days = 240, +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + cdef double d1, price, delta, gamma, theta, vega + + d1 = calculate_d1(s, k, r, t, v) + + price = calculate_price(s, k, r, t, v, cp, d1) + delta = calculate_delta(s, k, r, t, v, cp, d1) + gamma = calculate_gamma(s, k, r, t, v, d1) + theta = calculate_theta(s, k, r, t, v, cp, d1, annual_days) + vega = calculate_vega(s, k, r, t, v, d1) + + return price, delta, gamma, theta, vega + + +def calculate_impv( + double price, + double s, + double k, + double r, + double t, + int cp +): + """Calculate option implied volatility""" + cdef bint meet + cdef double v, p, vega, dx + + # Check option prive must be positive + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet = False + + if cp == 1 and (price > (s - k) * exp(-r * t)): + meet = True + elif cp == -1 and (price > k * exp(-r * t) - s): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p = calculate_price(s, k, r, t, v, cp) + vega = calculate_original_vega(s, k, r, t, v, cp) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/cython_model/black_76_cython/setup.py b/vnpy/app/option_master/pricing/cython_model/black_76_cython/setup.py new file mode 100644 index 00000000..17e6b589 --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/black_76_cython/setup.py @@ -0,0 +1,7 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup( + name='black_76_cython', + ext_modules=cythonize("black_76_cython.pyx"), +) diff --git a/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx new file mode 100644 index 00000000..6af59d7f --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx @@ -0,0 +1,238 @@ +from typing import Tuple + +cdef extern from "math.h" nogil: + double exp(double) + double sqrt(double) + double pow(double, double) + double log(double) + double erf(double) + double fabs(double) + + +cdef double cdf(double x): + return 0.5 * (1 + erf(x / sqrt(2.0))) + + +cdef double pdf(double x): + # 1 / sqrt(2 * 3.1416) = 0.3989422804014327 + return exp(- pow(x, 2) * 0.5) * 0.3989422804014327 + + +cdef double calculate_d1(double s, double k, double r, double t, double v): + """Calculate option D1 value""" + return (log(s / k) + (r + 0.5 * pow(v, 2)) * t) / (v * sqrt(t)) + + +def calculate_price( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0 +) -> float: + """Calculate option price""" + cdef double d2, price + + # Return option space value if volatility not positive + if v <= 0: + return max(0, cp * (s - k)) + + if not d1: + d1 = calculate_d1(s, k, r, r, v) + d2 = d1 - v * sqrt(t) + + price = cp * (s * cdf(cp * d1) - k * cdf(cp * d2) * exp(-r * t)) + return price + + +def calculate_delta( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0 +) -> float: + """Calculate option delta""" + cdef _delta, delta + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + _delta: float = cp * cdf(cp * d1) + delta: float = _delta * s * 0.01 + return delta + + +def calculate_gamma( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option gamma""" + cdef _gamma, gamma + + if v <= 0 or s <= 0 or t<= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + _gamma = pdf(d1) / (s * v * sqrt(t)) + gamma = _gamma * pow(s, 2) * 0.0001 + + return gamma + + +def calculate_theta( + double s, + double k, + double r, + double t, + double v, + int cp, + double d1 = 0.0, + int annual_days = 240 +) -> float: + """Calculate option theta""" + cdef double d2, _theta, theta + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + d2: float = d1 - v * sqrt(t) + + _theta = -s * pdf(d1) * v / (2 * sqrt(t)) \ + - cp * r * k * exp(-r * t) * cdf(cp * d2) + theta = _theta / annual_days + + return theta + + +def calculate_vega( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option vega(%)""" + vega: float = calculate_original_vega(s, k, r, t, v, d1) / 100 + return vega + + +def calculate_original_vega( + double s, + double k, + double r, + double t, + double v, + double d1 = 0.0 +) -> float: + """Calculate option vega""" + cdef double vega + + if v <= 0: + return 0 + + if not d1: + d1 = calculate_d1(s, k, r, t, v) + + vega: float = s * pdf(d1) * sqrt(t) + + return vega + + +def calculate_greeks( + double s, + double k, + double r, + double t, + double v, + int cp, + int annual_days = 240, +) -> Tuple[float, float, float, float, float]: + """Calculate option price and greeks""" + cdef double d1, price, delta, gamma, theta, vega + + d1 = calculate_d1(s, k, r, t, v) + + price = calculate_price(s, k, r, t, v, cp, d1) + delta = calculate_delta(s, k, r, t, v, cp, d1) + gamma = calculate_gamma(s, k, r, t, v, d1) + theta = calculate_theta(s, k, r, t, v, cp, d1, annual_days) + vega = calculate_vega(s, k, r, t, v, d1) + + return price, delta, gamma, theta, vega + + +def calculate_impv( + double price, + double s, + double k, + double r, + double t, + int cp +): + """Calculate option implied volatility""" + cdef bint meet + cdef double v, p, vega, dx + + # Check option prive must be positive + if price <= 0: + return 0 + + # Check if option price meets minimum value (exercise value) + meet = False + + if cp == 1 and (price > (s - k) * exp(-r * t)): + meet = True + elif cp == -1 and (price > k * exp(-r * t) - s): + meet = True + + # If minimum value not met, return 0 + if not meet: + return 0 + + # Calculate implied volatility with Newton's method + v = 0.3 # Initial guess of volatility + + for i in range(50): + # Caculate option price and vega with current guess + p = calculate_price(s, k, r, t, v, cp) + vega = calculate_original_vega(s, k, r, t, v, cp) + + # Break loop if vega too close to 0 + if not vega: + break + + # Calculate error value + dx = (price - p) / vega + + # Check if error value meets requirement + if abs(dx) < 0.00001: + break + + # Calculate guessed implied volatility of next round + v += dx + + # Check end result to be non-negative + if v <= 0: + return 0 + + # Round to 4 decimal places + v = round(v, 4) + + return v diff --git a/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/setup.py b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/setup.py new file mode 100644 index 00000000..5c38c23d --- /dev/null +++ b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/setup.py @@ -0,0 +1,7 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup( + name='black_scholes_cython', + ext_modules=cythonize("black_scholes_cython.pyx"), +) diff --git a/vnpy/app/option_master/time.py b/vnpy/app/option_master/time.py new file mode 100644 index 00000000..5600ec63 --- /dev/null +++ b/vnpy/app/option_master/time.py @@ -0,0 +1,61 @@ +from datetime import datetime, timedelta + + +ANNUAL_DAYS = 240 + +# For checking public holidays +PUBLIC_HOLIDAYS = set([ + datetime(2020, 1, 1), # New Year + + datetime(2020, 1, 24), # Spring Festival + datetime(2020, 1, 25), + datetime(2020, 1, 26), + datetime(2020, 1, 27), + datetime(2020, 1, 28), + datetime(2020, 1, 29), + datetime(2020, 1, 30), + + datetime(2020, 4, 4), # Qingming Festval + datetime(2020, 4, 5), + datetime(2020, 4, 6), + + datetime(2020, 5, 1), # Labour Day + datetime(2020, 5, 2), + datetime(2020, 5, 3), + datetime(2020, 5, 4), + datetime(2020, 5, 5), + + datetime(2020, 6, 25), # Duanwu Festival + datetime(2020, 6, 26), + datetime(2020, 6, 27), + + datetime(2020, 10, 1), # National Day + datetime(2020, 10, 2), + datetime(2020, 10, 3), + datetime(2020, 10, 4), + datetime(2020, 10, 5), + datetime(2020, 10, 6), + datetime(2020, 10, 7), + datetime(2020, 10, 8), +]) + + +def calculate_days_to_expiry(option_expiry: datetime) -> int: + """""" + current_dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + days = 1 + + while current_dt <= option_expiry: + current_dt += timedelta(days=1) + + # Ignore weekends + if current_dt.weekday() in [5, 6]: + continue + + # Ignore public holidays + if current_dt in PUBLIC_HOLIDAYS: + continue + + days += 1 + + return days diff --git a/vnpy/app/option_master/ui/__init__.py b/vnpy/app/option_master/ui/__init__.py new file mode 100644 index 00000000..248639d7 --- /dev/null +++ b/vnpy/app/option_master/ui/__init__.py @@ -0,0 +1 @@ +from .widget import OptionManager diff --git a/vnpy/app/option_master/ui/chart.py b/vnpy/app/option_master/ui/chart.py new file mode 100644 index 00000000..a3af98b0 --- /dev/null +++ b/vnpy/app/option_master/ui/chart.py @@ -0,0 +1,408 @@ +from typing import Dict, List + +import pyqtgraph as pg + +from vnpy.trader.ui import QtWidgets, QtCore +from vnpy.trader.event import EVENT_TIMER + +from ..base import PortfolioData +from ..engine import OptionEngine, Event +from ..time import ANNUAL_DAYS + +import numpy as np +import matplotlib +matplotlib.use('Qt5Agg') # noqa +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas # noqa +from matplotlib.figure import Figure +from mpl_toolkits.mplot3d import Axes3D # noqa +from pylab import mpl + +plt.style.use("dark_background") +mpl.rcParams['font.sans-serif'] = ['Microsoft YaHei'] # set font for Chinese +mpl.rcParams['axes.unicode_minus'] = False + + +class OptionVolatilityChart(QtWidgets.QWidget): + + signal_timer = QtCore.pyqtSignal(Event) + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.portfolio_name = portfolio_name + + self.timer_count = 0 + self.timer_trigger = 3 + + self.chain_checks: Dict[str, QtWidgets.QCheckBox] = {} + self.put_curves: Dict[str, pg.PlotCurveItem] = {} + self.call_curves: Dict[str, pg.PlotCurveItem] = {} + self.pricing_curves: Dict[str, pg.PlotCurveItem] = {} + + self.colors: List = [ + (255, 0, 0), + (255, 255, 0), + (0, 255, 0), + (0, 0, 255), + (0, 128, 0), + (19, 234, 201), + (195, 46, 212), + (250, 194, 5), + (0, 114, 189), + ] + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("波动率曲线") + + # Create checkbox for each chain + hbox = QtWidgets.QHBoxLayout() + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + chain_symbols = list(portfolio.chains.keys()) + chain_symbols.sort() + + hbox.addStretch() + + for chain_symbol in chain_symbols: + chain_check = QtWidgets.QCheckBox() + chain_check.setText(chain_symbol.split(".")[0]) + chain_check.setChecked(True) + chain_check.stateChanged.connect(self.update_curve_visible) + + hbox.addWidget(chain_check) + self.chain_checks[chain_symbol] = chain_check + + hbox.addStretch() + + # Create graphics window + pg.setConfigOptions(antialias=True) + + graphics_window = pg.GraphicsWindow() + self.impv_chart = graphics_window.addPlot(title="隐含波动率曲线") + self.impv_chart.showGrid(x=True, y=True) + self.impv_chart.setLabel("left", "波动率") + self.impv_chart.setLabel("bottom", "行权价") + self.impv_chart.addLegend() + self.impv_chart.setMenuEnabled(False) + self.impv_chart.setMouseEnabled(False, False) + + for chain_symbol in chain_symbols: + self.add_impv_curve(chain_symbol) + + # Set Layout + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox) + vbox.addWidget(graphics_window) + self.setLayout(vbox) + + def register_event(self) -> None: + """""" + self.signal_timer.connect(self.process_timer_event) + + self.event_engine.register(EVENT_TIMER, self.signal_timer.emit) + + def process_timer_event(self, event: Event) -> None: + """""" + self.timer_count += 1 + if self.timer_count < self.timer_trigger: + return + self.timer_trigger = 0 + + self.update_curve_data() + + def add_impv_curve(self, chain_symbol: str) -> None: + """""" + symbol_size = 14 + symbol = chain_symbol.split(".")[0] + color = self.colors.pop(0) + pen = pg.mkPen(color, width=2) + + self.call_curves[chain_symbol] = self.impv_chart.plot( + symbolSize=symbol_size, + symbol="t1", + name=symbol + " 看涨", + pen=pen, + symbolBrush=color + ) + self.put_curves[chain_symbol] = self.impv_chart.plot( + symbolSize=symbol_size, + symbol="t", + name=symbol + " 看跌", + pen=pen, + symbolBrush=color + ) + self.pricing_curves[chain_symbol] = self.impv_chart.plot( + symbolSize=symbol_size, + symbol="o", + name=symbol + " 定价", + pen=pen, + symbolBrush=color + ) + + def update_curve_data(self) -> None: + """""" + portfolio: PortfolioData = self.option_engine.get_portfolio(self.portfolio_name) + + for chain in portfolio.chains.values(): + call_impv = [] + put_impv = [] + pricing_impv = [] + strike_prices = [] + + for index in chain.indexes: + call = chain.calls[index] + call_impv.append(call.mid_impv * 100) + pricing_impv.append(call.pricing_impv * 100) + strike_prices.append(call.strike_price) + + put = chain.puts[index] + put_impv.append(put.mid_impv * 100) + + self.call_curves[chain.chain_symbol].setData( + y=call_impv, + x=strike_prices + ) + self.put_curves[chain.chain_symbol].setData( + y=put_impv, + x=strike_prices + ) + self.pricing_curves[chain.chain_symbol].setData( + y=pricing_impv, + x=strike_prices + ) + + def update_curve_visible(self) -> None: + """""" + # Remove old + legend: pg.LegendItem = self.impv_chart.legend + legend.scene().removeItem(legend) + + self.impv_chart.clear() + + # Add new + self.impv_chart.addLegend() + + for chain_symbol, checkbox in self.chain_checks.items(): + if checkbox.isChecked(): + call_curve = self.call_curves[chain_symbol] + put_curve = self.put_curves[chain_symbol] + pricing_curve = self.pricing_curves[chain_symbol] + + self.impv_chart.addItem(call_curve) + self.impv_chart.addItem(put_curve) + self.impv_chart.addItem(pricing_curve) + + +class ScenarioAnalysisChart(QtWidgets.QWidget): + """""" + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.portfolio_name = portfolio_name + + self.init_ui() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("情景分析") + + # Create widgets + self.price_change_spin = QtWidgets.QSpinBox() + self.price_change_spin.setSuffix("%") + self.price_change_spin.setMinimum(2) + self.price_change_spin.setValue(10) + + self.impv_change_spin = QtWidgets.QSpinBox() + self.impv_change_spin.setSuffix("%") + self.impv_change_spin.setMinimum(2) + self.impv_change_spin.setValue(10) + + self.time_change_spin = QtWidgets.QSpinBox() + self.time_change_spin.setSuffix("日") + self.time_change_spin.setMinimum(0) + self.time_change_spin.setValue(1) + + self.target_combo = QtWidgets.QComboBox() + self.target_combo.addItems([ + "盈亏", + "Delta", + "Gamma", + "Theta", + "Vega" + ]) + + button = QtWidgets.QPushButton("执行分析") + button.clicked.connect(self.run_analysis) + + # Create charts + fig = Figure() + canvas = FigureCanvas(fig) + + self.ax = fig.gca(projection="3d") + self.ax.set_xlabel("价格涨跌 %") + self.ax.set_ylabel("波动率涨跌 %") + self.ax.set_zlabel("盈亏") + + # Set layout + hbox1 = QtWidgets.QHBoxLayout() + hbox1.addWidget(QtWidgets.QLabel("目标数据")) + hbox1.addWidget(self.target_combo) + hbox1.addWidget(QtWidgets.QLabel("时间衰减")) + hbox1.addWidget(self.time_change_spin) + hbox1.addStretch() + + hbox2 = QtWidgets.QHBoxLayout() + hbox2.addWidget(QtWidgets.QLabel("价格变动")) + hbox2.addWidget(self.price_change_spin) + hbox2.addWidget(QtWidgets.QLabel("波动率变动")) + hbox2.addWidget(self.impv_change_spin) + hbox2.addStretch() + hbox2.addWidget(button) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox1) + vbox.addLayout(hbox2) + vbox.addWidget(canvas) + + self.setLayout(vbox) + + def run_analysis(self) -> None: + """""" + # Generate range + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + price_change_range = self.price_change_spin.value() + price_changes = np.arange(-price_change_range, price_change_range + 1) / 100 + + impv_change_range = self.impv_change_spin.value() + impv_changes = np.arange(-impv_change_range, impv_change_range + 1) / 100 + + time_change = self.time_change_spin.value() / ANNUAL_DAYS + target_name = self.target_combo.currentText() + + # Check underlying price exists + for underlying in portfolio.underlyings.values(): + if not underlying.mid_price: + QtWidgets.QMessageBox.warning( + self, + "无法执行情景分析", + f"标的物{underlying.symbol}当前中间价为{underlying.mid_price}", + QtWidgets.QMessageBox.Ok + ) + return + + # Run analysis calculation + pnls = [] + deltas = [] + gammas = [] + thetas = [] + vegas = [] + + for impv_change in impv_changes: + pnl_buf = [] + delta_buf = [] + gamma_buf = [] + theta_buf = [] + vega_buf = [] + + for price_change in price_changes: + portfolio_pnl = 0 + portfolio_delta = 0 + portfolio_gamma = 0 + portfolio_theta = 0 + portfolio_vega = 0 + + # Calculate underlying pnl + for underlying in portfolio.underlyings.values(): + if not underlying.net_pos: + continue + + value = underlying.mid_price * underlying.net_pos * underlying.size + portfolio_pnl += value * price_change + portfolio_delta += value / 100 + + # Calculate option pnl + for option in portfolio.options.values(): + if not option.net_pos: + continue + + new_underlying_price = option.underlying.mid_price * (1 + price_change) + new_time_to_expiry = max(option.time_to_expiry - time_change, 0) + new_mid_impv = option.mid_impv * (1 + impv_change) + + new_price, delta, gamma, theta, vega = option.calculate_greeks( + new_underlying_price, + option.strike_price, + option.interest_rate, + new_time_to_expiry, + new_mid_impv, + option.option_type + ) + + diff = new_price - option.tick.last_price + multiplier = option.net_pos * option.size + + portfolio_pnl += diff * multiplier + portfolio_delta += delta * multiplier + portfolio_gamma += gamma * multiplier + portfolio_theta += theta * multiplier + portfolio_vega += vega * multiplier + + pnl_buf.append(portfolio_pnl) + delta_buf.append(portfolio_delta) + gamma_buf.append(portfolio_gamma) + theta_buf.append(portfolio_theta) + vega_buf.append(portfolio_vega) + + pnls.append(pnl_buf) + deltas.append(delta_buf) + gammas.append(gamma_buf) + thetas.append(theta_buf) + vegas.append(vega_buf) + + # Plot chart + if target_name == "盈亏": + target_data = pnls + elif target_name == "Delta": + target_data = deltas + elif target_name == "Gamma": + target_data = gammas + elif target_name == "Theta": + target_data = thetas + else: + target_data = vegas + + self.update_chart(price_changes * 100, impv_changes * 100, target_data, target_name) + + def update_chart( + self, + price_changes: np.array, + impv_changes: np.array, + target_data: List[List[float]], + target_name: str + ) -> None: + """""" + self.ax.clear() + + price_changes, impv_changes = np.meshgrid(price_changes, impv_changes) + + self.ax.set_zlabel(target_name) + self.ax.plot_surface( + X=price_changes, + Y=impv_changes, + Z=np.array(target_data), + rstride=1, + cstride=1, + cmap=matplotlib.cm.coolwarm + ) diff --git a/vnpy/app/option_master/ui/manager.py b/vnpy/app/option_master/ui/manager.py new file mode 100644 index 00000000..a4b59da2 --- /dev/null +++ b/vnpy/app/option_master/ui/manager.py @@ -0,0 +1,891 @@ +from typing import Dict, List, Tuple +from copy import copy +from functools import partial + +from scipy import interpolate + +from vnpy.event import Event +from vnpy.trader.ui import QtWidgets, QtCore +from vnpy.trader.event import EVENT_TICK, EVENT_TIMER + +from ..engine import OptionEngine +from ..base import ( + EVENT_OPTION_ALGO_PRICING, + EVENT_OPTION_ALGO_TRADING, + EVENT_OPTION_ALGO_STATUS, + EVENT_OPTION_ALGO_LOG +) +from .monitor import ( + MonitorCell, IndexCell, BidCell, AskCell, PosCell, + COLOR_WHITE, COLOR_BLACK +) + + +class AlgoSpinBox(QtWidgets.QSpinBox): + """""" + + def __init__(self): + """""" + super().__init__() + + self.setMaximum(999999) + self.setMinimum(-999999) + self.setAlignment(QtCore.Qt.AlignCenter) + + def get_value(self) -> int: + """""" + return self.value() + + def update_status(self, active: bool) -> None: + """""" + self.setEnabled(not active) + + +class AlgoPositiveSpinBox(AlgoSpinBox): + """""" + + def __init__(self): + """""" + super().__init__() + + self.setMinimum(0) + + +class AlgoDoubleSpinBox(QtWidgets.QDoubleSpinBox): + """""" + + def __init__(self): + """""" + super().__init__() + + self.setDecimals(1) + self.setMaximum(9999.9) + self.setMinimum(0) + self.setAlignment(QtCore.Qt.AlignCenter) + + def get_value(self) -> float: + """""" + return self.value() + + def update_status(self, active: bool) -> None: + """""" + self.setEnabled(not active) + + +class AlgoDirectionCombo(QtWidgets.QComboBox): + """""" + + def __init__(self): + """""" + super().__init__() + + self.addItems([ + "双向", + "做多", + "做空" + ]) + + def get_value(self) -> Dict[str, bool]: + """""" + if self.currentText() == "双向": + value = { + "long_allowed": True, + "short_allowed": True + } + elif self.currentText() == "做多": + value = { + "long_allowed": True, + "short_allowed": False + } + else: + value = { + "long_allowed": False, + "short_allowed": True + } + + return value + + def update_status(self, active: bool) -> None: + """""" + self.setEnabled(not active) + + +class AlgoPricingButton(QtWidgets.QPushButton): + """""" + + def __init__(self, vt_symbol: str, manager: "ElectronicEyeManager"): + """""" + super().__init__() + + self.vt_symbol = vt_symbol + self.manager = manager + + self.active = False + self.setText("N") + self.clicked.connect(self.on_clicked) + + def on_clicked(self) -> None: + """""" + if self.active: + self.manager.stop_algo_pricing(self.vt_symbol) + else: + self.manager.start_algo_pricing(self.vt_symbol) + + def update_status(self, active: bool) -> None: + """""" + self.active = active + + if active: + self.setText("Y") + else: + self.setText("N") + + +class AlgoTradingButton(QtWidgets.QPushButton): + """""" + + def __init__(self, vt_symbol: str, manager: "ElectronicEyeManager"): + """""" + super().__init__() + + self.vt_symbol = vt_symbol + self.manager = manager + + self.active = False + self.setText("N") + self.clicked.connect(self.on_clicked) + + def on_clicked(self) -> None: + """""" + if self.active: + self.manager.stop_algo_trading(self.vt_symbol) + else: + self.manager.start_algo_trading(self.vt_symbol) + + def update_status(self, active: bool) -> None: + """""" + self.active = active + + if active: + self.setText("Y") + else: + self.setText("N") + + +class ElectronicEyeMonitor(QtWidgets.QTableWidget): + """""" + + signal_tick = QtCore.pyqtSignal(Event) + signal_pricing = QtCore.pyqtSignal(Event) + signal_status = QtCore.pyqtSignal(Event) + signal_trading = QtCore.pyqtSignal(Event) + + headers: List[Dict] = [ + {"name": "bid_volume", "display": "买量", "cell": BidCell}, + {"name": "bid_price", "display": "买价", "cell": BidCell}, + {"name": "ask_price", "display": "卖价", "cell": AskCell}, + {"name": "ask_volume", "display": "卖量", "cell": AskCell}, + {"name": "algo_bid_price", "display": "目标\n买价", "cell": BidCell}, + {"name": "algo_ask_price", "display": "目标\n卖价", "cell": AskCell}, + {"name": "algo_spread", "display": "价差", "cell": MonitorCell}, + {"name": "ref_price", "display": "理论价", "cell": MonitorCell}, + {"name": "pricing_impv", "display": "定价\n隐波", "cell": MonitorCell}, + {"name": "net_pos", "display": "净持仓", "cell": PosCell}, + + {"name": "price_spread", "display": "价格\n价差", "cell": AlgoDoubleSpinBox}, + {"name": "volatility_spread", "display": "隐波\n价差", "cell": AlgoDoubleSpinBox}, + {"name": "max_pos", "display": "持仓\n上限", "cell": AlgoPositiveSpinBox}, + {"name": "target_pos", "display": "目标\n持仓", "cell": AlgoSpinBox}, + {"name": "max_order_size", "display": "最大\n委托", "cell": AlgoPositiveSpinBox}, + {"name": "direction", "display": "方向", "cell": AlgoDirectionCombo}, + {"name": "pricing_active", "display": "定价", "cell": AlgoPricingButton}, + {"name": "trading_active", "display": "交易", "cell": AlgoTradingButton}, + + ] + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.algo_engine = option_engine.algo_engine + self.portfolio_name = portfolio_name + + self.cells: Dict[str, Dict] = {} + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("电子眼") + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + # Set table row and column numbers + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + row_count = 0 + for chain in portfolio.chains.values(): + row_count += (1 + len(chain.indexes)) + self.setRowCount(row_count) + + column_count = len(self.headers) * 2 + 1 + self.setColumnCount(column_count) + + call_labels = [d["display"] for d in self.headers] + put_labels = copy(call_labels) + put_labels.reverse() + labels = call_labels + ["行权价"] + put_labels + self.setHorizontalHeaderLabels(labels) + + # Init cells + strike_column = len(self.headers) + current_row = 0 + + chain_symbols = list(portfolio.chains.keys()) + chain_symbols.sort() + + for chain_symbol in chain_symbols: + chain = portfolio.get_chain(chain_symbol) + + self.setItem( + current_row, + strike_column, + IndexCell(chain.chain_symbol.split(".")[0]) + ) + + for index in chain.indexes: + call = chain.calls[index] + put = chain.puts[index] + + current_row += 1 + + # Call cells + call_cells = {} + + for column, d in enumerate(self.headers): + cell_type = d["cell"] + + if issubclass(cell_type, QtWidgets.QPushButton): + cell = cell_type(call.vt_symbol, self) + else: + cell = cell_type() + + call_cells[d["name"]] = cell + + if isinstance(cell, QtWidgets.QTableWidgetItem): + self.setItem(current_row, column, cell) + else: + self.setCellWidget(current_row, column, cell) + + self.cells[call.vt_symbol] = call_cells + + # Put cells + put_cells = {} + put_headers = copy(self.headers) + put_headers.reverse() + + for column, d in enumerate(put_headers): + column += (strike_column + 1) + + cell_type = d["cell"] + + if issubclass(cell_type, QtWidgets.QPushButton): + cell = cell_type(put.vt_symbol, self) + else: + cell = cell_type() + + put_cells[d["name"]] = cell + + if isinstance(cell, QtWidgets.QTableWidgetItem): + self.setItem(current_row, column, cell) + else: + self.setCellWidget(current_row, column, cell) + + self.cells[put.vt_symbol] = put_cells + + # Strike cell + index_cell = IndexCell(str(call.chain_index)) + self.setItem(current_row, strike_column, index_cell) + + # Move to next row + current_row += 1 + + self.resizeColumnsToContents() + + def register_event(self) -> None: + """""" + self.signal_pricing.connect(self.process_pricing_event) + self.signal_trading.connect(self.process_trading_event) + self.signal_status.connect(self.process_status_event) + self.signal_tick.connect(self.process_tick_event) + + self.event_engine.register( + EVENT_OPTION_ALGO_PRICING, + self.signal_pricing.emit + ) + self.event_engine.register( + EVENT_OPTION_ALGO_TRADING, + self.signal_trading.emit + ) + self.event_engine.register( + EVENT_OPTION_ALGO_STATUS, + self.signal_status.emit + ) + self.event_engine.register( + EVENT_TICK, + self.signal_tick.emit + ) + + def process_tick_event(self, event: Event) -> None: + """""" + tick = event.data + cells = self.cells.get(tick.vt_symbol, None) + if not cells: + return + + cells["bid_price"].setText(str(tick.bid_price_1)) + cells["ask_price"].setText(str(tick.ask_price_1)) + cells["bid_volume"].setText(str(tick.bid_volume_1)) + cells["ask_volume"].setText(str(tick.ask_volume_1)) + + def process_status_event(self, event: Event) -> None: + """""" + algo = event.data + cells = self.cells[algo.vt_symbol] + + cells["price_spread"].update_status(algo.pricing_active) + cells["volatility_spread"].update_status(algo.pricing_active) + cells["pricing_active"].update_status(algo.pricing_active) + + cells["max_pos"].update_status(algo.trading_active) + cells["target_pos"].update_status(algo.trading_active) + cells["max_order_size"].update_status(algo.trading_active) + cells["direction"].update_status(algo.trading_active) + cells["trading_active"].update_status(algo.trading_active) + + def process_pricing_event(self, event: Event) -> None: + """""" + algo = event.data + cells = self.cells[algo.vt_symbol] + + if algo.ref_price: + cells["algo_bid_price"].setText(str(algo.algo_bid_price)) + cells["algo_ask_price"].setText(str(algo.algo_ask_price)) + cells["algo_spread"].setText(str(algo.algo_spread)) + cells["ref_price"].setText(str(algo.ref_price)) + cells["pricing_impv"].setText(f"{algo.pricing_impv * 100:.2f}") + else: + cells["algo_bid_price"].setText("") + cells["algo_ask_price"].setText("") + cells["algo_spread"].setText("") + cells["ref_price"].setText("") + cells["pricing_impv"].setText("") + + def process_trading_event(self, event: Event) -> None: + """""" + algo = event.data + cells = self.cells[algo.vt_symbol] + + if algo.trading_active: + cells["net_pos"].setText(str(algo.option.net_pos)) + else: + cells["net_pos"].setText("") + + def process_position_event(self, event: Event) -> None: + """""" + algo = event.data + + cells = self.cells[algo.vt_symbol] + cells["net_pos"].setText(str(algo.option.net_pos)) + + def start_algo_pricing(self, vt_symbol: str) -> None: + """""" + cells = self.cells[vt_symbol] + + params = {} + params["price_spread"] = cells["price_spread"].get_value() + params["volatility_spread"] = cells["volatility_spread"].get_value() / 100 + + self.algo_engine.start_algo_pricing(vt_symbol, params) + + def stop_algo_pricing(self, vt_symbol: str) -> None: + """""" + self.algo_engine.stop_algo_pricing(vt_symbol) + + def start_algo_trading(self, vt_symbol: str) -> None: + """""" + cells = self.cells[vt_symbol] + + params = cells["direction"].get_value() + for name in [ + "max_pos", + "target_pos", + "max_order_size" + ]: + params[name] = cells[name].get_value() + + self.algo_engine.start_algo_trading(vt_symbol, params) + + def stop_algo_trading(self, vt_symbol: str) -> None: + """""" + self.algo_engine.stop_algo_trading(vt_symbol) + + +class ElectronicEyeManager(QtWidgets.QWidget): + """""" + + signal_log = QtCore.pyqtSignal(Event) + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_Engine = option_engine.event_engine + self.algo_engine = option_engine.algo_engine + self.portfolio_name = portfolio_name + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("期权电子眼") + + self.algo_monitor = ElectronicEyeMonitor(self.option_engine, self.portfolio_name) + + self.log_monitor = QtWidgets.QTextEdit() + self.log_monitor.setReadOnly(True) + self.log_monitor.setMaximumWidth(400) + + stop_pricing_button = QtWidgets.QPushButton("停止定价") + stop_pricing_button.clicked.connect(self.stop_pricing_for_all) + + stop_trading_button = QtWidgets.QPushButton("停止交易") + stop_trading_button.clicked.connect(self.stop_trading_for_all) + + self.price_spread_spin = AlgoDoubleSpinBox() + self.volatility_spread_spin = AlgoDoubleSpinBox() + self.direction_combo = AlgoDirectionCombo() + self.max_order_size_spin = AlgoPositiveSpinBox() + self.target_pos_spin = AlgoSpinBox() + self.max_pos_spin = AlgoPositiveSpinBox() + + price_spread_button = QtWidgets.QPushButton("设置") + price_spread_button.clicked.connect(self.set_price_spread_for_all) + + volatility_spread_button = QtWidgets.QPushButton("设置") + volatility_spread_button.clicked.connect(self.set_volatility_spread_for_all) + + direction_button = QtWidgets.QPushButton("设置") + direction_button.clicked.connect(self.set_direction_for_all) + + max_order_size_button = QtWidgets.QPushButton("设置") + max_order_size_button.clicked.connect(self.set_max_order_size_for_all) + + target_pos_button = QtWidgets.QPushButton("设置") + target_pos_button.clicked.connect(self.set_target_pos_for_all) + + max_pos_button = QtWidgets.QPushButton("设置") + max_pos_button.clicked.connect(self.set_max_pos_for_all) + + QLabel = QtWidgets.QLabel + grid = QtWidgets.QGridLayout() + grid.addWidget(QLabel("价格价差"), 0, 0) + grid.addWidget(self.price_spread_spin, 0, 1) + grid.addWidget(price_spread_button, 0, 2) + grid.addWidget(QLabel("隐波价差"), 1, 0) + grid.addWidget(self.volatility_spread_spin, 1, 1) + grid.addWidget(volatility_spread_button, 1, 2) + grid.addWidget(QLabel("持仓上限"), 2, 0) + grid.addWidget(self.max_pos_spin, 2, 1) + grid.addWidget(max_pos_button, 2, 2) + grid.addWidget(QLabel("目标持仓"), 3, 0) + grid.addWidget(self.target_pos_spin, 3, 1) + grid.addWidget(target_pos_button, 3, 2) + grid.addWidget(QLabel("最大委托"), 4, 0) + grid.addWidget(self.max_order_size_spin, 4, 1) + grid.addWidget(max_order_size_button, 4, 2) + grid.addWidget(QLabel("方向"), 5, 0) + grid.addWidget(self.direction_combo, 5, 1) + grid.addWidget(direction_button, 5, 2) + + hbox1 = QtWidgets.QHBoxLayout() + hbox1.addWidget(stop_pricing_button) + hbox1.addWidget(stop_trading_button) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox1) + vbox.addLayout(grid) + vbox.addWidget(self.log_monitor) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(self.algo_monitor) + hbox.addLayout(vbox) + + self.setLayout(hbox) + + def register_event(self) -> None: + """""" + self.signal_log.connect(self.process_log_event) + + self.event_Engine.register(EVENT_OPTION_ALGO_LOG, self.signal_log.emit) + + def process_log_event(self, event: Event) -> None: + """""" + log = event.data + timestr = log.time.strftime("%H:%M:%S") + msg = f"{timestr} {log.msg}" + self.log_monitor.append(msg) + + def show(self) -> None: + """""" + self.algo_engine.init_engine(self.portfolio_name) + self.algo_monitor.resizeColumnsToContents() + super().showMaximized() + + def set_price_spread_for_all(self) -> None: + """""" + price_spread = self.price_spread_spin.get_value() + + for cells in self.algo_monitor.cells.values(): + if cells["price_spread"].isEnabled(): + cells["price_spread"].setValue(price_spread) + + def set_volatility_spread_for_all(self) -> None: + """""" + volatility_spread = self.volatility_spread_spin.get_value() + + for cells in self.algo_monitor.cells.values(): + if cells["volatility_spread"].isEnabled(): + cells["volatility_spread"].setValue(volatility_spread) + + def set_direction_for_all(self) -> None: + """""" + ix = self.direction_combo.currentIndex() + + for cells in self.algo_monitor.cells.values(): + if cells["direction"].isEnabled(): + cells["direction"].setCurrentIndex(ix) + + def set_max_order_size_for_all(self) -> None: + """""" + size = self.max_order_size_spin.get_value() + + for cells in self.algo_monitor.cells.values(): + if cells["max_order_size"].isEnabled(): + cells["max_order_size"].setValue(size) + + def set_target_pos_for_all(self) -> None: + """""" + pos = self.target_pos_spin.get_value() + + for cells in self.algo_monitor.cells.values(): + if cells["target_pos"].isEnabled(): + cells["target_pos"].setValue(pos) + + def set_max_pos_for_all(self) -> None: + """""" + pos = self.max_pos_spin.get_value() + + for cells in self.algo_monitor.cells.values(): + if cells["max_pos"].isEnabled(): + cells["max_pos"].setValue(pos) + + def stop_pricing_for_all(self) -> None: + """""" + for vt_symbol in self.algo_monitor.cells.keys(): + self.algo_monitor.stop_algo_pricing(vt_symbol) + + def stop_trading_for_all(self) -> None: + """""" + for vt_symbol in self.algo_monitor.cells.keys(): + self.algo_monitor.stop_algo_trading(vt_symbol) + + +class VolatilityDoubleSpinBox(QtWidgets.QDoubleSpinBox): + """""" + + def __init__(self): + """""" + super().__init__() + + self.setDecimals(1) + self.setSuffix("%") + self.setMaximum(200.0) + self.setMinimum(0) + + def get_value(self) -> float: + """""" + return self.value() + + +class PricingVolatilityManager(QtWidgets.QWidget): + """""" + + signal_timer = QtCore.pyqtSignal(Event) + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.portfolio = option_engine.get_portfolio(portfolio_name) + + self.cells: Dict[Tuple, Dict] = {} + self.chain_symbols: List[str] = [] + self.chain_atm_index: Dict[str, str] = {} + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("波动率管理") + + tab = QtWidgets.QTabWidget() + vbox = QtWidgets.QVBoxLayout() + vbox.addWidget(tab) + self.setLayout(vbox) + + self.chain_symbols = list(self.portfolio.chains.keys()) + self.chain_symbols.sort() + + for chain_symbol in self.chain_symbols: + chain = self.portfolio.get_chain(chain_symbol) + + table = QtWidgets.QTableWidget() + table.setEditTriggers(table.NoEditTriggers) + table.verticalHeader().setVisible(False) + table.setColumnCount(4) + table.setRowCount(len(chain.indexes)) + table.setHorizontalHeaderLabels([ + "行权价", + "中值隐波", + "定价隐波", + "执行拟合" + ]) + table.horizontalHeader().setSectionResizeMode( + QtWidgets.QHeaderView.Stretch + ) + + for row, index in enumerate(chain.indexes): + index_cell = IndexCell(index) + mid_impv_cell = MonitorCell("") + + set_func = partial( + self.set_pricing_impv, + chain_symbol=chain_symbol, + index=index + ) + pricing_impv_spin = VolatilityDoubleSpinBox() + pricing_impv_spin.setAlignment(QtCore.Qt.AlignCenter) + pricing_impv_spin.valueChanged.connect(set_func) + + check = QtWidgets.QCheckBox() + + check_hbox = QtWidgets.QHBoxLayout() + check_hbox.setAlignment(QtCore.Qt.AlignCenter) + check_hbox.addWidget(check) + + check_widget = QtWidgets.QWidget() + check_widget.setLayout(check_hbox) + + table.setItem(row, 0, index_cell) + table.setItem(row, 1, mid_impv_cell) + table.setCellWidget(row, 2, pricing_impv_spin) + table.setCellWidget(row, 3, check_widget) + + cells = { + "mid_impv": mid_impv_cell, + "pricing_impv": pricing_impv_spin, + "check": check + } + + self.cells[(chain_symbol, index)] = cells + + reset_func = partial(self.reset_pricing_impv, chain_symbol=chain_symbol) + button_reset = QtWidgets.QPushButton("重置") + button_reset.clicked.connect(reset_func) + + fit_func = partial(self.fit_pricing_impv, chain_symbol=chain_symbol) + button_fit = QtWidgets.QPushButton("拟合") + button_fit.clicked.connect(fit_func) + + increase_func = partial(self.increase_pricing_impv, chain_symbol=chain_symbol) + button_increase = QtWidgets.QPushButton("+0.1%") + button_increase.clicked.connect(increase_func) + + decrease_func = partial(self.decrease_pricing_impv, chain_symbol=chain_symbol) + button_decrease = QtWidgets.QPushButton("-0.1%") + button_decrease.clicked.connect(decrease_func) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(button_reset) + hbox.addWidget(button_fit) + hbox.addWidget(button_increase) + hbox.addWidget(button_decrease) + + vbox = QtWidgets.QVBoxLayout() + vbox.addLayout(hbox) + vbox.addWidget(table) + + chain_widget = QtWidgets.QWidget() + chain_widget.setLayout(vbox) + tab.addTab(chain_widget, chain_symbol) + + self.update_pricing_impv(chain_symbol) + + self.default_foreground = mid_impv_cell.foreground() + self.default_background = mid_impv_cell.background() + + table.resizeRowsToContents() + + def register_event(self) -> None: + """""" + self.signal_timer.connect(self.process_timer_event) + + self.event_engine.register(EVENT_TIMER, self.signal_timer.emit) + + def process_timer_event(self, event: Event) -> None: + """""" + for chain_symbol in self.chain_symbols: + self.update_mid_impv(chain_symbol) + + def reset_pricing_impv(self, chain_symbol: str) -> None: + """ + Set pricing impv to the otm mid impv of each strike price. + """ + chain = self.portfolio.get_chain(chain_symbol) + atm_index = chain.atm_index + + for index in chain.indexes: + call = chain.calls[index] + put = chain.puts[index] + + if index >= atm_index: + otm = call + else: + otm = put + + call.pricing_impv = otm.mid_impv + put.pricing_impv = otm.mid_impv + + self.update_pricing_impv(chain_symbol) + + def fit_pricing_impv(self, chain_symbol: str) -> None: + """ + Fit pricing impv with cubic spline algo. + """ + chain = self.portfolio.get_chain(chain_symbol) + atm_index = chain.atm_index + + strike_prices = [] + pricing_impvs = [] + + for index in chain.indexes: + call = chain.calls[index] + put = chain.puts[index] + cells = self.cells[(chain_symbol, index)] + + if not cells["check"].isChecked(): + if index >= atm_index: + otm = call + else: + otm = put + + strike_prices.append(otm.strike_price) + pricing_impvs.append(otm.pricing_impv) + + cs = interpolate.CubicSpline(strike_prices, pricing_impvs) + + for index in chain.indexes: + call = chain.calls[index] + put = chain.puts[index] + + new_impv = float(cs(call.strike_price)) + call.pricing_impv = new_impv + put.pricing_impv = new_impv + + self.update_pricing_impv(chain_symbol) + + def increase_pricing_impv(self, chain_symbol: str) -> None: + """ + Increase pricing impv of all options within a chain by 0.1%. + """ + chain = self.portfolio.get_chain(chain_symbol) + + for option in chain.options.values(): + option.pricing_impv += 0.001 + + self.update_pricing_impv(chain_symbol) + + def decrease_pricing_impv(self, chain_symbol: str) -> None: + """ + Decrease pricing impv of all options within a chain by 0.1%. + """ + chain = self.portfolio.get_chain(chain_symbol) + + for option in chain.options.values(): + option.pricing_impv -= 0.001 + + self.update_pricing_impv(chain_symbol) + + def set_pricing_impv(self, value: float, chain_symbol: str, index: str) -> None: + """""" + new_impv = value / 100 + + chain = self.portfolio.get_chain(chain_symbol) + + call = chain.calls[index] + call.pricing_impv = new_impv + + put = chain.puts[index] + put.pricing_impv = new_impv + + def update_pricing_impv(self, chain_symbol: str) -> None: + """""" + chain = self.portfolio.get_chain(chain_symbol) + atm_index = chain.atm_index + + for index in chain.indexes: + if index >= atm_index: + otm = chain.calls[index] + else: + otm = chain.puts[index] + + value = round(otm.pricing_impv * 100, 1) + cells = self.cells[(chain_symbol, index)] + cells["pricing_impv"].setValue(value) + + def update_mid_impv(self, chain_symbol: str) -> None: + """""" + chain = self.portfolio.get_chain(chain_symbol) + atm_index = chain.atm_index + + for index in chain.indexes: + if index >= atm_index: + otm = chain.calls[index] + else: + otm = chain.puts[index] + + cells = self.cells[(chain_symbol, index)] + cells["mid_impv"].setText(f"{otm.mid_impv:.1%}") + + current_atm_index = self.chain_atm_index.get(chain_symbol, "") + if current_atm_index == atm_index: + return + self.chain_atm_index[chain_symbol] = atm_index + + if current_atm_index: + old_cells = self.cells[(chain_symbol, current_atm_index)] + old_cells["mid_impv"].setForeground(self.default_foreground) + old_cells["mid_impv"].setBackground(self.default_background) + + new_cells = self.cells[(chain_symbol, atm_index)] + new_cells["mid_impv"].setForeground(COLOR_BLACK) + new_cells["mid_impv"].setBackground(COLOR_WHITE) diff --git a/vnpy/app/option_master/ui/monitor.py b/vnpy/app/option_master/ui/monitor.py new file mode 100644 index 00000000..69c8153f --- /dev/null +++ b/vnpy/app/option_master/ui/monitor.py @@ -0,0 +1,597 @@ +from typing import List, Dict, Set, Union +from copy import copy +from collections import defaultdict + +from vnpy.event import Event +from vnpy.trader.ui import QtWidgets, QtCore, QtGui +from vnpy.trader.ui.widget import COLOR_BID, COLOR_ASK, COLOR_BLACK +from vnpy.trader.event import ( + EVENT_TICK, EVENT_TRADE, EVENT_POSITION, EVENT_TIMER +) +from vnpy.trader.utility import round_to +from ..engine import OptionEngine +from ..base import UnderlyingData, OptionData, ChainData, PortfolioData + + +COLOR_WHITE = QtGui.QColor("white") +COLOR_POS = QtGui.QColor("yellow") +COLOR_GREEKS = QtGui.QColor("cyan") + + +class MonitorCell(QtWidgets.QTableWidgetItem): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text) + + self.vt_symbol = vt_symbol + + self.setTextAlignment(QtCore.Qt.AlignCenter) + + +class IndexCell(MonitorCell): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text, vt_symbol) + + self.setForeground(COLOR_BLACK) + self.setBackground(COLOR_WHITE) + + +class BidCell(MonitorCell): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text, vt_symbol) + + self.setForeground(COLOR_BID) + + +class AskCell(MonitorCell): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text, vt_symbol) + + self.setForeground(COLOR_ASK) + + +class PosCell(MonitorCell): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text, vt_symbol) + + self.setForeground(COLOR_POS) + + +class GreeksCell(MonitorCell): + """""" + + def __init__(self, text: str = "", vt_symbol: str = ""): + """""" + super().__init__(text, vt_symbol) + + self.setForeground(COLOR_GREEKS) + + +class MonitorTable(QtWidgets.QTableWidget): + """""" + + def __init__(self): + """""" + super().__init__() + + self.init_menu() + + def init_menu(self) -> None: + """ + Create right click menu. + """ + self.menu = QtWidgets.QMenu(self) + + resize_action = QtWidgets.QAction("调整列宽", self) + resize_action.triggered.connect(self.resizeColumnsToContents) + self.menu.addAction(resize_action) + + def contextMenuEvent(self, event) -> None: + """ + Show menu with right click. + """ + self.menu.popup(QtGui.QCursor.pos()) + + +class OptionMarketMonitor(MonitorTable): + """""" + signal_tick = QtCore.pyqtSignal(Event) + signal_trade = QtCore.pyqtSignal(Event) + signal_position = QtCore.pyqtSignal(Event) + + headers: List[Dict] = [ + {"name": "symbol", "display": "代码", "cell": MonitorCell}, + {"name": "theo_vega", "display": "Vega", "cell": GreeksCell}, + {"name": "theo_theta", "display": "Theta", "cell": GreeksCell}, + {"name": "theo_gamma", "display": "Gamma", "cell": GreeksCell}, + {"name": "theo_delta", "display": "Delta", "cell": GreeksCell}, + {"name": "open_interest", "display": "持仓量", "cell": MonitorCell}, + {"name": "volume", "display": "成交量", "cell": MonitorCell}, + {"name": "bid_impv", "display": "买隐波", "cell": BidCell}, + {"name": "bid_volume", "display": "买量", "cell": BidCell}, + {"name": "bid_price", "display": "买价", "cell": BidCell}, + {"name": "ask_price", "display": "卖价", "cell": AskCell}, + {"name": "ask_volume", "display": "卖量", "cell": AskCell}, + {"name": "ask_impv", "display": "卖隐波", "cell": AskCell}, + {"name": "net_pos", "display": "净持仓", "cell": PosCell}, + ] + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.portfolio_name = portfolio_name + + self.cells: Dict[str, Dict] = {} + self.option_symbols: Set[str] = set() + self.underlying_option_map: Dict[str, List] = defaultdict(list) + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("T型报价") + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + # Store option and underlying symbols + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + for option in portfolio.options.values(): + self.option_symbols.add(option.vt_symbol) + self.underlying_option_map[option.underlying.vt_symbol].append(option.vt_symbol) + + # Set table row and column numbers + row_count = 0 + for chain in portfolio.chains.values(): + row_count += (1 + len(chain.indexes)) + self.setRowCount(row_count) + + column_count = len(self.headers) * 2 + 1 + self.setColumnCount(column_count) + + call_labels = [d["display"] for d in self.headers] + put_labels = copy(call_labels) + put_labels.reverse() + labels = call_labels + ["行权价"] + put_labels + self.setHorizontalHeaderLabels(labels) + + # Init cells + strike_column = len(self.headers) + current_row = 0 + + chain_symbols = list(portfolio.chains.keys()) + chain_symbols.sort() + + for chain_symbol in chain_symbols: + chain = portfolio.get_chain(chain_symbol) + + self.setItem( + current_row, + strike_column, + IndexCell(chain.chain_symbol.split(".")[0]) + ) + + for index in chain.indexes: + call = chain.calls[index] + put = chain.puts[index] + + current_row += 1 + + # Call cells + call_cells = {} + + for column, d in enumerate(self.headers): + value = getattr(call, d["name"], "") + cell = d["cell"]( + text=str(value), + vt_symbol=call.vt_symbol + ) + self.setItem(current_row, column, cell) + call_cells[d["name"]] = cell + + self.cells[call.vt_symbol] = call_cells + + # Put cells + put_cells = {} + put_headers = copy(self.headers) + put_headers.reverse() + + for column, d in enumerate(put_headers): + column += (strike_column + 1) + value = getattr(put, d["name"], "") + cell = d["cell"]( + text=str(value), + vt_symbol=put.vt_symbol + ) + self.setItem(current_row, column, cell) + put_cells[d["name"]] = cell + + self.cells[put.vt_symbol] = put_cells + + # Strike cell + index_cell = IndexCell(str(call.chain_index)) + self.setItem(current_row, strike_column, index_cell) + + # Move to next row + current_row += 1 + + def register_event(self) -> None: + """""" + self.signal_tick.connect(self.process_tick_event) + self.signal_trade.connect(self.process_trade_event) + self.signal_position.connect(self.process_position_event) + + self.event_engine.register(EVENT_TICK, self.signal_tick.emit) + self.event_engine.register(EVENT_TRADE, self.signal_trade.emit) + self.event_engine.register(EVENT_POSITION, self.signal_position.emit) + + def process_tick_event(self, event: Event) -> None: + """""" + tick = event.data + + if tick.vt_symbol in self.option_symbols: + self.update_price(tick.vt_symbol) + self.update_impv(tick.vt_symbol) + elif tick.vt_symbol in self.underlying_option_map: + option_symbols = self.underlying_option_map[tick.vt_symbol] + + for vt_symbol in option_symbols: + self.update_impv(vt_symbol) + self.update_greeks(vt_symbol) + + def process_trade_event(self, event: Event) -> None: + """""" + trade = event.data + self.update_pos(trade.vt_symbol) + + def process_position_event(self, event: Event) -> None: + """""" + position = event.data + self.update_pos(position.vt_symbol) + + def update_pos(self, vt_symbol: str) -> None: + """""" + option_cells = self.cells.get(vt_symbol, None) + if not option_cells: + return + + option = self.option_engine.get_instrument(vt_symbol) + + option_cells["net_pos"].setText(str(option.net_pos)) + + def update_price(self, vt_symbol: str) -> None: + """""" + option_cells = self.cells.get(vt_symbol, None) + if not option_cells: + return + + option = self.option_engine.get_instrument(vt_symbol) + tick = option.tick + option_cells["bid_price"].setText(f'{tick.bid_price_1:0.4f}') + option_cells["bid_volume"].setText(str(tick.bid_volume_1)) + option_cells["ask_price"].setText(f'{tick.ask_price_1:0.4f}') + option_cells["ask_volume"].setText(str(tick.ask_volume_1)) + option_cells["volume"].setText(str(tick.volume)) + option_cells["open_interest"].setText(str(tick.open_interest)) + + def update_impv(self, vt_symbol: str) -> None: + """""" + option_cells = self.cells.get(vt_symbol, None) + if not option_cells: + return + + option = self.option_engine.get_instrument(vt_symbol) + option_cells["bid_impv"].setText(f"{option.bid_impv * 100:.2f}") + option_cells["ask_impv"].setText(f"{option.ask_impv * 100:.2f}") + + def update_greeks(self, vt_symbol: str) -> None: + """""" + option_cells = self.cells.get(vt_symbol, None) + if not option_cells: + return + + option = self.option_engine.get_instrument(vt_symbol) + + option_cells["theo_delta"].setText(f"{option.theo_delta:.0f}") + option_cells["theo_gamma"].setText(f"{option.theo_gamma:.0f}") + option_cells["theo_theta"].setText(f"{option.theo_theta:.0f}") + option_cells["theo_vega"].setText(f"{option.theo_vega:.0f}") + + +class OptionGreeksMonitor(MonitorTable): + """""" + signal_tick = QtCore.pyqtSignal(Event) + signal_trade = QtCore.pyqtSignal(Event) + signal_position = QtCore.pyqtSignal(Event) + + headers: List[Dict] = [ + {"name": "long_pos", "display": "多仓", "cell": PosCell}, + {"name": "short_pos", "display": "空仓", "cell": PosCell}, + {"name": "net_pos", "display": "净仓", "cell": PosCell}, + {"name": "pos_delta", "display": "Delta", "cell": GreeksCell}, + {"name": "pos_gamma", "display": "Gamma", "cell": GreeksCell}, + {"name": "pos_theta", "display": "Theta", "cell": GreeksCell}, + {"name": "pos_vega", "display": "Vega", "cell": GreeksCell} + ] + + ROW_DATA = Union[OptionData, UnderlyingData, ChainData, PortfolioData] + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.portfolio_name = portfolio_name + + self.cells: Dict[str, Dict] = {} + self.option_symbols: Set[str] = set() + self.underlying_option_map: Dict[str, List] = defaultdict(list) + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("希腊值风险") + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + # Store option and underlying symbols + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + for option in portfolio.options.values(): + self.option_symbols.add(option.vt_symbol) + self.underlying_option_map[option.underlying.vt_symbol].append(option.vt_symbol) + + # Set table row and column numbers + row_count = 1 + for chain in portfolio.chains.values(): + row_count += (1 + len(chain.indexes) * 2) + self.setRowCount(row_count) + + column_count = len(self.headers) + 2 + self.setColumnCount(column_count) + + labels = ["类别", "代码"] + [d["display"] for d in self.headers] + self.setHorizontalHeaderLabels(labels) + + # Init cells + row_names = [self.portfolio_name] + row_names.append("") + + underlying_symbols = list(portfolio.underlyings.keys()) + underlying_symbols.sort() + row_names.extend(underlying_symbols) + row_names.append("") + + chain_symbols = list(portfolio.chains.keys()) + chain_symbols.sort() + row_names.extend(chain_symbols) + row_names.append("") + + option_symbols = list(portfolio.options.keys()) + option_symbols.sort() + row_names.extend(option_symbols) + + type_map = {} + type_map[self.portfolio_name] = "组合" + + for symbol in underlying_symbols: + type_map[symbol] = "标的" + + for symbol in chain_symbols: + type_map[symbol] = "期权链" + + for symbol in option_symbols: + type_map[symbol] = "期权" + + for row, row_name in enumerate(row_names): + if not row_name: + continue + + row_cells = {} + + type_cell = MonitorCell(type_map[row_name]) + self.setItem(row, 0, type_cell) + + name = row_name.split(".")[0] + name_cell = MonitorCell(name) + self.setItem(row, 1, name_cell) + + for column, d in enumerate(self.headers): + cell = d["cell"]() + self.setItem(row, column + 2, cell) + row_cells[d["name"]] = cell + self.cells[row_name] = row_cells + + if row_name != self.portfolio_name: + self.hideRow(row) + + self.resizeColumnToContents(0) + + def register_event(self) -> None: + """""" + self.signal_tick.connect(self.process_tick_event) + self.signal_trade.connect(self.process_trade_event) + self.signal_position.connect(self.process_position_event) + + self.event_engine.register(EVENT_TICK, self.signal_tick.emit) + self.event_engine.register(EVENT_TRADE, self.signal_trade.emit) + self.event_engine.register(EVENT_POSITION, self.signal_position.emit) + + def process_tick_event(self, event: Event) -> None: + """""" + tick = event.data + + if tick.vt_symbol not in self.underlying_option_map: + return + + self.update_underlying_tick(tick.vt_symbol) + + def process_trade_event(self, event: Event) -> None: + """""" + trade = event.data + if trade.vt_symbol not in self.cells: + return + + self.update_pos(trade.vt_symbol) + + def process_position_event(self, event: Event) -> None: + """""" + position = event.data + if position.vt_symbol not in self.cells: + return + + self.update_pos(position.vt_symbol) + + def update_underlying_tick(self, vt_symbol: str) -> None: + """""" + underlying = self.option_engine.get_instrument(vt_symbol) + self.update_row(vt_symbol, underlying) + + for chain in underlying.chains.values(): + self.update_row(chain.chain_symbol, chain) + + for option in chain.options.values(): + self.update_row(option.vt_symbol, option) + + portfolio = underlying.portfolio + self.update_row(portfolio.name, portfolio) + + def update_pos(self, vt_symbol: str) -> None: + """""" + instrument = self.option_engine.get_instrument(vt_symbol) + self.update_row(vt_symbol, instrument) + + # For option, greeks of chain also needs to be updated. + if isinstance(instrument, OptionData): + chain = instrument.chain + self.update_row(chain.chain_symbol, chain) + + portfolio = instrument.portfolio + self.update_row(portfolio.name, portfolio) + + def update_row(self, row_name: str, row_data: ROW_DATA) -> None: + """""" + row_cells = self.cells[row_name] + row = self.row(row_cells["long_pos"]) + + # Hide rows with no existing position + if not row_data.long_pos and not row_data.short_pos: + if row_name != self.portfolio_name: + self.hideRow(row) + return + + self.showRow(row) + + row_cells["long_pos"].setText(f"{row_data.long_pos}") + row_cells["short_pos"].setText(f"{row_data.short_pos}") + row_cells["net_pos"].setText(f"{row_data.net_pos}") + row_cells["pos_delta"].setText(f"{row_data.pos_delta:.0f}") + + if not isinstance(row_data, UnderlyingData): + row_cells["pos_gamma"].setText(f"{row_data.pos_gamma:.0f}") + row_cells["pos_theta"].setText(f"{row_data.pos_theta:.0f}") + row_cells["pos_vega"].setText(f"{row_data.pos_vega:.0f}") + + +class OptionChainMonitor(MonitorTable): + """""" + signal_timer = QtCore.pyqtSignal(Event) + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.event_engine = option_engine.event_engine + self.portfolio_name = portfolio_name + + self.cells: Dict[str, Dict] = {} + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("期权链跟踪") + self.verticalHeader().setVisible(False) + self.setEditTriggers(self.NoEditTriggers) + + # Store option and underlying symbols + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + # Set table row and column numbers + self.setRowCount(len(portfolio.chains)) + + labels = ["期权链", "剩余交易日", "标的物", "升贴水"] + self.setColumnCount(len(labels)) + self.setHorizontalHeaderLabels(labels) + + # Init cells + chain_symbols = list(portfolio.chains.keys()) + chain_symbols.sort() + + for row, chain_symbol in enumerate(chain_symbols): + chain = portfolio.chains[chain_symbol] + adjustment_cell = MonitorCell() + underlying_cell = MonitorCell() + + self.setItem(row, 0, MonitorCell(chain.chain_symbol.split(".")[0])) + self.setItem(row, 1, MonitorCell(str(chain.days_to_expiry))) + self.setItem(row, 2, underlying_cell) + self.setItem(row, 3, adjustment_cell) + + self.cells[chain.chain_symbol] = { + "underlying": underlying_cell, + "adjustment": adjustment_cell + } + + # Additional table adjustment + horizontal_header = self.horizontalHeader() + horizontal_header.setSectionResizeMode(horizontal_header.Stretch) + + def register_event(self) -> None: + """""" + self.signal_timer.connect(self.process_timer_event) + + self.event_engine.register(EVENT_TIMER, self.signal_timer.emit) + + def process_timer_event(self, event: Event) -> None: + """""" + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + for chain in portfolio.chains.values(): + underlying: UnderlyingData = chain.underlying + + underlying_symbol: str = underlying.vt_symbol.split(".")[0] + + if chain.underlying_adjustment == float("inf"): + continue + + adjustment = round_to( + chain.underlying_adjustment, underlying.pricetick + ) + + chain_cells = self.cells[chain.chain_symbol] + chain_cells["underlying"].setText(underlying_symbol) + chain_cells["adjustment"].setText(str(adjustment)) diff --git a/vnpy/app/option_master/ui/option.ico b/vnpy/app/option_master/ui/option.ico new file mode 100644 index 00000000..a30dd286 Binary files /dev/null and b/vnpy/app/option_master/ui/option.ico differ diff --git a/vnpy/app/option_master/ui/widget.py b/vnpy/app/option_master/ui/widget.py new file mode 100644 index 00000000..c8ad9c6d --- /dev/null +++ b/vnpy/app/option_master/ui/widget.py @@ -0,0 +1,705 @@ +from typing import Dict + +from vnpy.event import EventEngine, Event +from vnpy.trader.engine import MainEngine +from vnpy.trader.ui import QtWidgets, QtCore, QtGui +from vnpy.trader.constant import Direction, Offset, OrderType +from vnpy.trader.object import OrderRequest, ContractData, TickData +from vnpy.trader.event import EVENT_TICK + +from ..base import APP_NAME, EVENT_OPTION_NEW_PORTFOLIO +from ..engine import OptionEngine, PRICING_MODELS +from .monitor import ( + OptionMarketMonitor, OptionGreeksMonitor, OptionChainMonitor, + MonitorCell +) +from .chart import OptionVolatilityChart, ScenarioAnalysisChart +from .manager import ElectronicEyeManager, PricingVolatilityManager + + +class OptionManager(QtWidgets.QWidget): + """""" + signal_new_portfolio = QtCore.pyqtSignal(Event) + + def __init__(self, main_engine: MainEngine, event_engine: EventEngine): + """""" + super().__init__() + + self.main_engine = main_engine + self.event_engine = event_engine + self.option_engine = main_engine.get_engine(APP_NAME) + + self.portfolio_name: str = "" + + self.market_monitor: OptionMarketMonitor = None + self.greeks_monitor: OptionGreeksMonitor = None + self.volatility_chart: OptionVolatilityChart = None + self.chain_monitor: OptionChainMonitor = None + self.manual_trader: OptionManualTrader = None + self.hedge_widget: OptionHedgeWidget = None + self.scenario_chart: ScenarioAnalysisChart = None + self.eye_manager: ElectronicEyeManager = None + self.pricing_manager: PricingVolatilityManager = None + + self.init_ui() + self.register_event() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("OptionMaster") + + self.portfolio_combo = QtWidgets.QComboBox() + self.portfolio_combo.setFixedWidth(150) + self.update_portfolio_combo() + + self.portfolio_button = QtWidgets.QPushButton("配置") + self.portfolio_button.clicked.connect(self.open_portfolio_dialog) + + self.market_button = QtWidgets.QPushButton("T型报价") + self.greeks_button = QtWidgets.QPushButton("持仓希腊值") + self.chain_button = QtWidgets.QPushButton("升贴水监控") + self.manual_button = QtWidgets.QPushButton("快速交易") + self.volatility_button = QtWidgets.QPushButton("波动率曲线") + self.hedge_button = QtWidgets.QPushButton("Delta对冲") + self.scenario_button = QtWidgets.QPushButton("情景分析") + self.eye_button = QtWidgets.QPushButton("电子眼") + self.pricing_button = QtWidgets.QPushButton("波动率管理") + + for button in [ + self.market_button, + self.greeks_button, + self.chain_button, + self.manual_button, + self.volatility_button, + self.hedge_button, + self.scenario_button, + self.eye_button, + self.pricing_button + ]: + button.setEnabled(False) + + hbox = QtWidgets.QHBoxLayout() + hbox.addWidget(QtWidgets.QLabel("期权产品")) + hbox.addWidget(self.portfolio_combo) + hbox.addWidget(self.portfolio_button) + hbox.addWidget(self.market_button) + hbox.addWidget(self.greeks_button) + hbox.addWidget(self.manual_button) + hbox.addWidget(self.chain_button) + hbox.addWidget(self.volatility_button) + hbox.addWidget(self.hedge_button) + hbox.addWidget(self.scenario_button) + hbox.addWidget(self.pricing_button) + hbox.addWidget(self.eye_button) + + self.setLayout(hbox) + + def register_event(self) -> None: + """""" + self.signal_new_portfolio.connect(self.process_new_portfolio_event) + + self.event_engine.register(EVENT_OPTION_NEW_PORTFOLIO, self.signal_new_portfolio.emit) + + def process_new_portfolio_event(self, event: Event) -> None: + """""" + self.update_portfolio_combo() + + def update_portfolio_combo(self) -> None: + """""" + if not self.portfolio_combo.isEnabled(): + return + + self.portfolio_combo.clear() + portfolio_names = self.option_engine.get_portfolio_names() + self.portfolio_combo.addItems(portfolio_names) + + def open_portfolio_dialog(self) -> None: + """""" + portfolio_name = self.portfolio_combo.currentText() + if not portfolio_name: + return + + self.portfolio_name = portfolio_name + + dialog = PortfolioDialog(self.option_engine, portfolio_name) + result = dialog.exec_() + + if result == dialog.Accepted: + self.portfolio_combo.setEnabled(False) + self.portfolio_button.setEnabled(False) + + self.init_widgets() + + def init_widgets(self) -> None: + """""" + self.market_monitor = OptionMarketMonitor(self.option_engine, self.portfolio_name) + self.greeks_monitor = OptionGreeksMonitor(self.option_engine, self.portfolio_name) + self.volatility_chart = OptionVolatilityChart(self.option_engine, self.portfolio_name) + self.chain_monitor = OptionChainMonitor(self.option_engine, self.portfolio_name) + self.manual_trader = OptionManualTrader(self.option_engine, self.portfolio_name) + self.hedge_widget = OptionHedgeWidget(self.option_engine, self.portfolio_name) + self.scenario_chart = ScenarioAnalysisChart(self.option_engine, self.portfolio_name) + self.eye_manager = ElectronicEyeManager(self.option_engine, self.portfolio_name) + self.pricing_manager = PricingVolatilityManager(self.option_engine, self.portfolio_name) + + self.market_monitor.itemDoubleClicked.connect(self.manual_trader.update_symbol) + + self.market_button.clicked.connect(self.market_monitor.show) + self.greeks_button.clicked.connect(self.greeks_monitor.show) + self.manual_button.clicked.connect(self.manual_trader.show) + self.chain_button.clicked.connect(self.chain_monitor.show) + self.volatility_button.clicked.connect(self.volatility_chart.show) + self.scenario_button.clicked.connect(self.scenario_chart.show) + self.hedge_button.clicked.connect(self.hedge_widget.show) + self.eye_button.clicked.connect(self.eye_manager.show) + self.pricing_button.clicked.connect(self.pricing_manager.show) + + for button in [ + self.market_button, + self.greeks_button, + self.chain_button, + self.manual_button, + self.volatility_button, + self.scenario_button, + self.hedge_button, + self.eye_button, + self.pricing_button + ]: + button.setEnabled(True) + + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + """""" + if self.portfolio_name: + self.market_monitor.close() + self.greeks_monitor.close() + self.volatility_chart.close() + self.chain_monitor.close() + self.manual_trader.close() + self.hedge_widget.close() + self.scenario_chart.close() + self.eye_manager.close() + self.pricing_manager.close() + + event.accept() + + +class PortfolioDialog(QtWidgets.QDialog): + """""" + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.portfolio_name = portfolio_name + + self.init_ui() + + def init_ui(self) -> None: + """""" + self.setWindowTitle(f"{self.portfolio_name}组合配置") + + portfolio_setting = self.option_engine.get_portfolio_setting( + self.portfolio_name + ) + + form = QtWidgets.QFormLayout() + + # Model Combo + self.model_name_combo = QtWidgets.QComboBox() + self.model_name_combo.addItems(list(PRICING_MODELS.keys())) + + model_name = portfolio_setting.get("model_name", "") + if model_name: + self.model_name_combo.setCurrentIndex( + self.model_name_combo.findText(model_name) + ) + + form.addRow("模型", self.model_name_combo) + + # Interest rate spin + self.interest_rate_spin = QtWidgets.QDoubleSpinBox() + self.interest_rate_spin.setMinimum(0) + self.interest_rate_spin.setMaximum(20) + self.interest_rate_spin.setDecimals(1) + self.interest_rate_spin.setSuffix("%") + + interest_rate = portfolio_setting.get("interest_rate", 0.02) + self.interest_rate_spin.setValue(interest_rate * 100) + + form.addRow("利率", self.interest_rate_spin) + + # Underlying for each chain + self.combos: Dict[str, QtWidgets.QComboBox] = {} + + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + underlying_symbols = self.option_engine.get_underlying_symbols( + self.portfolio_name + ) + + chain_symbols = list(portfolio._chains.keys()) + chain_symbols.sort() + + chain_underlying_map = portfolio_setting.get("chain_underlying_map", {}) + + for chain_symbol in chain_symbols: + combo = QtWidgets.QComboBox() + combo.addItem("") + combo.addItems(underlying_symbols) + + underlying_symbol = chain_underlying_map.get(chain_symbol, "") + if underlying_symbol: + combo.setCurrentIndex(combo.findText(underlying_symbol)) + + form.addRow(chain_symbol, combo) + self.combos[chain_symbol] = combo + + # Set layout + button = QtWidgets.QPushButton("确定") + button.clicked.connect(self.update_portfolio_setting) + form.addRow(button) + + self.setLayout(form) + + def update_portfolio_setting(self) -> None: + """""" + model_name = self.model_name_combo.currentText() + interest_rate = self.interest_rate_spin.value() / 100 + + chain_underlying_map = {} + for chain_symbol, combo in self.combos.items(): + underlying_symbol = combo.currentText() + + if underlying_symbol: + chain_underlying_map[chain_symbol] = underlying_symbol + + self.option_engine.update_portfolio_setting( + self.portfolio_name, + model_name, + interest_rate, + chain_underlying_map + ) + + result = self.option_engine.init_portfolio(self.portfolio_name) + + if result: + self.accept() + else: + self.close() + + +class OptionManualTrader(QtWidgets.QWidget): + """""" + signal_tick = QtCore.pyqtSignal(TickData) + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.main_engine: MainEngine = option_engine.main_engine + self.event_engine: EventEngine = option_engine.event_engine + + self.contracts: Dict[str, ContractData] = {} + self.vt_symbol = "" + + self.init_ui() + self.init_contracts() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("期权交易") + + # Trading Area + self.symbol_line = QtWidgets.QLineEdit() + self.symbol_line.returnPressed.connect(self._update_symbol) + + float_validator = QtGui.QDoubleValidator() + float_validator.setBottom(0) + + self.price_line = QtWidgets.QLineEdit() + self.price_line.setValidator(float_validator) + + int_validator = QtGui.QIntValidator() + int_validator.setBottom(0) + + self.volume_line = QtWidgets.QLineEdit() + self.volume_line.setValidator(int_validator) + + 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.OPEN.value, + Offset.CLOSE.value + ]) + + order_button = QtWidgets.QPushButton("委托") + order_button.clicked.connect(self.send_order) + + cancel_button = QtWidgets.QPushButton("全撤") + cancel_button.clicked.connect(self.cancel_all) + + form1 = QtWidgets.QFormLayout() + form1.addRow("代码", self.symbol_line) + form1.addRow("方向", self.direction_combo) + form1.addRow("开平", self.offset_combo) + form1.addRow("价格", self.price_line) + form1.addRow("数量", self.volume_line) + form1.addRow(order_button) + form1.addRow(cancel_button) + + # Depth Area + bid_color = "rgb(255,174,201)" + ask_color = "rgb(160,255,160)" + + self.bp1_label = self.create_label(bid_color) + self.bp2_label = self.create_label(bid_color) + self.bp3_label = self.create_label(bid_color) + self.bp4_label = self.create_label(bid_color) + self.bp5_label = self.create_label(bid_color) + + self.bv1_label = self.create_label( + bid_color, alignment=QtCore.Qt.AlignRight) + self.bv2_label = self.create_label( + bid_color, alignment=QtCore.Qt.AlignRight) + self.bv3_label = self.create_label( + bid_color, alignment=QtCore.Qt.AlignRight) + self.bv4_label = self.create_label( + bid_color, alignment=QtCore.Qt.AlignRight) + self.bv5_label = self.create_label( + bid_color, alignment=QtCore.Qt.AlignRight) + + self.ap1_label = self.create_label(ask_color) + self.ap2_label = self.create_label(ask_color) + self.ap3_label = self.create_label(ask_color) + self.ap4_label = self.create_label(ask_color) + self.ap5_label = self.create_label(ask_color) + + self.av1_label = self.create_label( + ask_color, alignment=QtCore.Qt.AlignRight) + self.av2_label = self.create_label( + ask_color, alignment=QtCore.Qt.AlignRight) + self.av3_label = self.create_label( + ask_color, alignment=QtCore.Qt.AlignRight) + self.av4_label = self.create_label( + ask_color, alignment=QtCore.Qt.AlignRight) + self.av5_label = self.create_label( + ask_color, alignment=QtCore.Qt.AlignRight) + + self.lp_label = self.create_label() + self.return_label = self.create_label(alignment=QtCore.Qt.AlignRight) + + min_width = 70 + self.lp_label.setMinimumWidth(min_width) + self.return_label.setMinimumWidth(min_width) + + form2 = QtWidgets.QFormLayout() + form2.addRow(self.ap5_label, self.av5_label) + form2.addRow(self.ap4_label, self.av4_label) + form2.addRow(self.ap3_label, self.av3_label) + form2.addRow(self.ap2_label, self.av2_label) + form2.addRow(self.ap1_label, self.av1_label) + form2.addRow(self.lp_label, self.return_label) + form2.addRow(self.bp1_label, self.bv1_label) + form2.addRow(self.bp2_label, self.bv2_label) + form2.addRow(self.bp3_label, self.bv3_label) + form2.addRow(self.bp4_label, self.bv4_label) + form2.addRow(self.bp5_label, self.bv5_label) + + # Set layout + hbox = QtWidgets.QHBoxLayout() + hbox.addLayout(form1) + hbox.addLayout(form2) + self.setLayout(hbox) + + def init_contracts(self) -> None: + """""" + contracts = self.main_engine.get_all_contracts() + for contract in contracts: + self.contracts[contract.symbol] = contract + + def connect_signal(self) -> None: + """""" + self.signal_tick.connect(self.update_tick) + + def send_order(self) -> None: + """""" + symbol = self.symbol_line.text() + contract = self.contracts.get(symbol, None) + if not contract: + return + + 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 = int(volume_text) + direction = Direction(self.direction_combo.currentText()) + offset = Offset(self.offset_combo.currentText()) + + req = OrderRequest( + symbol=contract.symbol, + exchange=contract.exchange, + direction=direction, + type=OrderType.LIMIT, + offset=offset, + volume=volume, + price=price + ) + self.main_engine.send_order(req, contract.gateway_name) + + def cancel_all(self) -> None: + """""" + for order in self.main_engine.get_all_active_orders(): + req = order.create_cancel_request() + self.main_engine.cancel_order(req, order.gateway_name) + + def update_symbol(self, cell: MonitorCell) -> None: + """""" + if not cell.vt_symbol: + return + + symbol = cell.vt_symbol.split(".")[0] + self.symbol_line.setText(symbol) + self._update_symbol() + + def _update_symbol(self) -> None: + """""" + symbol = self.symbol_line.text() + contract = self.contracts.get(symbol, None) + + if contract and contract.vt_symbol == self.vt_symbol: + return + + if self.vt_symbol: + self.event_engine.unregister(EVENT_TICK + self.vt_symbol, self.process_tick_event) + self.clear_data() + self.vt_symbol = "" + + if not contract: + return + + vt_symbol = contract.vt_symbol + self.vt_symbol = vt_symbol + + tick = self.main_engine.get_tick(vt_symbol) + if tick: + self.update_tick(tick) + + self.event_engine.unregister(EVENT_TICK + vt_symbol, self.process_tick_event) + + def create_label( + self, + color: str = "", + alignment: int = QtCore.Qt.AlignLeft + ) -> QtWidgets.QLabel: + """ + Create label with certain font color. + """ + label = QtWidgets.QLabel("-") + if color: + label.setStyleSheet(f"color:{color}") + label.setAlignment(alignment) + return label + + def process_tick_event(self, event: Event) -> None: + """""" + tick = event.data + + if tick.vt_symbol != self.vt_symbol: + return + + self.signal_tick.emit(tick) + + def update_tick(self, tick: TickData) -> None: + """""" + self.lp_label.setText(str(tick.last_price)) + self.bp1_label.setText(str(tick.bid_price_1)) + self.bv1_label.setText(str(tick.bid_volume_1)) + self.ap1_label.setText(str(tick.ask_price_1)) + self.av1_label.setText(str(tick.ask_volume_1)) + + if tick.pre_close: + r = (tick.last_price / tick.pre_close - 1) * 100 + self.return_label.setText(f"{r:.2f}%") + + if tick.bid_price_2: + self.bp2_label.setText(str(tick.bid_price_2)) + self.bv2_label.setText(str(tick.bid_volume_2)) + self.ap2_label.setText(str(tick.ask_price_2)) + self.av2_label.setText(str(tick.ask_volume_2)) + + self.bp3_label.setText(str(tick.bid_price_3)) + self.bv3_label.setText(str(tick.bid_volume_3)) + self.ap3_label.setText(str(tick.ask_price_3)) + self.av3_label.setText(str(tick.ask_volume_3)) + + self.bp4_label.setText(str(tick.bid_price_4)) + self.bv4_label.setText(str(tick.bid_volume_4)) + self.ap4_label.setText(str(tick.ask_price_4)) + self.av4_label.setText(str(tick.ask_volume_4)) + + self.bp5_label.setText(str(tick.bid_price_5)) + self.bv5_label.setText(str(tick.bid_volume_5)) + self.ap5_label.setText(str(tick.ask_price_5)) + self.av5_label.setText(str(tick.ask_volume_5)) + + def clear_data(self) -> None: + """""" + self.lp_label.setText("-") + self.return_label.setText("-") + self.bp1_label.setText("-") + self.bv1_label.setText("-") + self.ap1_label.setText("-") + self.av1_label.setText("-") + + self.bp2_label.setText("-") + self.bv2_label.setText("-") + self.ap2_label.setText("-") + self.av2_label.setText("-") + + self.bp3_label.setText("-") + self.bv3_label.setText("-") + self.ap3_label.setText("-") + self.av3_label.setText("-") + + self.bp4_label.setText("-") + self.bv4_label.setText("-") + self.ap4_label.setText("-") + self.av4_label.setText("-") + + self.bp5_label.setText("-") + self.bv5_label.setText("-") + self.ap5_label.setText("-") + self.av5_label.setText("-") + + +class OptionHedgeWidget(QtWidgets.QWidget): + """""" + + def __init__(self, option_engine: OptionEngine, portfolio_name: str): + """""" + super().__init__() + + self.option_engine = option_engine + self.portfolio_name = portfolio_name + self.hedge_engine = option_engine.hedge_engine + + self.symbol_map: Dict[str, str] = {} + + self.init_ui() + + def init_ui(self) -> None: + """""" + self.setWindowTitle("Delta对冲") + + underlying_symbols = [] + portfolio = self.option_engine.get_portfolio(self.portfolio_name) + + for chain in portfolio.chains.values(): + underlying_symbol = chain.underlying.symbol + self.symbol_map[underlying_symbol] = chain.underlying.vt_symbol + + if underlying_symbol not in underlying_symbols: + underlying_symbols.append(underlying_symbol) + + underlying_symbols.sort() + + self.symbol_combo = QtWidgets.QComboBox() + self.symbol_combo.addItems(underlying_symbols) + + self.trigger_spin = QtWidgets.QSpinBox() + self.trigger_spin.setSuffix("秒") + self.trigger_spin.setMinimum(1) + self.trigger_spin.setValue(5) + + self.target_spin = QtWidgets.QSpinBox() + self.target_spin.setMaximum(99999999) + self.target_spin.setMinimum(-99999999) + self.target_spin.setValue(0) + + self.range_spin = QtWidgets.QSpinBox() + self.range_spin.setMinimum(0) + self.range_spin.setMaximum(9999999) + self.range_spin.setValue(12000) + + self.payup_spin = QtWidgets.QSpinBox() + self.payup_spin.setMinimum(0) + self.payup_spin.setValue(3) + + self.start_button = QtWidgets.QPushButton("启动") + self.start_button.clicked.connect(self.start) + + self.stop_button = QtWidgets.QPushButton("停止") + self.stop_button.clicked.connect(self.stop) + self.stop_button.setEnabled(False) + + form = QtWidgets.QFormLayout() + form.addRow("对冲合约", self.symbol_combo) + form.addRow("执行频率", self.trigger_spin) + form.addRow("Delta目标", self.target_spin) + form.addRow("对冲阈值", self.range_spin) + form.addRow("委托超价", self.payup_spin) + form.addRow(self.start_button) + form.addRow(self.stop_button) + + self.setLayout(form) + + def start(self) -> None: + """""" + symbol = self.symbol_combo.currentText() + vt_symbol = self.symbol_map[symbol] + timer_trigger = self.trigger_spin.value() + delta_target = self.target_spin.value() + delta_range = self.range_spin.value() + hedge_payup = self.payup_spin.value() + + # Check delta of underlying + underlying = self.option_engine.get_instrument(vt_symbol) + min_range = int(underlying.theo_delta * 0.6) + if delta_range < min_range: + msg = f"Delta对冲阈值({delta_range})低于对冲合约"\ + f"Delta值的60%({min_range}),可能导致来回频繁对冲!" + + QtWidgets.QMessageBox.warning( + self, + "无法启动自动对冲", + msg, + QtWidgets.QMessageBox.Ok + ) + return + + self.hedge_engine.start( + self.portfolio_name, + vt_symbol, + timer_trigger, + delta_target, + delta_range, + hedge_payup + ) + + self.update_widget_status(False) + + def stop(self) -> None: + """""" + self.hedge_engine.stop() + + self.update_widget_status(True) + + def update_widget_status(self, status: bool) -> None: + """""" + self.start_button.setEnabled(status) + self.symbol_combo.setEnabled(status) + self.target_spin.setEnabled(status) + self.range_spin.setEnabled(status) + self.payup_spin.setEnabled(status) + self.trigger_spin.setEnabled(status) + self.stop_button.setEnabled(not status) diff --git a/vnpy/component/cta_line_bar.py b/vnpy/component/cta_line_bar.py index 86e28d3b..31550c96 100644 --- a/vnpy/component/cta_line_bar.py +++ b/vnpy/component/cta_line_bar.py @@ -2014,15 +2014,15 @@ class CtaLineBar(object): upper = round(upper_list[-1], self.round_n) self.line_boll_upper.append(upper) # 上轨 - self.cur_upper = upper - upper % self.price_tick # 上轨取整 + self.cur_upper = upper # 上轨 middle = round(middle_list[-1], self.round_n) self.line_boll_middle.append(middle) # 中轨 - self.cur_middle = middle - middle % self.price_tick # 中轨取整 + self.cur_middle = middle # 中轨 lower = round(lower_list[-1], self.round_n) self.line_boll_lower.append(lower) # 下轨 - self.cur_lower = lower - lower % self.price_tick # 下轨取整 + self.cur_lower = lower # 下轨 # 计算斜率 if len(self.line_boll_upper) > 2 and self.line_boll_upper[-2] != 0: @@ -2072,15 +2072,15 @@ class CtaLineBar(object): upper = round(upper_list[-1], self.round_n) self.line_boll2_upper.append(upper) # 上轨 - self.cur_upper2 = upper - upper % self.price_tick # 上轨取整 + self.cur_upper2 = upper # 上轨 middle = round(middle_list[-1], self.round_n) self.line_boll2_middle.append(middle) # 中轨 - self.cur_middle2 = middle - middle % self.price_tick # 中轨取整 + self.cur_middle2 = middle # 中轨 lower = round(lower_list[-1], self.round_n) self.line_boll2_lower.append(lower) # 下轨 - self.cur_lower2 = lower - lower % self.price_tick # 下轨取整 + self.cur_lower2 = lower # 下轨 # 计算斜率 if len(self.line_boll2_upper) > 2 and self.line_boll2_upper[-2] != 0: