[bug fix]

This commit is contained in:
msincenselee 2020-03-24 13:27:04 +08:00
parent 152fc1d83b
commit 8e28264c71
43 changed files with 6708 additions and 46 deletions

View File

@ -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

View File

@ -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 就可以了。

View File

@ -21,6 +21,7 @@ function install-ta-lib()
make install
popd
}
function ta-lib-exists()
{
ta-lib-config --libs > /dev/null

View File

@ -7,7 +7,7 @@ websocket-client
peewee
mongoengine
numpy
pandas>=0.24.2
pandas
matplotlib
seaborn
futu-api

1
run_celery.sh Normal file
View File

@ -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 &

View File

@ -9,6 +9,7 @@ from .template import (
Direction,
Offset,
Status,
Color,
TickData,
BarData,
TradeData,

View File

@ -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:

View File

@ -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"

View File

@ -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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@ -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)

View File

@ -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"

View File

@ -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()

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,79 @@
from pathlib import Path
from vnpy.event import EventEngine, Event
from vnpy.trader.engine import MainEngine
from vnpy.trader.ui import QtWidgets, QtCore
from vnpy.trader.object import LogData
from ..engine import APP_NAME, EVENT_RTD_LOG
class RtdManager(QtWidgets.QWidget):
""""""
signal_log = QtCore.pyqtSignal(Event)
def __init__(self, main_engine: MainEngine, event_engine: EventEngine):
""""""
super().__init__()
self.main_engine = main_engine
self.event_engine = event_engine
self.rm_engine = main_engine.get_engine(APP_NAME)
self.init_ui()
self.register_event()
def init_ui(self) -> None:
"""
Init widget ui components.
"""
self.setWindowTitle("Excel RTD")
self.resize(600, 600)
module_path = Path(__file__).parent.parent
client_path = module_path.joinpath("vnpy_rtd.py")
self.client_line = QtWidgets.QLineEdit(str(client_path))
self.client_line.setReadOnly(True)
copy_button = QtWidgets.QPushButton("复制")
copy_button.clicked.connect(self.copy_client_path)
self.log_monitor = QtWidgets.QTextEdit()
self.log_monitor.setReadOnly(True)
self.port_label = QtWidgets.QLabel(
"使用Socket端口请求回应9001、广播推送9002"
)
hbox = QtWidgets.QHBoxLayout()
hbox.addWidget(self.client_line)
hbox.addWidget(copy_button)
vbox = QtWidgets.QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.log_monitor)
vbox.addWidget(self.port_label)
self.setLayout(vbox)
def register_event(self) -> None:
"""
Register event handler.
"""
self.signal_log.connect(self.process_log_event)
self.event_engine.register(EVENT_RTD_LOG, self.signal_log.emit)
def process_log_event(self, event: Event) -> None:
"""
Show log message in monitor.
"""
log: LogData = event.data
msg = f"{log.time}: {log.msg}"
self.log_monitor.append(msg)
def copy_client_path(self) -> None:
"""
Copy path of client python file to clipboard.
"""
self.client_line.selectAll()
self.client_line.copy()

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -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

Binary file not shown.

View File

@ -0,0 +1,216 @@
from scipy import stats
from math import log, pow, sqrt, exp
from typing import Tuple
cdf = stats.norm.cdf
pdf = stats.norm.pdf
def calculate_d1(
s: float,
k: float,
r: float,
t: float,
v: float
) -> float:
"""Calculate option D1 value"""
d1: float = (log(s / k) + (r + 0.5 * pow(v, 2)) * t) / (v * sqrt(t))
return d1
def calculate_price(
s: float,
k: float,
r: float,
t: float,
v: float,
cp: int,
d1: float = 0.0
) -> float:
"""Calculate option price"""
# Return option space value if volatility not positive
if v <= 0:
return max(0, cp * (s - k))
if not d1:
d1: float = calculate_d1(s, k, r, t, v)
d2: float = d1 - v * sqrt(t)
price: float = cp * (s * cdf(cp * d1) - k * cdf(cp * d2) * exp(-r * t))
return price
def calculate_delta(
s: float,
k: float,
r: float,
t: float,
v: float,
cp: int,
d1: float = 0.0
) -> float:
"""Calculate option delta"""
if v <= 0:
return 0
if not d1:
d1: float = calculate_d1(s, k, r, t, v)
_delta: float = cp * cdf(cp * d1)
delta: float = _delta * s * 0.01
return delta
def calculate_gamma(
s: float,
k: float,
r: float,
t: float,
v: float,
d1: float = 0.0
) -> float:
"""Calculate option gamma"""
if v <= 0:
return 0
if not d1:
d1: float = calculate_d1(s, k, r, t, v)
_gamma: float = pdf(d1) / (s * v * sqrt(t))
gamma: float = _gamma * pow(s, 2) * 0.0001
return gamma
def calculate_theta(
s: float,
k: float,
r: float,
t: float,
v: float,
cp: int,
d1: float = 0.0,
annual_days: int = 240
) -> float:
"""Calculate option theta"""
if v <= 0:
return 0
if not d1:
d1: float = calculate_d1(s, k, r, t, v)
d2: float = d1 - v * sqrt(t)
_theta = -s * pdf(d1) * v / (2 * sqrt(t)) \
- cp * r * k * exp(-r * t) * cdf(cp * d2)
theta = _theta / annual_days
return theta
def calculate_vega(
s: float,
k: float,
r: float,
t: float,
v: float,
d1: float = 0.0
) -> float:
"""Calculate option vega(%)"""
vega: float = calculate_original_vega(s, k, r, t, v, d1) / 100
return vega
def calculate_original_vega(
s: float,
k: float,
r: float,
t: float,
v: float,
d1: float = 0.0
) -> float:
"""Calculate option vega"""
if v <= 0:
return 0
if not d1:
d1: float = calculate_d1(s, k, r, t, v)
vega: float = s * pdf(d1) * sqrt(t)
return vega
def calculate_greeks(
s: float,
k: float,
r: float,
t: float,
v: float,
cp: int,
annual_days: int = 240
) -> Tuple[float, float, float, float, float]:
"""Calculate option price and greeks"""
d1: float = calculate_d1(s, k, r, t, v)
price: float = calculate_price(s, k, r, t, v, cp, d1)
delta: float = calculate_delta(s, k, r, t, v, cp, d1)
gamma: float = calculate_gamma(s, k, r, t, v, d1)
theta: float = calculate_theta(s, k, r, t, v, cp, d1, annual_days)
vega: float = calculate_vega(s, k, r, t, v, d1)
return price, delta, gamma, theta, vega
def calculate_impv(
price: float,
s: float,
k: float,
r: float,
t: float,
cp: int
):
"""Calculate option implied volatility"""
# Check option price must be positive
if price <= 0:
return 0
# Check if option price meets minimum value (exercise value)
meet: bool = False
if cp == 1 and (price > (s - k) * exp(-r * t)):
meet = True
elif cp == -1 and (price > k * exp(-r * t) - s):
meet = True
# If minimum value not met, return 0
if not meet:
return 0
# Calculate implied volatility with Newton's method
v: float = 0.3 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess
p: float = calculate_price(s, k, r, t, v, cp)
vega: float = calculate_original_vega(s, k, r, t, v, cp)
# Break loop if vega too close to 0
if not vega:
break
# Calculate error value
dx: float = (price - p) / vega
# Check if error value meets requirement
if abs(dx) < 0.00001:
break
# Calculate guessed implied volatility of next round
v += dx
# Check end result to be non-negative
if v <= 0:
return 0
# Round to 4 decimal places
v = round(v, 4)
return v

View File

@ -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

View File

@ -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()]
)

View File

@ -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

View File

@ -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"),
)

View File

@ -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

View File

@ -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"),
)

View File

@ -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

View File

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

View File

@ -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
)

View File

@ -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)

View File

@ -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))

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -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)

View File

@ -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: