diff --git a/vnpy/api/sopt/vnsopt/vnsopttd/vnsopttd.cpp b/vnpy/api/sopt/vnsopt/vnsopttd/vnsopttd.cpp index bdc7fd1d..e84ae639 100644 --- a/vnpy/api/sopt/vnsopt/vnsopttd/vnsopttd.cpp +++ b/vnpy/api/sopt/vnsopt/vnsopttd/vnsopttd.cpp @@ -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() diff --git a/vnpy/api/sopt/vnsopttd.pyd b/vnpy/api/sopt/vnsopttd.pyd index 0cf3939b..a6284a0a 100644 Binary files a/vnpy/api/sopt/vnsopttd.pyd and b/vnpy/api/sopt/vnsopttd.pyd differ diff --git a/vnpy/app/algo_trading/engine.py b/vnpy/app/algo_trading/engine.py index 252c56c3..920db323 100644 --- a/vnpy/app/algo_trading/engine.py +++ b/vnpy/app/algo_trading/engine.py @@ -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 - - algos = self.symbol_algo_map.get(tick.vt_symbol, None) - if algos: - for algo in algos: - algo.update_tick(tick) + 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 - algos = list(self.algos.values()) + try: + algos = list(self.algos.values()) - for algo in algos: - algo.update_timer() + 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): """""" - 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) + 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): """""" - 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) + 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()): diff --git a/vnpy/app/algo_trading/template.py b/vnpy/app/algo_trading/template.py index 0cda7251..2044b4df 100644 --- a/vnpy/app/algo_trading/template.py +++ b/vnpy/app/algo_trading/template.py @@ -103,6 +103,7 @@ class AlgoTemplate: self.put_variables_event() self.write_log("停止算法") + return True def subscribe(self, vt_symbol): """""" diff --git a/vnpy/app/cta_strategy_pro/back_testing.py b/vnpy/app/cta_strategy_pro/back_testing.py index 0c844d05..d33b33bc 100644 --- a/vnpy/app/cta_strategy_pro/back_testing.py +++ b/vnpy/app/cta_strategy_pro/back_testing.py @@ -787,7 +787,8 @@ class BackTestingEngine(object): """保存策略数据""" for strategy in self.strategies.values(): self.write_log(u'save strategy data') - strategy.save_data() + if hasattr(strategy,'save_data'): + strategy.save_data() def send_order(self, strategy: CtaTemplate, diff --git a/vnpy/app/cta_strategy_pro/template.py b/vnpy/app/cta_strategy_pro/template.py index e74e07e0..891e9626 100644 --- a/vnpy/app/cta_strategy_pro/template.py +++ b/vnpy/app/cta_strategy_pro/template.py @@ -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, diff --git a/vnpy/app/option_master/algo.py b/vnpy/app/option_master/algo.py index b59563fd..d3d3e19f 100644 --- a/vnpy/app/option_master/algo.py +++ b/vnpy/app/option_master/algo.py @@ -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 diff --git a/vnpy/app/option_master/base.py b/vnpy/app/option_master/base.py index ad0307b7..a6bbd6a7 100644 --- a/vnpy/app/option_master/base.py +++ b/vnpy/app/option_master/base.py @@ -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,7 +385,13 @@ class ChainData: if option.chain_index not in self.indexes: self.indexes.append(option.chain_index) - self.indexes.sort() + + # 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) diff --git a/vnpy/app/option_master/engine.py b/vnpy/app/option_master/engine.py index a018a153..07759e71 100644 --- a/vnpy/app/option_master/engine.py +++ b/vnpy/app/option_master/engine.py @@ -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 diff --git a/vnpy/app/option_master/pricing/binomial_tree.py b/vnpy/app/option_master/pricing/binomial_tree.py index d6697f12..91d09adf 100644 --- a/vnpy/app/option_master/pricing/binomial_tree.py +++ b/vnpy/app/option_master/pricing/binomial_tree.py @@ -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,9 +249,9 @@ def calculate_impv( # Calculate guessed implied volatility of next round v += dx - # Check end result to be non-negative - if v <= 0: - return 0 + # Check new volatility to be non-negative + if v <= 0: + return 0 # Round to 4 decimal places v = round(v, 4) diff --git a/vnpy/app/option_master/pricing/binomial_tree_cython.pyd b/vnpy/app/option_master/pricing/binomial_tree_cython.pyd index dc3682f7..49457a44 100644 Binary files a/vnpy/app/option_master/pricing/binomial_tree_cython.pyd and b/vnpy/app/option_master/pricing/binomial_tree_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/black_76.py b/vnpy/app/option_master/pricing/black_76.py index 3ed021c3..4a2bda47 100644 --- a/vnpy/app/option_master/pricing/black_76.py +++ b/vnpy/app/option_master/pricing/black_76.py @@ -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 diff --git a/vnpy/app/option_master/pricing/black_76_cython.pyd b/vnpy/app/option_master/pricing/black_76_cython.pyd index 85b0aad7..bc26ec3b 100644 Binary files a/vnpy/app/option_master/pricing/black_76_cython.pyd and b/vnpy/app/option_master/pricing/black_76_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/black_scholes.py b/vnpy/app/option_master/pricing/black_scholes.py index c3b6c979..36d9c347 100644 --- a/vnpy/app/option_master/pricing/black_scholes.py +++ b/vnpy/app/option_master/pricing/black_scholes.py @@ -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 diff --git a/vnpy/app/option_master/pricing/black_scholes_cython.pyd b/vnpy/app/option_master/pricing/black_scholes_cython.pyd index a636dbb1..7d40cded 100644 Binary files a/vnpy/app/option_master/pricing/black_scholes_cython.pyd and b/vnpy/app/option_master/pricing/black_scholes_cython.pyd differ diff --git a/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx index 9390d302..e0ee0185 100644 --- a/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx +++ b/vnpy/app/option_master/pricing/cython_model/binomial_tree_cython/binomial_tree_cython.pyx @@ -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,9 +277,9 @@ def calculate_impv( # Calculate guessed implied volatility of next round v += dx - # Check end result to be non-negative - if v <= 0: - return 0 + # Check new volatility to be non-negative + if v <= 0: + return 0 # Round to 4 decimal places v = round(v, 4) diff --git a/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx b/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx index cd290cbb..7cd37a6e 100644 --- a/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx +++ b/vnpy/app/option_master/pricing/cython_model/black_76_cython/black_76_cython.pyx @@ -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 diff --git a/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx index 6af59d7f..66977ba3 100644 --- a/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx +++ b/vnpy/app/option_master/pricing/cython_model/black_scholes_cython/black_scholes_cython.pyx @@ -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 diff --git a/vnpy/app/option_master/ui/manager.py b/vnpy/app/option_master/ui/manager.py index a4b59da2..cd9503b9 100644 --- a/vnpy/app/option_master/ui/manager.py +++ b/vnpy/app/option_master/ui/manager.py @@ -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,8 +933,11 @@ class PricingVolatilityManager(QtWidgets.QWidget): otm = chain.puts[index] value = round(otm.pricing_impv * 100, 1) - cells = self.cells[(chain_symbol, index)] - cells["pricing_impv"].setValue(value) + + 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: """""" diff --git a/vnpy/app/option_master/ui/monitor.py b/vnpy/app/option_master/ui/monitor.py index 69c8153f..e1aacc55 100644 --- a/vnpy/app/option_master/ui/monitor.py +++ b/vnpy/app/option_master/ui/monitor.py @@ -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): diff --git a/vnpy/app/option_master/ui/widget.py b/vnpy/app/option_master/ui/widget.py index c8ad9c6d..2ad56ef9 100644 --- a/vnpy/app/option_master/ui/widget.py +++ b/vnpy/app/option_master/ui/widget.py @@ -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}),可能导致来回频繁对冲!" diff --git a/vnpy/gateway/ctp/ctp_gateway.py b/vnpy/gateway/ctp/ctp_gateway.py index 75c9ee5a..2caa0ea6 100644 --- a/vnpy/gateway/ctp/ctp_gateway.py +++ b/vnpy/gateway/ctp/ctp_gateway.py @@ -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) diff --git a/vnpy/gateway/rohon/rohon_gateway.py b/vnpy/gateway/rohon/rohon_gateway.py index b0952edc..864930d8 100644 --- a/vnpy/gateway/rohon/rohon_gateway.py +++ b/vnpy/gateway/rohon/rohon_gateway.py @@ -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, "") diff --git a/vnpy/gateway/sopt/sopt_gateway.py b/vnpy/gateway/sopt/sopt_gateway.py index aa21b500..8bcd3593 100644 --- a/vnpy/gateway/sopt/sopt_gateway.py +++ b/vnpy/gateway/sopt/sopt_gateway.py @@ -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) diff --git a/vnpy/trader/utility.py b/vnpy/trader/utility.py index fef74bca..f40ad200 100644 --- a/vnpy/trader/utility.py +++ b/vnpy/trader/utility.py @@ -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: