[update] 期权api,gateway, vnpy 2.15 sync
This commit is contained in:
parent
4d15d355c6
commit
12a44cdfe2
@ -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.
@ -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()):
|
||||
|
@ -103,6 +103,7 @@ class AlgoTemplate:
|
||||
self.put_variables_event()
|
||||
|
||||
self.write_log("停止算法")
|
||||
return True
|
||||
|
||||
def subscribe(self, vt_symbol):
|
||||
""""""
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Binary file not shown.
@ -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
|
||||
|
Binary file not shown.
@ -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
|
||||
|
Binary file not shown.
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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}),可能导致来回频繁对冲!"
|
||||
|
@ -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)
|
||||
|
@ -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, "")
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user