[update] 期权api,gateway, vnpy 2.15 sync

This commit is contained in:
msincenselee 2020-08-16 19:51:36 +08:00
parent 4d15d355c6
commit 12a44cdfe2
25 changed files with 792 additions and 150 deletions

View File

@ -8678,12 +8678,12 @@ int TdApi::exit()
void TdApi::subscribePrivateTopic(int x)
{
this->api->SubscribePrivateTopic(THOST_TERT_RESTART);
this->api->SubscribePrivateTopic((THOST_TE_RESUME_TYPE) x);
}
void TdApi::subscribePublicTopic(int x)
{
this->api->SubscribePublicTopic(THOST_TERT_RESTART);
this->api->SubscribePublicTopic((THOST_TE_RESUME_TYPE) x);
}
string TdApi::getTradingDay()

Binary file not shown.

View File

@ -1,6 +1,7 @@
import os
import sys
import traceback
from datetime import datetime
from functools import lru_cache
from vnpy.event import EventEngine, Event
@ -101,40 +102,55 @@ class AlgoEngine(BaseEngine):
def process_tick_event(self, event: Event):
""""""
tick = event.data
try:
algos = self.symbol_algo_map.get(tick.vt_symbol, None)
if algos:
for algo in algos:
algo.update_tick(tick)
except Exception as ex:
self.write_error(f'algo ontick exception:{str(ex)}')
self.write_error(traceback.format_exc())
def process_timer_event(self, event: Event):
""""""
# Generate a list of algos first to avoid dict size change
try:
algos = list(self.algos.values())
for algo in algos:
algo.update_timer()
except Exception as ex:
self.write_error(f'algo ontimer exception:{str(ex)}')
self.write_error(traceback.format_exc())
def process_trade_event(self, event: Event):
""""""
try:
trade = event.data
self.offset_converter.update_trade(trade)
algo = self.orderid_algo_map.get(trade.vt_orderid, None)
if algo:
algo.update_trade(trade)
except Exception as ex:
self.write_error(f'algo ontrade exception:{str(ex)}')
self.write_error(traceback.format_exc())
def process_order_event(self, event: Event):
""""""
try:
order = event.data
self.offset_converter.update_order(order)
algo = self.orderid_algo_map.get(order.vt_orderid, None)
if algo:
algo.update_order(order)
except Exception as ex:
self.write_error(f'algo onorder exception:{str(ex)}')
self.write_error(traceback.format_exc())
def process_position_event(self, event: Event):
""""""
position = event.data
self.offset_converter.update_position(position)
def start_algo(self, setting: dict):
@ -156,6 +172,8 @@ class AlgoEngine(BaseEngine):
self.algos.pop(algo_name)
return True
return False
def stop_all(self):
""""""
for algo_name in list(self.algos.keys()):

View File

@ -103,6 +103,7 @@ class AlgoTemplate:
self.put_variables_event()
self.write_log("停止算法")
return True
def subscribe(self, vt_symbol):
""""""

View File

@ -787,6 +787,7 @@ class BackTestingEngine(object):
"""保存策略数据"""
for strategy in self.strategies.values():
self.write_log(u'save strategy data')
if hasattr(strategy,'save_data'):
strategy.save_data()
def send_order(self,

View File

@ -1922,8 +1922,8 @@ class CtaProFutureTemplate(CtaProTemplate):
grid.traded_volume = 0
# 非股指,需要检查是否有持仓
if self.exchange==Exchange.CFFEX and grid_pos.short_pos < grid.volume:
self.write_error(f'账号{cover_symbol}单持仓:{grid_pos.short_pos}不满足平仓:{grid.volume}要求:')
if self.exchange!=Exchange.CFFEX and grid_pos.short_pos < grid.volume:
self.write_error(f'账号{cover_symbol}单持仓:{grid_pos.short_pos}不满足平仓:{grid.volume}要求:')
return False
vt_orderids = self.cover(price=cover_price,

View File

@ -268,7 +268,7 @@ class ElectronicEyeAlgo:
# Calculate spread
algo_spread = max(
self.price_spread,
self.volatility_spread * option.theo_vega
self.volatility_spread * option.cash_vega
)
half_spread = algo_spread / 2

View File

@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timedelta
from typing import Dict, List, Callable
from types import ModuleType
@ -19,19 +19,33 @@ EVENT_OPTION_ALGO_LOG = "eOptionAlgoLog"
CHAIN_UNDERLYING_MAP = {
# ETF Options
"510050_O.SSE": "510050",
"510300_O.SSE": "510300",
"159919_O.SSE": "159919",
"159919_O.SZSE": "159919",
# Futures Options
"IO.CFFEX": "IF",
"HO.CFFEX": "IH",
"i_o.DCE": "i",
"pg_o.DCE": "pg",
"m_o.DCE": "m",
"c_o.DCE": "c",
"cu_o.SHFE": "cu",
"ru_o.SHFE": "ru",
"au_o.SHFE": "au",
"SR.CZCE": "SR",
"CF.CZCE": "CF",
"TA.CZCE": "TA",
"MA.CZCE": "MA",
"RM.CZCE": "RM",
# Crypto Options
"BTC.DERIBIT": "BTC-PERPETUAL",
"BTC-USD.OKEX": "BTC-USD-SWAP"
}
@ -114,6 +128,7 @@ class OptionData(InstrumentData):
self.time_to_expiry: float = self.days_to_expiry / ANNUAL_DAYS
self.interest_rate: float = 0
self.inverse: bool = False
# Option portfolio related
self.underlying: UnderlyingData = None
@ -132,10 +147,10 @@ class OptionData(InstrumentData):
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.cash_delta: float = 0
self.cash_gamma: float = 0
self.cash_theta: float = 0
self.cash_vega: float = 0
self.pos_value: float = 0
self.pos_delta: float = 0
@ -145,7 +160,7 @@ class OptionData(InstrumentData):
def calculate_option_impv(self) -> None:
""""""
if not self.tick:
if not self.tick or not self.underlying:
return
underlying_price = self.underlying.mid_price
@ -153,8 +168,16 @@ class OptionData(InstrumentData):
return
underlying_price += self.underlying_adjustment
# Adjustment for crypto inverse option contract
if self.inverse:
ask_price = self.tick.ask_price_1 * underlying_price
bid_price = self.tick.bid_price_1 * underlying_price
else:
ask_price = self.tick.ask_price_1
bid_price = self.tick.bid_price_1
self.ask_impv = self.calculate_impv(
self.tick.ask_price_1,
ask_price,
underlying_price,
self.strike_price,
self.interest_rate,
@ -163,7 +186,7 @@ class OptionData(InstrumentData):
)
self.bid_impv = self.calculate_impv(
self.tick.bid_price_1,
bid_price,
underlying_price,
self.strike_price,
self.interest_rate,
@ -173,7 +196,7 @@ class OptionData(InstrumentData):
self.mid_impv = (self.ask_impv + self.bid_impv) / 2
def calculate_theo_greeks(self) -> None:
def calculate_cash_greeks(self) -> None:
""""""
if not self.underlying:
return
@ -192,20 +215,27 @@ class OptionData(InstrumentData):
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
self.cash_delta = delta * self.size
self.cash_gamma = gamma * self.size
self.cash_theta = theta * self.size
self.cash_vega = vega * self.size
# Adjustment for crypto inverse option contract
if self.inverse:
self.cash_delta /= underlying_price
self.cash_gamma /= underlying_price
self.cash_theta /= underlying_price
self.cash_vega /= underlying_price
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
self.pos_delta = self.cash_delta * self.net_pos
self.pos_gamma = self.cash_gamma * self.net_pos
self.pos_theta = self.cash_theta * self.net_pos
self.pos_vega = self.cash_vega * self.net_pos
def calculate_ref_price(self) -> float:
""""""
@ -221,11 +251,21 @@ class OptionData(InstrumentData):
self.option_type
)
# Adjustment for crypto inverse option contract
if self.inverse:
ref_price /= underlying_price
return ref_price
def update_tick(self, tick: TickData) -> None:
""""""
super().update_tick(tick)
if self.inverse:
current_dt = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
self.days_to_expiry = self.option_expiry - current_dt
self.time_to_expiry = self.days_to_expiry / timedelta(365)
self.calculate_option_impv()
def update_trade(self, trade: TradeData) -> None:
@ -238,7 +278,7 @@ class OptionData(InstrumentData):
self.underlying_adjustment = underlying_adjustment
self.calculate_option_impv()
self.calculate_theo_greeks()
self.calculate_cash_greeks()
self.calculate_pos_greeks()
def set_chain(self, chain: "ChainData") -> None:
@ -253,6 +293,10 @@ class OptionData(InstrumentData):
""""""
self.interest_rate = interest_rate
def set_inverse(self, inverse: bool) -> None:
""""""
self.inverse = inverse
def set_pricing_model(self, pricing_model: ModuleType) -> None:
""""""
self.calculate_greeks = pricing_model.calculate_greeks
@ -267,7 +311,7 @@ class UnderlyingData(InstrumentData):
""""""
super().__init__(contract)
self.theo_delta: float = 0
self.cash_delta: float = 0
self.pos_delta: float = 0
self.chains: Dict[str: ChainData] = {}
@ -279,7 +323,7 @@ class UnderlyingData(InstrumentData):
""""""
super().update_tick(tick)
self.theo_delta = self.size * self.mid_price / 100
self.cash_delta = self.size * self.mid_price / 100
for chain in self.chains.values():
chain.update_underlying_tick()
@ -293,7 +337,7 @@ class UnderlyingData(InstrumentData):
def calculate_pos_greeks(self) -> None:
""""""
self.pos_delta = self.theo_delta * self.net_pos
self.pos_delta = self.cash_delta * self.net_pos
class ChainData:
@ -326,6 +370,7 @@ class ChainData:
self.atm_index: str = ""
self.underlying_adjustment: float = 0
self.days_to_expiry: int = 0
self.inverse: bool = False
def add_option(self, option: OptionData) -> None:
""""""
@ -340,6 +385,12 @@ class ChainData:
if option.chain_index not in self.indexes:
self.indexes.append(option.chain_index)
# Sort index by number if possible, otherwise by string
try:
float(option.chain_index)
self.indexes.sort(key=float)
except ValueError:
self.indexes.sort()
self.days_to_expiry = option.days_to_expiry
@ -428,6 +479,13 @@ class ChainData:
for option in self.options.values():
option.set_pricing_model(pricing_model)
def set_inverse(self, inverse: bool) -> None:
""""""
self.inverse = inverse
for option in self.options.values():
option.set_inverse(inverse)
def set_portfolio(self, portfolio: "PortfolioData") -> None:
""""""
for option in self.options:
@ -460,7 +518,15 @@ class ChainData:
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
# Adjustment for crypto inverse option contract
if self.inverse:
call_price = atm_call.mid_price * self.underlying.mid_price
put_price = atm_put.mid_price * self.underlying.mid_price
else:
call_price = atm_call.mid_price
put_price = atm_put.mid_price
synthetic_price = call_price - put_price + self.atm_price
self.underlying_adjustment = synthetic_price - self.underlying.mid_price
@ -488,6 +554,9 @@ class PortfolioData:
self.chains: Dict[str, ChainData] = {}
self.underlyings: Dict[str, UnderlyingData] = {}
# Greeks decimals precision
self.precision: int = 0
def calculate_pos_greeks(self) -> None:
""""""
self.long_pos = 0
@ -548,6 +617,15 @@ class PortfolioData:
for chain in self.chains.values():
chain.set_pricing_model(pricing_model)
def set_inverse(self, inverse: bool) -> None:
""""""
for chain in self.chains.values():
chain.set_inverse(inverse)
def set_precision(self, precision: int) -> None:
""""""
self.precision = precision
def set_chain_underlying(self, chain_symbol: str, contract: ContractData) -> None:
""""""
underlying = self.underlyings.get(contract.vt_symbol, None)

View File

@ -244,6 +244,8 @@ class OptionEngine(BaseEngine):
model_name: str,
interest_rate: float,
chain_underlying_map: Dict[str, str],
inverse: bool = False,
precision: int = 0
) -> None:
""""""
portfolio = self.get_portfolio(portfolio_name)
@ -256,12 +258,16 @@ class OptionEngine(BaseEngine):
pricing_model = PRICING_MODELS[model_name]
portfolio.set_pricing_model(pricing_model)
portfolio.set_inverse(inverse)
portfolio.set_precision(precision)
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
"chain_underlying_map": chain_underlying_map,
"inverse": inverse,
"precision": precision
}
self.save_setting()
@ -434,13 +440,17 @@ class OptionHedgeEngine:
# 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
hedge_volume = delta_to_hedge / instrument.cash_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)
# Check if hedge volume meets contract minimum trading volume
if abs(hedge_volume) < contract.min_volume:
return
if hedge_volume > 0:
price = tick.ask_price_1 + contract.pricetick * self.hedge_payup
direction = Direction.LONG

View File

@ -77,9 +77,13 @@ def calculate_delta(
) -> 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
option_price_change: float = option_tree[0, 1] - option_tree[1, 1]
underlying_price_change: float = underlying_tree[0, 1] - underlying_tree[1, 1]
_delta: float = option_price_change / underlying_price_change
delta: float = _delta * f * 0.01
return delta
def calculate_gamma(
@ -94,12 +98,14 @@ def calculate_gamma(
"""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]) / \
gamma_delta_1: float = (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]) / \
gamma_delta_2: float = (option_tree[1, 2] - option_tree[2, 2]) / \
(underlying_tree[1, 2] - underlying_tree[2, 2])
gamma = (gamma_delta_1 - gamma_delta_2) / \
_gamma: float = (gamma_delta_1 - gamma_delta_2) / \
(0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2]))
gamma: float = _gamma * pow(f, 2) * 0.0001
return gamma
@ -174,15 +180,17 @@ def calculate_greeks(
# 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
_delta: float = option_price_change / underlying_price_change
delta: float = _delta * f * 0.01
# 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) / \
_gamma: float = (gamma_delta_1 - gamma_delta_2) / \
(0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2]))
gamma: float = _gamma * pow(f, 2) * 0.0001
# Theta
theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days)
@ -220,7 +228,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v = 0.3 # Initial guess of volatility
v: float = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess
@ -241,7 +249,7 @@ def calculate_impv(
# Calculate guessed implied volatility of next round
v += dx
# Check end result to be non-negative
# Check new volatility to be non-negative
if v <= 0:
return 0

View File

@ -33,7 +33,7 @@ def calculate_price(
return max(0, cp * (s - k))
if not d1:
d1: float = calculate_d1(s, k, r, r, v)
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)
@ -186,7 +186,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v: float = 0.3 # Initial guess of volatility
v: float = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess

View File

@ -185,7 +185,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v: float = 0.3 # Initial guess of volatility
v: float = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess

View File

@ -88,11 +88,17 @@ def calculate_delta(
int n = DEFAULT_STEP
) -> float:
"""Calculate option delta"""
cdef double option_price_change, underlying_price_change
cdef _delta, 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
_delta = option_price_change / underlying_price_change
delta = _delta * f * 0.01
return delta
def calculate_gamma(
double f,
@ -104,14 +110,19 @@ def calculate_gamma(
int n = DEFAULT_STEP
) -> float:
"""Calculate option gamma"""
cdef double gamma_delta_1, gamma_delta_2
cdef double _gamma, 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) / \
_gamma = (gamma_delta_1 - gamma_delta_2) / \
(0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2]))
gamma = _gamma * pow(f, 2) * 0.0001
return gamma
@ -127,6 +138,8 @@ def calculate_theta(
int annual_days = 240
) -> float:
"""Calcualte option theta"""
cdef double dt, theta
option_tree, underlying_tree = generate_tree(f, k, r, t, v, cp, n)
dt = t / n
@ -177,9 +190,10 @@ def calculate_greeks(
) -> 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
cdef double price, delta, gamma, vega, theta
cdef double _delta, _gamma
cdef double option_price_change, underlying_price_change
cdef double 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)
@ -190,15 +204,19 @@ def calculate_greeks(
# 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
_delta = option_price_change / underlying_price_change
delta = _delta * f * 0.01
# 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) / \
_gamma = (gamma_delta_1 - gamma_delta_2) / \
(0.5 * (underlying_tree[0, 2] - underlying_tree[2, 2]))
gamma = _gamma * pow(f, 2) * 0.0001
# Theta
theta = (option_tree[1, 2] - option_tree[0, 0]) / (2 * dt * annual_days)
@ -238,7 +256,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v = 0.3 # Initial guess of volatility
v = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess
@ -259,7 +277,7 @@ def calculate_impv(
# Calculate guessed implied volatility of next round
v += dx
# Check end result to be non-negative
# Check new volatility to be non-negative
if v <= 0:
return 0

View File

@ -40,7 +40,7 @@ def calculate_price(
return max(0, cp * (s - k))
if not d1:
d1 = calculate_d1(s, k, r, r, v)
d1 = calculate_d1(s, k, r, t, v)
d2 = d1 - v * sqrt(t)
price = cp * (s * cdf(cp * d1) - k * cdf(cp * d2)) * exp(-r * t)
@ -208,7 +208,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v = 0.3 # Initial guess of volatility
v = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess

View File

@ -40,7 +40,7 @@ def calculate_price(
return max(0, cp * (s - k))
if not d1:
d1 = calculate_d1(s, k, r, r, v)
d1 = calculate_d1(s, k, r, t, v)
d2 = d1 - v * sqrt(t)
price = cp * (s * cdf(cp * d1) - k * cdf(cp * d2) * exp(-r * t))
@ -207,7 +207,7 @@ def calculate_impv(
return 0
# Calculate implied volatility with Newton's method
v = 0.3 # Initial guess of volatility
v = 0.01 # Initial guess of volatility
for i in range(50):
# Caculate option price and vega with current guess

View File

@ -5,13 +5,14 @@ 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 vnpy.trader.ui import QtWidgets, QtCore, QtGui
from vnpy.trader.event import EVENT_TICK, EVENT_TIMER, EVENT_TRADE
from vnpy.trader.object import TickData, TradeData
from vnpy.trader.utility import save_json, load_json
from ..engine import OptionEngine
from ..base import (
EVENT_OPTION_ALGO_PRICING,
EVENT_OPTION_ALGO_TRADING,
EVENT_OPTION_ALGO_STATUS,
EVENT_OPTION_ALGO_LOG
)
@ -36,6 +37,10 @@ class AlgoSpinBox(QtWidgets.QSpinBox):
""""""
return self.value()
def set_value(self, value: int) -> None:
""""""
self.setValue(value)
def update_status(self, active: bool) -> None:
""""""
self.setEnabled(not active)
@ -67,6 +72,10 @@ class AlgoDoubleSpinBox(QtWidgets.QDoubleSpinBox):
""""""
return self.value()
def set_value(self, value: float) -> None:
""""""
self.setValue(value)
def update_status(self, active: bool) -> None:
""""""
self.setEnabled(not active)
@ -105,6 +114,15 @@ class AlgoDirectionCombo(QtWidgets.QComboBox):
return value
def set_value(self, value: dict) -> None:
""""""
if value["long_allowed"] and value["short_allowed"]:
self.setCurrentIndex(0)
elif value["long_allowed"]:
self.setCurrentIndex(1)
else:
self.setCurrentIndex(2)
def update_status(self, active: bool) -> None:
""""""
self.setEnabled(not active)
@ -178,7 +196,7 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
signal_tick = QtCore.pyqtSignal(Event)
signal_pricing = QtCore.pyqtSignal(Event)
signal_status = QtCore.pyqtSignal(Event)
signal_trading = QtCore.pyqtSignal(Event)
signal_trade = QtCore.pyqtSignal(Event)
headers: List[Dict] = [
{"name": "bid_volume", "display": "买量", "cell": BidCell},
@ -194,7 +212,7 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
{"name": "price_spread", "display": "价格\n价差", "cell": AlgoDoubleSpinBox},
{"name": "volatility_spread", "display": "隐波\n价差", "cell": AlgoDoubleSpinBox},
{"name": "max_pos", "display": "持仓\n上限", "cell": AlgoPositiveSpinBox},
{"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},
@ -209,13 +227,16 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
self.option_engine = option_engine
self.event_engine = option_engine.event_engine
self.main_engine = option_engine.main_engine
self.algo_engine = option_engine.algo_engine
self.portfolio_name = portfolio_name
self.setting_filename = f"{portfolio_name}_electronic_eye.json"
self.cells: Dict[str, Dict] = {}
self.init_ui()
self.register_event()
self.load_setting()
def init_ui(self) -> None:
""""""
@ -315,21 +336,64 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
self.resizeColumnsToContents()
# Update all net pos and tick cells
for vt_symbol in self.cells.keys():
self.update_net_pos(vt_symbol)
tick = self.main_engine.get_tick(vt_symbol)
if tick:
self.update_tick(tick)
def load_setting(self) -> None:
""""""
fields = [
"price_spread",
"volatility_spread",
"max_pos",
"target_pos",
"max_order_size",
"direction"
]
setting = load_json(self.setting_filename)
for vt_symbol, cells in self.cells.items():
buf = setting.get(vt_symbol, None)
if buf:
for field in fields:
cells[field].set_value(buf[field])
def save_setting(self) -> None:
""""""
fields = [
"price_spread",
"volatility_spread",
"max_pos",
"target_pos",
"max_order_size",
"direction"
]
setting = {}
for vt_symbol, cells in self.cells.items():
buf = {}
for field in fields:
buf[field] = cells[field].get_value()
setting[vt_symbol] = buf
save_json(self.setting_filename, setting)
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.signal_trade.connect(self.process_trade_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
@ -338,10 +402,18 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
EVENT_TICK,
self.signal_tick.emit
)
self.event_engine.register(
EVENT_TRADE,
self.signal_trade.emit
)
def process_tick_event(self, event: Event) -> None:
""""""
tick = event.data
tick: TickData = event.data
self.update_tick(tick)
def update_tick(self, tick: TickData) -> None:
""""""
cells = self.cells.get(tick.vt_symbol, None)
if not cells:
return
@ -384,22 +456,19 @@ class ElectronicEyeMonitor(QtWidgets.QTableWidget):
cells["ref_price"].setText("")
cells["pricing_impv"].setText("")
def process_trading_event(self, event: Event) -> None:
def process_trade_event(self, event: Event) -> None:
""""""
algo = event.data
cells = self.cells[algo.vt_symbol]
trade: TradeData = event.data
self.update_net_pos(trade.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:
def update_net_pos(self, vt_symbol: str) -> None:
""""""
algo = event.data
cells = self.cells.get(vt_symbol, None)
if not cells:
return
cells = self.cells[algo.vt_symbol]
cells["net_pos"].setText(str(algo.option.net_pos))
option = self.option_engine.get_instrument(vt_symbol)
cells["net_pos"].setText(str(option.net_pos))
def start_algo_pricing(self, vt_symbol: str) -> None:
""""""
@ -605,6 +674,11 @@ class ElectronicEyeManager(QtWidgets.QWidget):
for vt_symbol in self.algo_monitor.cells.keys():
self.algo_monitor.stop_algo_trading(vt_symbol)
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
""""""
self.algo_monitor.save_setting()
event.accept()
class VolatilityDoubleSpinBox(QtWidgets.QDoubleSpinBox):
""""""
@ -859,7 +933,10 @@ class PricingVolatilityManager(QtWidgets.QWidget):
otm = chain.puts[index]
value = round(otm.pricing_impv * 100, 1)
cells = self.cells[(chain_symbol, index)]
key = (chain_symbol, index)
cells = self.cells.get(key, None)
if cells:
cells["pricing_impv"].setValue(value)
def update_mid_impv(self, chain_symbol: str) -> None:

View File

@ -115,10 +115,10 @@ class OptionMarketMonitor(MonitorTable):
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": "cash_vega", "display": "Vega", "cell": GreeksCell},
{"name": "cash_theta", "display": "Theta", "cell": GreeksCell},
{"name": "cash_gamma", "display": "Gamma", "cell": GreeksCell},
{"name": "cash_delta", "display": "Delta", "cell": GreeksCell},
{"name": "open_interest", "display": "持仓量", "cell": MonitorCell},
{"name": "volume", "display": "成交量", "cell": MonitorCell},
{"name": "bid_impv", "display": "买隐波", "cell": BidCell},
@ -158,6 +158,9 @@ class OptionMarketMonitor(MonitorTable):
self.option_symbols.add(option.vt_symbol)
self.underlying_option_map[option.underlying.vt_symbol].append(option.vt_symbol)
# Get greeks decimals precision
self.greeks_precision = f"{portfolio.precision}f"
# Set table row and column numbers
row_count = 0
for chain in portfolio.chains.values():
@ -310,10 +313,10 @@ class OptionMarketMonitor(MonitorTable):
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}")
option_cells["cash_delta"].setText(f"{option.cash_delta:.{self.greeks_precision}}")
option_cells["cash_gamma"].setText(f"{option.cash_gamma:.{self.greeks_precision}}")
option_cells["cash_theta"].setText(f"{option.cash_theta:.{self.greeks_precision}}")
option_cells["cash_vega"].setText(f"{option.cash_vega:.{self.greeks_precision}}")
class OptionGreeksMonitor(MonitorTable):
@ -362,6 +365,9 @@ class OptionGreeksMonitor(MonitorTable):
self.option_symbols.add(option.vt_symbol)
self.underlying_option_map[option.underlying.vt_symbol].append(option.vt_symbol)
# Get greeks decimals precision
self.greeks_precision = f"{portfolio.precision}f"
# Set table row and column numbers
row_count = 1
for chain in portfolio.chains.values():
@ -506,12 +512,12 @@ class OptionGreeksMonitor(MonitorTable):
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}")
row_cells["pos_delta"].setText(f"{row_data.pos_delta:.{self.greeks_precision}}")
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}")
row_cells["pos_gamma"].setText(f"{row_data.pos_gamma:.{self.greeks_precision}}")
row_cells["pos_theta"].setText(f"{row_data.pos_theta:.{self.greeks_precision}}")
row_cells["pos_vega"].setText(f"{row_data.pos_vega:.{self.greeks_precision}}")
class OptionChainMonitor(MonitorTable):

View File

@ -6,6 +6,7 @@ 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 vnpy.trader.utility import get_digits
from ..base import APP_NAME, EVENT_OPTION_NEW_PORTFOLIO
from ..engine import OptionEngine, PRICING_MODELS
@ -169,7 +170,7 @@ class OptionManager(QtWidgets.QWidget):
def closeEvent(self, event: QtGui.QCloseEvent) -> None:
""""""
if self.portfolio_name:
if self.market_monitor:
self.market_monitor.close()
self.greeks_monitor.close()
self.volatility_chart.close()
@ -215,7 +216,7 @@ class PortfolioDialog(QtWidgets.QDialog):
self.model_name_combo.findText(model_name)
)
form.addRow("模型", self.model_name_combo)
form.addRow("定价模型", self.model_name_combo)
# Interest rate spin
self.interest_rate_spin = QtWidgets.QDoubleSpinBox()
@ -227,7 +228,29 @@ class PortfolioDialog(QtWidgets.QDialog):
interest_rate = portfolio_setting.get("interest_rate", 0.02)
self.interest_rate_spin.setValue(interest_rate * 100)
form.addRow("利率", self.interest_rate_spin)
form.addRow("年化利率", self.interest_rate_spin)
# Inverse combo
self.inverse_combo = QtWidgets.QComboBox()
self.inverse_combo.addItems(["正向", "反向"])
inverse = portfolio_setting.get("inverse", False)
if inverse:
self.inverse_combo.setCurrentIndex(1)
else:
self.inverse_combo.setCurrentIndex(0)
form.addRow("合约模式", self.inverse_combo)
# Greeks decimals precision
self.precision_spin = QtWidgets.QSpinBox()
self.precision_spin.setMinimum(0)
self.precision_spin.setMaximum(10)
precision = portfolio_setting.get("precision", 0)
self.precision_spin.setValue(precision)
form.addRow("Greeks小数位", self.precision_spin)
# Underlying for each chain
self.combos: Dict[str, QtWidgets.QComboBox] = {}
@ -266,6 +289,13 @@ class PortfolioDialog(QtWidgets.QDialog):
model_name = self.model_name_combo.currentText()
interest_rate = self.interest_rate_spin.value() / 100
if self.inverse_combo.currentIndex() == 0:
inverse = False
else:
inverse = True
precision = self.precision_spin.value()
chain_underlying_map = {}
for chain_symbol, combo in self.combos.items():
underlying_symbol = combo.currentText()
@ -277,7 +307,9 @@ class PortfolioDialog(QtWidgets.QDialog):
self.portfolio_name,
model_name,
interest_rate,
chain_underlying_map
chain_underlying_map,
inverse,
precision
)
result = self.option_engine.init_portfolio(self.portfolio_name)
@ -301,10 +333,12 @@ class OptionManualTrader(QtWidgets.QWidget):
self.event_engine: EventEngine = option_engine.event_engine
self.contracts: Dict[str, ContractData] = {}
self.vt_symbol = ""
self.vt_symbol: str = ""
self.price_digits: int = 0
self.init_ui()
self.init_contracts()
self.connect_signal()
def init_ui(self) -> None:
""""""
@ -489,12 +523,14 @@ class OptionManualTrader(QtWidgets.QWidget):
vt_symbol = contract.vt_symbol
self.vt_symbol = vt_symbol
self.price_digits = get_digits(contract.pricetick)
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)
print(EVENT_TICK + vt_symbol)
self.event_engine.register(EVENT_TICK + vt_symbol, self.process_tick_event)
def create_label(
self,
@ -513,18 +549,18 @@ class OptionManualTrader(QtWidgets.QWidget):
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))
price_digits = self.price_digits
self.lp_label.setText(f"{tick.last_price:.{price_digits}f}")
self.bp1_label.setText(f"{tick.bid_price_1:.{price_digits}f}")
self.bv1_label.setText(str(tick.bid_volume_1))
self.ap1_label.setText(str(tick.ask_price_1))
self.ap1_label.setText(f"{tick.ask_price_1:.{price_digits}f}")
self.av1_label.setText(str(tick.ask_volume_1))
if tick.pre_close:
@ -532,24 +568,24 @@ class OptionManualTrader(QtWidgets.QWidget):
self.return_label.setText(f"{r:.2f}%")
if tick.bid_price_2:
self.bp2_label.setText(str(tick.bid_price_2))
self.bp2_label.setText(f"{tick.bid_price_2:.{price_digits}f}")
self.bv2_label.setText(str(tick.bid_volume_2))
self.ap2_label.setText(str(tick.ask_price_2))
self.ap2_label.setText(f"{tick.ask_price_2:.{price_digits}f}")
self.av2_label.setText(str(tick.ask_volume_2))
self.bp3_label.setText(str(tick.bid_price_3))
self.bp3_label.setText(f"{tick.bid_price_3:.{price_digits}f}")
self.bv3_label.setText(str(tick.bid_volume_3))
self.ap3_label.setText(str(tick.ask_price_3))
self.ap3_label.setText(f"{tick.ask_price_3:.{price_digits}f}")
self.av3_label.setText(str(tick.ask_volume_3))
self.bp4_label.setText(str(tick.bid_price_4))
self.bp4_label.setText(f"{tick.bid_price_4:.{price_digits}f}")
self.bv4_label.setText(str(tick.bid_volume_4))
self.ap4_label.setText(str(tick.ask_price_4))
self.ap4_label.setText(f"{tick.ask_price_4:.{price_digits}f}")
self.av4_label.setText(str(tick.ask_volume_4))
self.bp5_label.setText(str(tick.bid_price_5))
self.bp5_label.setText(f"{tick.bid_price_5:.{price_digits}f}")
self.bv5_label.setText(str(tick.bid_volume_5))
self.ap5_label.setText(str(tick.ask_price_5))
self.ap5_label.setText(f"{tick.ask_price_5:.{price_digits}f}")
self.av5_label.setText(str(tick.ask_volume_5))
def clear_data(self) -> None:
@ -664,7 +700,7 @@ class OptionHedgeWidget(QtWidgets.QWidget):
# Check delta of underlying
underlying = self.option_engine.get_instrument(vt_symbol)
min_range = int(underlying.theo_delta * 0.6)
min_range = int(underlying.cash_delta * 0.6)
if delta_range < min_range:
msg = f"Delta对冲阈值({delta_range})低于对冲合约"\
f"Delta值的60%({min_range}),可能导致来回频繁对冲!"

View File

@ -842,7 +842,7 @@ class CtpTdApi(TdApi):
)
self.gateway.on_order(order)
self.gateway.write_error("交易委托失败", error)
#self.gateway.write_error("交易委托失败", error)
def onRspOrderAction(self, data: dict, error: dict, reqid: int, last: bool):
""""""
@ -1076,6 +1076,7 @@ class CtpTdApi(TdApi):
traded=data["VolumeTraded"],
status=STATUS_CTP2VT[data["OrderStatus"]],
time=data["InsertTime"],
cancel_time=data["CancelTime"],
gateway_name=self.gateway_name
)
self.gateway.on_order(order)

View File

@ -1042,8 +1042,8 @@ class RohonTdApi(TdApi):
"""
Callback of order status update.
"""
#if self.gateway.debug:
# print(f'onRtnOrder')
if self.gateway.debug:
print(f'onRtnOrder')
symbol = data["InstrumentID"]
exchange = symbol_exchange_map.get(symbol, "")

View File

@ -1,9 +1,10 @@
"""
"""
import pytz
from datetime import datetime
from time import sleep
from copy import copy
from vnpy.api.sopt import (
MdApi,
TdApi,
@ -59,7 +60,15 @@ from vnpy.trader.object import (
CancelRequest,
SubscribeRequest,
)
from vnpy.trader.utility import get_folder_path
from vnpy.trader.utility import (
extract_vt_symbol,
get_folder_path,
get_trading_date,
get_underlying_symbol,
round_to,
BarGenerator,
print_dict
)
from vnpy.trader.event import EVENT_TIMER
@ -111,6 +120,7 @@ OPTIONTYPE_SOPT2VT = {
THOST_FTDC_CP_PutOptions: OptionType.PUT
}
CHINA_TZ = pytz.timezone("Asia/Shanghai")
symbol_exchange_map = {}
symbol_name_map = {}
@ -135,13 +145,19 @@ class SoptGateway(BaseGateway):
exchanges = list(EXCHANGE_SOPT2VT.values())
def __init__(self, event_engine):
def __init__(self, event_engine, gateway_name="SOPT"):
"""Constructor"""
super().__init__(event_engine, "SOPT")
super().__init__(event_engine, gateway_name)
self.td_api = SoptTdApi(self)
self.md_api = SoptMdApi(self)
self.subscribed_symbols = set() # 已订阅合约代码
# 自定义价差/加比的tick合成器
self.combiners = {}
self.tick_combiner_map = {}
def connect(self, setting: dict):
""""""
userid = setting["用户名"]
@ -161,10 +177,88 @@ class SoptGateway(BaseGateway):
self.td_api.connect(td_address, userid, password, brokerid, auth_code, appid, product_info)
self.md_api.connect(md_address, userid, password, brokerid)
# 获取自定义价差/价比合约的配置
try:
from vnpy.trader.engine import CustomContract
c = CustomContract()
self.combiner_conf_dict = c.get_config()
if len(self.combiner_conf_dict) > 0:
self.write_log(u'加载的自定义价差/价比配置:{}'.format(self.combiner_conf_dict))
contract_dict = c.get_contracts()
for vt_symbol, contract in contract_dict.items():
contract.gateway_name = self.gateway_name
symbol_exchange_map[contract.symbol] = contract.exchange
self.on_contract(contract)
except Exception as ex: # noqa
pass
self.init_query()
# 从新发出委托
for (vt_symbol, is_bar) in list(self.subscribed_symbols):
symbol, exchange = extract_vt_symbol(vt_symbol)
req = SubscribeRequest(
symbol=symbol,
exchange=exchange,
is_bar=is_bar
)
self.subscribe(req)
def subscribe(self, req: SubscribeRequest):
""""""
# 如果是自定义的套利合约符号
if req.symbol in self.combiner_conf_dict:
self.write_log(u'订阅自定义套利合约:{}'.format(req.symbol))
# 创建合成器
if req.symbol not in self.combiners:
setting = self.combiner_conf_dict.get(req.symbol)
setting.update({"symbol": req.symbol})
combiner = TickCombiner(self, setting)
# 更新合成器
self.write_log(u'添加{}与合成器映射'.format(req.symbol))
self.combiners.update({setting.get('symbol'): combiner})
# 增加映射( leg1 对应的合成器列表映射)
leg1_symbol = setting.get('leg1_symbol')
leg1_exchange = Exchange(setting.get('leg1_exchange'))
combiner_list = self.tick_combiner_map.get(leg1_symbol, [])
if combiner not in combiner_list:
self.write_log(u'添加Leg1:{}与合成器得映射'.format(leg1_symbol))
combiner_list.append(combiner)
self.tick_combiner_map.update({leg1_symbol: combiner_list})
# 增加映射( leg2 对应的合成器列表映射)
leg2_symbol = setting.get('leg2_symbol')
leg2_exchange = Exchange(setting.get('leg2_exchange'))
combiner_list = self.tick_combiner_map.get(leg2_symbol, [])
if combiner not in combiner_list:
self.write_log(u'添加Leg2:{}与合成器得映射'.format(leg2_symbol))
combiner_list.append(combiner)
self.tick_combiner_map.update({leg2_symbol: combiner_list})
self.write_log(u'订阅leg1:{}'.format(leg1_symbol))
leg1_req = SubscribeRequest(
symbol=leg1_symbol,
exchange=leg1_exchange
)
self.subscribe(leg1_req)
self.write_log(u'订阅leg2:{}'.format(leg2_symbol))
leg2_req = SubscribeRequest(
symbol=leg2_symbol,
exchange=leg2_exchange
)
self.subscribe(leg2_req)
self.subscribed_symbols.add((req.vt_symbol, req.is_bar))
else:
self.write_log(u'{}合成器已经在存在'.format(req.symbol))
return
elif req.exchange == Exchange.SPD:
self.write_error(u'自定义合约{}不在CTP设置中'.format(req.symbol))
self.md_api.subscribe(req)
def send_order(self, req: OrderRequest):
@ -213,6 +307,15 @@ class SoptGateway(BaseGateway):
self.event_engine.register(EVENT_TIMER, self.process_timer_event)
def on_custom_tick(self, tick):
"""推送自定义合约行情"""
# 自定义合约行情
for combiner in self.tick_combiner_map.get(tick.symbol, []):
tick = copy(tick)
combiner.on_tick(tick)
class SoptMdApi(MdApi):
""""""
@ -239,6 +342,7 @@ class SoptMdApi(MdApi):
"""
self.gateway.write_log("行情服务器连接成功")
self.login()
self.gateway.status.update({'md_con': True, 'md_con_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
def onFrontDisconnected(self, reason: int):
"""
@ -246,6 +350,7 @@ class SoptMdApi(MdApi):
"""
self.login_status = False
self.gateway.write_log(f"行情服务器连接断开,原因{reason}")
self.gateway.status.update({'md_con': False, 'md_dis_con_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
def onRspUserLogin(self, data: dict, error: dict, reqid: int, last: bool):
"""
@ -282,11 +387,13 @@ class SoptMdApi(MdApi):
if not exchange:
return
timestamp = f"{data['TradingDay']} {data['UpdateTime']}.{int(data['UpdateMillisec']/100)}"
dt = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f")
dt = CHINA_TZ.localize(dt)
tick = TickData(
symbol=symbol,
exchange=exchange,
datetime=datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f"),
datetime=dt,
name=symbol_name_map[symbol],
volume=data["Volume"],
open_interest=data["OpenInterest"],
@ -325,6 +432,7 @@ class SoptMdApi(MdApi):
tick.ask_volume_5 = data["AskVolume5"]
self.gateway.on_tick(tick)
self.gateway.on_custom_tick(tick)
def connect(self, address: str, userid: str, password: str, brokerid: int):
"""
@ -370,6 +478,7 @@ class SoptMdApi(MdApi):
Subscribe to tick data update.
"""
if self.login_status:
self.gateway.write_log(f'订阅:{req.exchange} {req.symbol}')
self.subscribeMarketData(req.symbol)
self.subscribed.add(req.symbol)
@ -422,11 +531,13 @@ class SoptTdApi(TdApi):
self.authenticate()
else:
self.login()
self.gateway.status.update({'td_con': True, 'td_con_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
def onFrontDisconnected(self, reason: int):
""""""
self.login_status = False
self.gateway.write_log(f"交易服务器连接断开,原因{reason}")
self.gateway.status.update({'td_con': True, 'td_dis_con_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')})
def onRspAuthenticate(self, data: dict, error: dict, reqid: int, last: bool):
""""""
@ -464,14 +575,19 @@ class SoptTdApi(TdApi):
symbol = data["InstrumentID"]
exchange = symbol_exchange_map[symbol]
order_type = OrderType.LIMIT
if data["OrderPriceType"] == THOST_FTDC_OPT_AnyPrice:
order_type = OrderType.MARKET
order = OrderData(
accountid=self.userid,
symbol=symbol,
exchange=exchange,
orderid=orderid,
sys_orderid=orderid,
direction=DIRECTION_SOPT2VT[data["Direction"]],
offset=OFFSET_SOPT2VT[data["CombOffsetFlag"]],
price=data["LimitPrice"],
type=order_type,
volume=data["VolumeTotalOriginal"],
status=Status.REJECTED,
gateway_name=self.gateway_name
@ -558,6 +674,20 @@ class SoptTdApi(TdApi):
gateway_name=self.gateway_name
)
account.available = data["Available"]
account.commission = round(float(data['Commission']), 7)
account.margin = round(float(data['CurrMargin']), 7)
account.close_profit = round(float(data['CloseProfit']), 7)
account.holding_profit = round(float(data['PositionProfit']), 7)
account.trading_day = str(data.get('TradingDay',datetime.now().strftime('%Y-%m-%d')))
if '-' not in account.trading_day and len(account.trading_day) == 8:
account.trading_day = '-'.join(
[
account.trading_day[0:4],
account.trading_day[4:6],
account.trading_day[6:8]
]
)
self.gateway.on_account(account)
@ -589,7 +719,7 @@ class SoptTdApi(TdApi):
)
contract.option_type = OPTIONTYPE_SOPT2VT.get(data["OptionsType"], None)
contract.option_strike = data["StrikePrice"]
contract.option_index = str(data["StrikePrice"])
#contract.option_index = str(data["StrikePrice"])
contract.option_expiry = datetime.strptime(data["ExpireDate"], "%Y%m%d")
contract.option_index = get_option_index(
contract.option_strike, data["InstrumentCode"]
@ -627,10 +757,16 @@ class SoptTdApi(TdApi):
order_ref = data["OrderRef"]
orderid = f"{frontid}_{sessionid}_{order_ref}"
timestamp = f"{data['InsertDate']} {data['InsertTime']}"
dt = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S")
dt = CHINA_TZ.localize(dt)
order = OrderData(
accountid=self.userid,
symbol=symbol,
exchange=exchange,
orderid=orderid,
sys_orderid=orderid,
type=ORDERTYPE_SOPT2VT[data["OrderPriceType"]],
direction=DIRECTION_SOPT2VT[data["Direction"]],
offset=OFFSET_SOPT2VT[data["CombOffsetFlag"]],
@ -638,7 +774,8 @@ class SoptTdApi(TdApi):
volume=data["VolumeTotalOriginal"],
traded=data["VolumeTraded"],
status=STATUS_SOPT2VT[data["OrderStatus"]],
time=data["InsertTime"],
datetime=dt,
cancel_time=data["CancelTime"],
gateway_name=self.gateway_name
)
self.gateway.on_order(order)
@ -657,16 +794,22 @@ class SoptTdApi(TdApi):
orderid = self.sysid_orderid_map[data["OrderSysID"]]
timestamp = f"{data['TradeDate']} {data['TradeTime']}"
dt = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S")
dt = CHINA_TZ.localize(dt)
trade = TradeData(
accountid=self.userid,
symbol=symbol,
exchange=exchange,
orderid=orderid,
sys_orderid=orderid,
tradeid=data["TradeID"],
direction=DIRECTION_SOPT2VT[data["Direction"]],
offset=OFFSET_SOPT2VT[data["OffsetFlag"]],
price=data["Price"],
volume=data["Volume"],
time=data["TradeTime"],
datetime=dt,
gateway_name=self.gateway_name
)
self.gateway.on_trade(trade)
@ -783,6 +926,8 @@ class SoptTdApi(TdApi):
orderid = f"{self.frontid}_{self.sessionid}_{self.order_ref}"
order = req.create_order_data(orderid, self.gateway_name)
order.accountid = self.userid
order.vt_accountid = f"{self.gateway_name}.{self.userid}"
self.gateway.on_order(order)
return order.vt_orderid
@ -852,3 +997,227 @@ def get_option_index(strike_price: float, exchange_instrument_id: str) -> str:
option_index = f"{strike_price:.3f}-{index}"
return option_index
class TickCombiner(object):
"""
Tick合成类
"""
def __init__(self, gateway, setting):
self.gateway = gateway
self.gateway_name = self.gateway.gateway_name
self.gateway.write_log(u'创建tick合成类:{}'.format(setting))
self.symbol = setting.get('symbol', None)
self.leg1_symbol = setting.get('leg1_symbol', None)
self.leg2_symbol = setting.get('leg2_symbol', None)
self.leg1_ratio = setting.get('leg1_ratio', 1) # 腿1的数量配比
self.leg2_ratio = setting.get('leg2_ratio', 1) # 腿2的数量配比
self.price_tick = setting.get('price_tick', 1) # 合成价差加比后的最小跳动
# 价差
self.is_spread = setting.get('is_spread', False)
# 价比
self.is_ratio = setting.get('is_ratio', False)
self.last_leg1_tick = None
self.last_leg2_tick = None
# 价差日内最高/最低价
self.spread_high = None
self.spread_low = None
# 价比日内最高/最低价
self.ratio_high = None
self.ratio_low = None
# 当前交易日
self.trading_day = None
if self.is_ratio and self.is_spread:
self.gateway.write_error(u'{}参数有误,不能同时做价差/加比.setting:{}'.format(self.symbol, setting))
return
self.gateway.write_log(u'初始化{}合成器成功'.format(self.symbol))
if self.is_spread:
self.gateway.write_log(
u'leg1:{} * {} - leg2:{} * {}'.format(self.leg1_symbol, self.leg1_ratio, self.leg2_symbol,
self.leg2_ratio))
if self.is_ratio:
self.gateway.write_log(
u'leg1:{} * {} / leg2:{} * {}'.format(self.leg1_symbol, self.leg1_ratio, self.leg2_symbol,
self.leg2_ratio))
def on_tick(self, tick):
"""OnTick处理"""
combinable = False
if tick.symbol == self.leg1_symbol:
# leg1合约
self.last_leg1_tick = tick
if self.last_leg2_tick is not None:
if self.last_leg1_tick.datetime.replace(microsecond=0) == self.last_leg2_tick.datetime.replace(
microsecond=0):
combinable = True
elif tick.symbol == self.leg2_symbol:
# leg2合约
self.last_leg2_tick = tick
if self.last_leg1_tick is not None:
if self.last_leg2_tick.datetime.replace(microsecond=0) == self.last_leg1_tick.datetime.replace(
microsecond=0):
combinable = True
# 不能合并
if not combinable:
return
if not self.is_ratio and not self.is_spread:
return
# 以下情况,基本为单腿涨跌停,不合成价差/价格比 Tick
if (self.last_leg1_tick.ask_price_1 == 0 or self.last_leg1_tick.bid_price_1 == self.last_leg1_tick.limit_up) \
and self.last_leg1_tick.ask_volume_1 == 0:
self.gateway.write_log(
u'leg1:{0}涨停{1}不合成价差Tick'.format(self.last_leg1_tick.vt_symbol, self.last_leg1_tick.bid_price_1))
return
if (self.last_leg1_tick.bid_price_1 == 0 or self.last_leg1_tick.ask_price_1 == self.last_leg1_tick.limit_down) \
and self.last_leg1_tick.bid_volume_1 == 0:
self.gateway.write_log(
u'leg1:{0}跌停{1}不合成价差Tick'.format(self.last_leg1_tick.vt_symbol, self.last_leg1_tick.ask_price_1))
return
if (self.last_leg2_tick.ask_price_1 == 0 or self.last_leg2_tick.bid_price_1 == self.last_leg2_tick.limit_up) \
and self.last_leg2_tick.ask_volume_1 == 0:
self.gateway.write_log(
u'leg2:{0}涨停{1}不合成价差Tick'.format(self.last_leg2_tick.vt_symbol, self.last_leg2_tick.bid_price_1))
return
if (self.last_leg2_tick.bid_price_1 == 0 or self.last_leg2_tick.ask_price_1 == self.last_leg2_tick.limit_down) \
and self.last_leg2_tick.bid_volume_1 == 0:
self.gateway.write_log(
u'leg2:{0}跌停{1}不合成价差Tick'.format(self.last_leg2_tick.vt_symbol, self.last_leg2_tick.ask_price_1))
return
if self.trading_day != tick.trading_day:
self.trading_day = tick.trading_day
self.spread_high = None
self.spread_low = None
self.ratio_high = None
self.ratio_low = None
if self.is_spread:
spread_tick = TickData(gateway_name=self.gateway_name,
symbol=self.symbol,
exchange=Exchange.SPD,
datetime=tick.datetime)
spread_tick.trading_day = tick.trading_day
spread_tick.date = tick.date
spread_tick.time = tick.time
# 叫卖价差=leg1.ask_price_1 * 配比 - leg2.bid_price_1 * 配比volume为两者最小
spread_tick.ask_price_1 = round_to(target=self.price_tick,
value=self.last_leg1_tick.ask_price_1 * self.leg1_ratio - self.last_leg2_tick.bid_price_1 * self.leg2_ratio)
spread_tick.ask_volume_1 = min(self.last_leg1_tick.ask_volume_1, self.last_leg2_tick.bid_volume_1)
# 叫买价差=leg1.bid_price_1 * 配比 - leg2.ask_price_1 * 配比volume为两者最小
spread_tick.bid_price_1 = round_to(target=self.price_tick,
value=self.last_leg1_tick.bid_price_1 * self.leg1_ratio - self.last_leg2_tick.ask_price_1 * self.leg2_ratio)
spread_tick.bid_volume_1 = min(self.last_leg1_tick.bid_volume_1, self.last_leg2_tick.ask_volume_1)
# 最新价
spread_tick.last_price = round_to(target=self.price_tick,
value=(spread_tick.ask_price_1 + spread_tick.bid_price_1) / 2)
# 昨收盘价
if self.last_leg2_tick.pre_close > 0 and self.last_leg1_tick.pre_close > 0:
spread_tick.pre_close = round_to(target=self.price_tick,
value=self.last_leg1_tick.pre_close * self.leg1_ratio - self.last_leg2_tick.pre_close * self.leg2_ratio)
# 开盘价
if self.last_leg2_tick.open_price > 0 and self.last_leg1_tick.open_price > 0:
spread_tick.open_price = round_to(target=self.price_tick,
value=self.last_leg1_tick.open_price * self.leg1_ratio - self.last_leg2_tick.open_price * self.leg2_ratio)
# 最高价
if self.spread_high:
self.spread_high = max(self.spread_high, spread_tick.ask_price_1)
else:
self.spread_high = spread_tick.ask_price_1
spread_tick.high_price = self.spread_high
# 最低价
if self.spread_low:
self.spread_low = min(self.spread_low, spread_tick.bid_price_1)
else:
self.spread_low = spread_tick.bid_price_1
spread_tick.low_price = self.spread_low
self.gateway.on_tick(spread_tick)
if self.is_ratio:
ratio_tick = TickData(
gateway_name=self.gateway_name,
symbol=self.symbol,
exchange=Exchange.SPD,
datetime=tick.datetime
)
ratio_tick.trading_day = tick.trading_day
ratio_tick.date = tick.date
ratio_tick.time = tick.time
# 比率tick = (腿1 * 腿1 手数 / 腿2价格 * 腿2手数) 百分比
ratio_tick.ask_price_1 = 100 * self.last_leg1_tick.ask_price_1 * self.leg1_ratio \
/ (self.last_leg2_tick.bid_price_1 * self.leg2_ratio) # noqa
ratio_tick.ask_price_1 = round_to(
target=self.price_tick,
value=ratio_tick.ask_price_1
)
ratio_tick.ask_volume_1 = min(self.last_leg1_tick.ask_volume_1, self.last_leg2_tick.bid_volume_1)
ratio_tick.bid_price_1 = 100 * self.last_leg1_tick.bid_price_1 * self.leg1_ratio \
/ (self.last_leg2_tick.ask_price_1 * self.leg2_ratio) # noqa
ratio_tick.bid_price_1 = round_to(
target=self.price_tick,
value=ratio_tick.bid_price_1
)
ratio_tick.bid_volume_1 = min(self.last_leg1_tick.bid_volume_1, self.last_leg2_tick.ask_volume_1)
ratio_tick.last_price = (ratio_tick.ask_price_1 + ratio_tick.bid_price_1) / 2
ratio_tick.last_price = round_to(
target=self.price_tick,
value=ratio_tick.last_price
)
# 昨收盘价
if self.last_leg2_tick.pre_close > 0 and self.last_leg1_tick.pre_close > 0:
ratio_tick.pre_close = 100 * self.last_leg1_tick.pre_close * self.leg1_ratio / (
self.last_leg2_tick.pre_close * self.leg2_ratio) # noqa
ratio_tick.pre_close = round_to(
target=self.price_tick,
value=ratio_tick.pre_close
)
# 开盘价
if self.last_leg2_tick.open_price > 0 and self.last_leg1_tick.open_price > 0:
ratio_tick.open_price = 100 * self.last_leg1_tick.open_price * self.leg1_ratio / (
self.last_leg2_tick.open_price * self.leg2_ratio) # noqa
ratio_tick.open_price = round_to(
target=self.price_tick,
value=ratio_tick.open_price
)
# 最高价
if self.ratio_high:
self.ratio_high = max(self.ratio_high, ratio_tick.ask_price_1)
else:
self.ratio_high = ratio_tick.ask_price_1
ratio_tick.high_price = self.spread_high
# 最低价
if self.ratio_low:
self.ratio_low = min(self.ratio_low, ratio_tick.bid_price_1)
else:
self.ratio_low = ratio_tick.bid_price_1
ratio_tick.low_price = self.spread_low
self.gateway.on_tick(ratio_tick)

View File

@ -316,6 +316,21 @@ def ceil_to(value: float, target: float) -> float:
return result
def get_digits(value: float) -> int:
"""
Get number of digits after decimal point.
"""
value_str = str(value)
if "e-" in value_str:
_, buf = value_str.split("e-")
return int(buf)
elif "." in value_str:
_, buf = value_str.split(".")
return len(buf)
else:
return 0
def print_dict(d: dict):
"""返回dict的字符串类型"""
return '\n'.join([f'{key}:{d[key]}' for key in sorted(d.keys())])
@ -705,6 +720,10 @@ class BarGenerator:
if not tick.last_price:
return
# Filter tick data with older timestamp
if self.last_tick and tick.datetime < self.last_tick.datetime:
return
if not self.bar:
new_minute = True
elif self.bar.datetime.minute != tick.datetime.minute: