From 8e28264c71bdf96c2db06f886a3fd30a91fea294 Mon Sep 17 00:00:00 2001 From: msincenselee Date: Tue, 24 Mar 2020 13:27:04 +0800 Subject: [PATCH] [bug fix] --- Q_n_A.md | 18 +- centos_setup.md | 41 +- install.sh | 1 + requirements.txt | 2 +- run_celery.sh | 1 + vnpy/app/cta_strategy_pro/__init__.py | 1 + vnpy/app/cta_strategy_pro/template.py | 36 +- vnpy/app/data_manager/__init__.py | 16 + vnpy/app/data_manager/engine.py | 166 ++++ vnpy/app/data_manager/ui/__init__.py | 1 + vnpy/app/data_manager/ui/manager.ico | Bin 0 -> 63038 bytes vnpy/app/data_manager/ui/widget.py | 422 +++++++++ vnpy/app/excel_rtd/__init__.py | 14 + vnpy/app/excel_rtd/engine.py | 80 ++ vnpy/app/excel_rtd/ui/__init__.py | 1 + vnpy/app/excel_rtd/ui/rtd.ico | Bin 0 -> 67646 bytes vnpy/app/excel_rtd/ui/widget.py | 79 ++ vnpy/app/excel_rtd/vnpy_rtd.py | 114 +++ vnpy/app/option_master/__init__.py | 14 + vnpy/app/option_master/algo.py | 342 +++++++ vnpy/app/option_master/base.py | 594 ++++++++++++ vnpy/app/option_master/engine.py | 683 ++++++++++++++ vnpy/app/option_master/pricing/__init__.py | 0 .../option_master/pricing/binomial_tree.py | 251 +++++ .../pricing/binomial_tree_cython.pyd | Bin 0 -> 91136 bytes vnpy/app/option_master/pricing/black_76.py | 217 +++++ .../option_master/pricing/black_76_cython.pyd | Bin 0 -> 71680 bytes .../option_master/pricing/black_scholes.py | 216 +++++ .../pricing/black_scholes_cython.pyd | Bin 0 -> 71168 bytes .../binomial_tree_cython.pyx | 269 ++++++ .../binomial_tree_cython/setup.py | 9 + .../black_76_cython/black_76_cython.pyx | 239 +++++ .../cython_model/black_76_cython/setup.py | 7 + .../black_scholes_cython.pyx | 238 +++++ .../black_scholes_cython/setup.py | 7 + vnpy/app/option_master/time.py | 61 ++ vnpy/app/option_master/ui/__init__.py | 1 + vnpy/app/option_master/ui/chart.py | 408 ++++++++ vnpy/app/option_master/ui/manager.py | 891 ++++++++++++++++++ vnpy/app/option_master/ui/monitor.py | 597 ++++++++++++ vnpy/app/option_master/ui/option.ico | Bin 0 -> 67646 bytes vnpy/app/option_master/ui/widget.py | 705 ++++++++++++++ vnpy/component/cta_line_bar.py | 12 +- 43 files changed, 6708 insertions(+), 46 deletions(-) create mode 100644 run_celery.sh create mode 100644 vnpy/app/data_manager/__init__.py create mode 100644 vnpy/app/data_manager/engine.py create mode 100644 vnpy/app/data_manager/ui/__init__.py create mode 100644 vnpy/app/data_manager/ui/manager.ico create mode 100644 vnpy/app/data_manager/ui/widget.py create mode 100644 vnpy/app/excel_rtd/__init__.py create mode 100644 vnpy/app/excel_rtd/engine.py create mode 100644 vnpy/app/excel_rtd/ui/__init__.py create mode 100644 vnpy/app/excel_rtd/ui/rtd.ico create mode 100644 vnpy/app/excel_rtd/ui/widget.py create mode 100644 vnpy/app/excel_rtd/vnpy_rtd.py create mode 100644 vnpy/app/option_master/__init__.py create mode 100644 vnpy/app/option_master/algo.py create mode 100644 vnpy/app/option_master/base.py create mode 100644 vnpy/app/option_master/engine.py create mode 100644 vnpy/app/option_master/pricing/__init__.py create mode 100644 vnpy/app/option_master/pricing/binomial_tree.py create mode 100644 vnpy/app/option_master/pricing/binomial_tree_cython.pyd create mode 100644 vnpy/app/option_master/pricing/black_76.py create mode 100644 vnpy/app/option_master/pricing/black_76_cython.pyd create mode 100644 vnpy/app/option_master/pricing/black_scholes.py create mode 100644 vnpy/app/option_master/pricing/black_scholes_cython.pyd create mode 100644 vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx create mode 100644 vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/setup.py create mode 100644 vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx create mode 100644 vnpy/app/option_master/pricing/cython_model/black_76_cython/setup.py create mode 100644 vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx create mode 100644 vnpy/app/option_master/pricing/cython_model/black_scholes_cython/setup.py create mode 100644 vnpy/app/option_master/time.py create mode 100644 vnpy/app/option_master/ui/__init__.py create mode 100644 vnpy/app/option_master/ui/chart.py create mode 100644 vnpy/app/option_master/ui/manager.py create mode 100644 vnpy/app/option_master/ui/monitor.py create mode 100644 vnpy/app/option_master/ui/option.ico create mode 100644 vnpy/app/option_master/ui/widget.py 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 0000000000000000000000000000000000000000..6ad2845ebfc2361c42252d719f68d9772499843b GIT binary patch literal 63038 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f6c9cbfd3fa8a80307a235c91220a673f263ea00 GIT binary patch literal 67646 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 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 0000000000000000000000000000000000000000..dc3682f70826aa65e02f174815e79f39037c9e0f GIT binary patch literal 91136 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 LucLMU_x}DLUiN{S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..85b0aad7ff536ada0464588941747f6d59432839 GIT binary patch literal 71680 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 z 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%0A-T@Q)agLlVOF;)aydx$+*Kw4E^U{)g)UO!lJZ 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 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 0000000000000000000000000000000000000000..a636dbb1785a1b047e49205ab5b09fa407c81d99 GIT binary patch literal 71168 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

cnI-R^j)Qa{`m|CF?rM3Hj9VMxkNU4psr9Kr6 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 NiVoNg{H>0${{b`0yC?tv literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a30dd286494a70042b49cb33bb265ad126ff736e GIT binary patch literal 67646 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 literal 0 HcmV?d00001 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: