[bug fix]
This commit is contained in:
parent
152fc1d83b
commit
8e28264c71
18
Q_n_A.md
18
Q_n_A.md
@ -35,7 +35,13 @@
|
|||||||
|
|
||||||
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
|
||||||
conda config --set show_channel_urls yes
|
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
|
若出现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
|
../configure --prefix=/usr --disable-profile --enable-add-ons --with-headers=/usr/include --with-binutils=/usr/bin
|
||||||
make –j4
|
make –j4
|
||||||
make install
|
make install
|
||||||
|
|
||||||
|
12. pip 增加国内源
|
||||||
|
|
||||||
|
|
||||||
|
创建/home/trade/.pip目录
|
||||||
|
创建pip.conf文件,内容:
|
||||||
|
[global]
|
||||||
|
index-url=http://pypi.douban.com/simple
|
||||||
|
[install]
|
||||||
|
trusted-host=pypi.douban.com
|
||||||
|
@ -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
|
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
|
tar xvf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib
|
cd ta-lib
|
||||||
./autogen.sh
|
./configure --prefix=/usr
|
||||||
./configure
|
|
||||||
make
|
make
|
||||||
|
|
||||||
下面指令用用root权限运行,会把编译结果放在/usr/local/lib下
|
下面指令用用root权限运行,会把编译结果放在/usr/local/lib下
|
||||||
@ -19,26 +18,28 @@ conda create -name py37=python3.7
|
|||||||
|
|
||||||
pip install ta-lib
|
pip install ta-lib
|
||||||
|
|
||||||
错误:
|
错误:
|
||||||
|
|
||||||
ImportError: libta_lib.so.0: cannot open shared object file: No such file or directory
|
ImportError: libta_lib.so.0: cannot open shared object file: No such file or directory
|
||||||
|
|
||||||
解决:
|
解决:
|
||||||
|
sudo find / -name libta_lib.so.0
|
||||||
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
|
||||||
/home/ai/eco-ta/ta-lib/src/.libs/libta_lib.so.0
|
vi /etc/profile
|
||||||
|
添加
|
||||||
/usr/local/lib/libta_lib.so.0
|
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
|
||||||
|
|
||||||
vi /etc/profile
|
|
||||||
|
|
||||||
添加
|
|
||||||
|
|
||||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib
|
|
||||||
|
|
||||||
source /etc/profile
|
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接口
|
单独编译ctp接口
|
||||||
|
|
||||||
一般直接使用提供的vnctptd.so 和 vnctpmd.so 就可以了。
|
一般直接使用提供的vnctptd.so 和 vnctpmd.so 就可以了。
|
||||||
|
@ -21,6 +21,7 @@ function install-ta-lib()
|
|||||||
make install
|
make install
|
||||||
popd
|
popd
|
||||||
}
|
}
|
||||||
|
|
||||||
function ta-lib-exists()
|
function ta-lib-exists()
|
||||||
{
|
{
|
||||||
ta-lib-config --libs > /dev/null
|
ta-lib-config --libs > /dev/null
|
||||||
|
@ -7,7 +7,7 @@ websocket-client
|
|||||||
peewee
|
peewee
|
||||||
mongoengine
|
mongoengine
|
||||||
numpy
|
numpy
|
||||||
pandas>=0.24.2
|
pandas
|
||||||
matplotlib
|
matplotlib
|
||||||
seaborn
|
seaborn
|
||||||
futu-api
|
futu-api
|
||||||
|
1
run_celery.sh
Normal file
1
run_celery.sh
Normal 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 &
|
@ -9,6 +9,7 @@ from .template import (
|
|||||||
Direction,
|
Direction,
|
||||||
Offset,
|
Offset,
|
||||||
Status,
|
Status,
|
||||||
|
Color,
|
||||||
TickData,
|
TickData,
|
||||||
BarData,
|
BarData,
|
||||||
TradeData,
|
TradeData,
|
||||||
|
@ -11,7 +11,7 @@ from copy import copy
|
|||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
from logging import INFO, ERROR
|
from logging import INFO, ERROR
|
||||||
from datetime import datetime
|
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.object import BarData, TickData, OrderData, TradeData
|
||||||
from vnpy.trader.utility import virtual, append_data, extract_vt_symbol, get_underlying_symbol
|
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))
|
self.write_log(u'{} 订单信息:{}'.format(order.vt_orderid, old_order))
|
||||||
old_order['traded'] = order.traded
|
old_order['traded'] = order.traded
|
||||||
# order_time = old_order['order_time']
|
# 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']
|
order_volume = old_order['volume'] - old_order['traded']
|
||||||
if order_volume <= 0:
|
if order_volume <= 0:
|
||||||
msg = u'{} {}{}重新平仓数量为{},不再平仓' \
|
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.write_error(msg)
|
||||||
self.send_wechat(msg)
|
self.send_wechat(msg)
|
||||||
self.write_log(u'活动订单移除:{}'.format(order.vt_orderid))
|
self.write_log(u'活动订单移除:{}'.format(order.vt_orderid))
|
||||||
@ -1434,11 +1434,11 @@ class CtaProFutureTemplate(CtaProTemplate):
|
|||||||
|
|
||||||
order_price = old_order['price']
|
order_price = old_order['price']
|
||||||
order_type = old_order.get('order_type', OrderType.LIMIT)
|
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)
|
grid = old_order.get('grid', None)
|
||||||
if order_retry > 20:
|
if order_retry > 20:
|
||||||
msg = u'{} 平仓撤单 {}/{}手, 重试平仓次数{}>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.write_error(msg)
|
||||||
self.send_wechat(msg)
|
self.send_wechat(msg)
|
||||||
if grid:
|
if grid:
|
||||||
@ -1458,25 +1458,25 @@ class CtaProFutureTemplate(CtaProTemplate):
|
|||||||
if old_order['direction'] == Direction.LONG and order_type == OrderType.FAK:
|
if old_order['direction'] == Direction.LONG and order_type == OrderType.FAK:
|
||||||
self.write_log(u'FAK模式,需要重新发送cover委托.grid:{}'.format(grid.__dict__))
|
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
|
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:
|
if cover_tick.limit_up > 0 and cover_price > cover_tick.limit_up:
|
||||||
cover_price = cover_tick.limit_up
|
cover_price = cover_tick.limit_up
|
||||||
|
|
||||||
if self.is_upper_limit(order_symbol):
|
if self.is_upper_limit(order_vt_symbol):
|
||||||
self.write_log(u'{}涨停,不做cover'.format(order_symbol))
|
self.write_log(u'{}涨停,不做cover'.format(order_vt_symbol))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 发送委托
|
# 发送委托
|
||||||
vt_orderids = self.cover(price=cover_price,
|
vt_orderids = self.cover(price=cover_price,
|
||||||
volume=order_volume,
|
volume=order_volume,
|
||||||
vt_symbol=order_symbol,
|
vt_symbol=order_vt_symbol,
|
||||||
order_type=OrderType.FAK,
|
order_type=OrderType.FAK,
|
||||||
order_time=self.cur_datetime,
|
order_time=self.cur_datetime,
|
||||||
grid=grid)
|
grid=grid)
|
||||||
if not vt_orderids:
|
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
|
return
|
||||||
|
|
||||||
for vt_orderid in vt_orderids:
|
for vt_orderid in vt_orderids:
|
||||||
@ -1485,31 +1485,31 @@ class CtaProFutureTemplate(CtaProTemplate):
|
|||||||
|
|
||||||
self.gt.save()
|
self.gt.save()
|
||||||
self.write_log(u'移除活动订单:{}'.format(order.vt_orderid))
|
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:
|
elif old_order['direction'] == Direction.SHORT and order_type == OrderType.FAK:
|
||||||
self.write_log(u'FAK模式,需要重新发送sell委托.grid:{}'.format(grid.__dict__))
|
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
|
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:
|
if sell_tick.limit_down > 0 and sell_price < sell_tick.limit_down:
|
||||||
sell_price = sell_tick.limit_down
|
sell_price = sell_tick.limit_down
|
||||||
|
|
||||||
if self.is_lower_limit(order_symbol):
|
if self.is_lower_limit(order_vt_symbol):
|
||||||
self.write_log(u'{}涨停,不做sell'.format(order_symbol))
|
self.write_log(u'{}涨停,不做sell'.format(order_vt_symbol))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 发送委托
|
# 发送委托
|
||||||
vt_orderids = self.sell(price=sell_price,
|
vt_orderids = self.sell(price=sell_price,
|
||||||
volume=order_volume,
|
volume=order_volume,
|
||||||
vt_symbol=order_symbol,
|
vt_symbol=order_vt_symbol,
|
||||||
order_type=OrderType.FAK,
|
order_type=OrderType.FAK,
|
||||||
order_time=self.cur_datetime,
|
order_time=self.cur_datetime,
|
||||||
grid=grid)
|
grid=grid)
|
||||||
|
|
||||||
if not vt_orderids:
|
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
|
return
|
||||||
|
|
||||||
for vt_orderid in vt_orderids:
|
for vt_orderid in vt_orderids:
|
||||||
@ -1568,11 +1568,11 @@ class CtaProFutureTemplate(CtaProTemplate):
|
|||||||
over_seconds = (dt - order_time).total_seconds()
|
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: # 超过设置的时间还未成交
|
if over_seconds > self.cancel_seconds or force: # 超过设置的时间还未成交
|
||||||
self.write_log(u'超时{}秒未成交,取消委托单:vt_orderid:{},order:{}'
|
self.write_log(u'超时{}秒未成交,取消委托单:vt_orderid:{},order:{}'
|
||||||
.format(over_seconds, vt_orderid, order_info))
|
.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})
|
self.active_orders.update({vt_orderid: order_info})
|
||||||
ret = self.cancel_order(str(vt_orderid))
|
ret = self.cancel_order(str(vt_orderid))
|
||||||
if not ret:
|
if not ret:
|
||||||
|
16
vnpy/app/data_manager/__init__.py
Normal file
16
vnpy/app/data_manager/__init__.py
Normal 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"
|
166
vnpy/app/data_manager/engine.py
Normal file
166
vnpy/app/data_manager/engine.py
Normal 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
|
1
vnpy/app/data_manager/ui/__init__.py
Normal file
1
vnpy/app/data_manager/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .widget import ManagerWidget
|
BIN
vnpy/app/data_manager/ui/manager.ico
Normal file
BIN
vnpy/app/data_manager/ui/manager.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
422
vnpy/app/data_manager/ui/widget.py
Normal file
422
vnpy/app/data_manager/ui/widget.py
Normal 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)
|
14
vnpy/app/excel_rtd/__init__.py
Normal file
14
vnpy/app/excel_rtd/__init__.py
Normal 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"
|
80
vnpy/app/excel_rtd/engine.py
Normal file
80
vnpy/app/excel_rtd/engine.py
Normal 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()
|
1
vnpy/app/excel_rtd/ui/__init__.py
Normal file
1
vnpy/app/excel_rtd/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .widget import RtdManager
|
BIN
vnpy/app/excel_rtd/ui/rtd.ico
Normal file
BIN
vnpy/app/excel_rtd/ui/rtd.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
79
vnpy/app/excel_rtd/ui/widget.py
Normal file
79
vnpy/app/excel_rtd/ui/widget.py
Normal 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()
|
114
vnpy/app/excel_rtd/vnpy_rtd.py
Normal file
114
vnpy/app/excel_rtd/vnpy_rtd.py
Normal 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
|
14
vnpy/app/option_master/__init__.py
Normal file
14
vnpy/app/option_master/__init__.py
Normal 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"
|
342
vnpy/app/option_master/algo.py
Normal file
342
vnpy/app/option_master/algo.py
Normal 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)
|
594
vnpy/app/option_master/base.py
Normal file
594
vnpy/app/option_master/base.py
Normal 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()
|
683
vnpy/app/option_master/engine.py
Normal file
683
vnpy/app/option_master/engine.py
Normal 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)
|
0
vnpy/app/option_master/pricing/__init__.py
Normal file
0
vnpy/app/option_master/pricing/__init__.py
Normal file
251
vnpy/app/option_master/pricing/binomial_tree.py
Normal file
251
vnpy/app/option_master/pricing/binomial_tree.py
Normal 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
|
BIN
vnpy/app/option_master/pricing/binomial_tree_cython.pyd
Normal file
BIN
vnpy/app/option_master/pricing/binomial_tree_cython.pyd
Normal file
Binary file not shown.
217
vnpy/app/option_master/pricing/black_76.py
Normal file
217
vnpy/app/option_master/pricing/black_76.py
Normal 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
|
BIN
vnpy/app/option_master/pricing/black_76_cython.pyd
Normal file
BIN
vnpy/app/option_master/pricing/black_76_cython.pyd
Normal file
Binary file not shown.
216
vnpy/app/option_master/pricing/black_scholes.py
Normal file
216
vnpy/app/option_master/pricing/black_scholes.py
Normal 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
|
BIN
vnpy/app/option_master/pricing/black_scholes_cython.pyd
Normal file
BIN
vnpy/app/option_master/pricing/black_scholes_cython.pyd
Normal file
Binary file not shown.
@ -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
|
@ -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()]
|
||||||
|
)
|
@ -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
|
@ -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"),
|
||||||
|
)
|
@ -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
|
@ -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"),
|
||||||
|
)
|
61
vnpy/app/option_master/time.py
Normal file
61
vnpy/app/option_master/time.py
Normal 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
|
1
vnpy/app/option_master/ui/__init__.py
Normal file
1
vnpy/app/option_master/ui/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .widget import OptionManager
|
408
vnpy/app/option_master/ui/chart.py
Normal file
408
vnpy/app/option_master/ui/chart.py
Normal 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
|
||||||
|
)
|
891
vnpy/app/option_master/ui/manager.py
Normal file
891
vnpy/app/option_master/ui/manager.py
Normal 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)
|
597
vnpy/app/option_master/ui/monitor.py
Normal file
597
vnpy/app/option_master/ui/monitor.py
Normal 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))
|
BIN
vnpy/app/option_master/ui/option.ico
Normal file
BIN
vnpy/app/option_master/ui/option.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
705
vnpy/app/option_master/ui/widget.py
Normal file
705
vnpy/app/option_master/ui/widget.py
Normal 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)
|
@ -2014,15 +2014,15 @@ class CtaLineBar(object):
|
|||||||
|
|
||||||
upper = round(upper_list[-1], self.round_n)
|
upper = round(upper_list[-1], self.round_n)
|
||||||
self.line_boll_upper.append(upper) # 上轨
|
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)
|
middle = round(middle_list[-1], self.round_n)
|
||||||
self.line_boll_middle.append(middle) # 中轨
|
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)
|
lower = round(lower_list[-1], self.round_n)
|
||||||
self.line_boll_lower.append(lower) # 下轨
|
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:
|
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)
|
upper = round(upper_list[-1], self.round_n)
|
||||||
self.line_boll2_upper.append(upper) # 上轨
|
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)
|
middle = round(middle_list[-1], self.round_n)
|
||||||
self.line_boll2_middle.append(middle) # 中轨
|
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)
|
lower = round(lower_list[-1], self.round_n)
|
||||||
self.line_boll2_lower.append(lower) # 下轨
|
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:
|
if len(self.line_boll2_upper) > 2 and self.line_boll2_upper[-2] != 0:
|
||||||
|
Loading…
Reference in New Issue
Block a user