Skip to content

Commit 1bc9a27

Browse files
authored
Merge pull request #998 from ricequant/develop
release 6.1.4
2 parents ef978b8 + a63ab78 commit 1bc9a27

13 files changed

Lines changed: 411 additions & 80 deletions

File tree

CHANGELOG.rst

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,30 @@
22
CHANGELOG
33
==================
44

5+
6.1.4
6+
==================
7+
8+
下单接口变更:
9+
- `order_target_portfolio_smart` API 支持 ETF/LOF/REITs 品种
10+
- `order_target_portfolio` API 支持 REITs 品种
11+
12+
涨跌停边界撮合修正:
13+
- 是否触及涨跌停判断修改为按 tick_size 做容差处理
14+
- 获取到的涨跌停价为 NaN 时,认为标的没有涨跌停限制
15+
16+
17+
6.1.3
18+
==================
19+
20+
- 修复 `all_instruments` 获取数据确实的问题
21+
22+
23+
6.1.2
24+
==================
25+
26+
- 修复更新 bundle 数据时,由于数据量过大导致断开与 rqdata 的连接
27+
28+
529
6.1.0
630
==================
731

rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ def order_lots(
319319

320320

321321
ORDER_TARGET_PORTFOLIO_SUPPORTED_INS_TYPES = {
322-
INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF, INSTRUMENT_TYPE.LOF, INSTRUMENT_TYPE.INDX, INSTRUMENT_TYPE.CONVERTIBLE
322+
INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF, INSTRUMENT_TYPE.LOF, INSTRUMENT_TYPE.INDX, INSTRUMENT_TYPE.CONVERTIBLE, INSTRUMENT_TYPE.REITs
323323
}
324324

325325

@@ -384,7 +384,7 @@ def order_target_portfolio(
384384
).format(id_or_ins, type(id_or_ins)))
385385
if ins.type not in ORDER_TARGET_PORTFOLIO_SUPPORTED_INS_TYPES:
386386
raise RQInvalidArgument(_(
387-
"function order_target_portfolio: invalid instrument type, excepted CS/ETF/LOF/INDX, got {}"
387+
"function order_target_portfolio: invalid instrument type, excepted CS/ETF/LOF/INDX/Convertible/REITs, got {}"
388388
).format(ins.order_book_id))
389389
order_book_id = ins.order_book_id
390390
last_price = env.data_proxy.get_last_price(order_book_id)

rqalpha/mod/rqalpha_mod_sys_accounts/api/order_target_portfolio.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from rqalpha.utils.exception import RQApiNotSupportedError, RQInvalidArgument
2929
from rqalpha.utils.functools import lru_cache
3030
from rqalpha.utils.i18n import gettext as _, lazy_gettext
31+
from rqalpha.utils.price_limits import reaches_limit_down_vectorized, reaches_limit_up_vectorized
3132

3233

3334
class AdjustingResult(NamedTuple):
@@ -72,6 +73,15 @@ class DenialReason(CommentedEnum):
7273
closable_exceeded = 'closable_exceeded', lazy_gettext('Order creation failed: insufficient closable position')
7374

7475

76+
SUPPORTED_INSTRUMENT_TYPES = {
77+
INSTRUMENT_TYPE.CS,
78+
INSTRUMENT_TYPE.CONVERTIBLE,
79+
INSTRUMENT_TYPE.ETF,
80+
INSTRUMENT_TYPE.LOF,
81+
INSTRUMENT_TYPE.REITs,
82+
}
83+
84+
7585
class OrderTargetPortfolio:
7686
def __init__(
7787
self,
@@ -101,10 +111,11 @@ def __init__(
101111

102112
instruments = env.data_proxy.get_active_instruments(index, env.trading_dt)
103113
for i in instruments.values():
104-
if i.type != INSTRUMENT_TYPE.CS:
114+
if i.type not in SUPPORTED_INSTRUMENT_TYPES:
105115
raise RQApiNotSupportedError(_('instrument type {} is not supported').format(i.type))
106116

107117
self._market = Series({i.order_book_id: i.market for i in instruments.values()}, dtype='object')
118+
self._tick_sizes = Series({i: env.data_proxy.get_tick_size(i) for i in index}, dtype=float)
108119
self._min_qty = Series(
109120
{i.order_book_id: i.min_order_quantity for i in instruments.values()},
110121
dtype='int64',
@@ -136,25 +147,19 @@ def __init__(
136147
else:
137148
prev_date = env.data_proxy.get_previous_trading_date(env.trading_dt)
138149
for order_book_id in index:
139-
bars = env.data_proxy.history_bars(order_book_id, 1, '1d', ['close'], prev_date)
140-
if bars is None:
150+
bar = env.data_proxy.get_bar(order_book_id, prev_date, '1d')
151+
if bar.isnan:
141152
raise RuntimeError('missing valuation prices: {}'.format(order_book_id))
142-
self._prices.loc[order_book_id, 'last'] = bars['close'][0]
153+
self._prices.loc[order_book_id, 'last'] = bar.close
143154
elif phase == EXECUTION_PHASE.ON_BAR:
144155
if env.config.base.frequency == '1d':
145156
# TODO:根据算法时间选择最近的分钟线作为估值
146157
# 当前先选择开盘价
147158
for order_book_id in index:
148-
bars = env.data_proxy.history_bars(
149-
order_book_id,
150-
1,
151-
'1d',
152-
['open', 'limit_up', 'limit_down'],
153-
env.trading_dt,
154-
)
155-
if bars is None:
159+
bar = env.data_proxy.get_bar(order_book_id, env.trading_dt, '1d')
160+
if bar.isnan:
156161
raise RuntimeError('missing valuation prices: {}'.format(order_book_id))
157-
self._prices.loc[order_book_id] = list(bars[0])
162+
self._prices.loc[order_book_id] = [bar.open, bar.limit_up, bar.limit_down]
158163
if valuation_prices is not None:
159164
self._prices['last'] = valuation_prices
160165
elif env.config.base.frequency == '1m':
@@ -212,8 +217,8 @@ def _calc_adjusting(
212217
denials[DenialReason.suspended_sell] = (diff < 0) & self._suspended
213218
denials[DenialReason.no_price] = prices.isna() & (diff != 0)
214219

215-
limit_up = ~(limit_up.isna()) & (prices >= limit_up)
216-
limit_down = ~(limit_down.isna()) & (prices <= limit_down)
220+
limit_up = reaches_limit_up_vectorized(prices, limit_up, self._tick_sizes)
221+
limit_down = reaches_limit_down_vectorized(prices, limit_down, self._tick_sizes)
217222
if direction == POSITION_DIRECTION.LONG:
218223
# 涨停不能开、跌停不能平
219224
limit_buy = (diff > 0) & limit_up
@@ -304,6 +309,7 @@ def __call__(self, direction: POSITION_DIRECTION = POSITION_DIRECTION.LONG) -> A
304309
safety -= min(max(proportion_diff / 10, 0.0001), 0.002)
305310
return AdjustingResult(adjustments=last_diff, denials=self._format_denials(last_denials))
306311

312+
307313
@export_as_api
308314
@ExecutionContext.enforce_phase(
309315
EXECUTION_PHASE.OPEN_AUCTION,

rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
from rqalpha.const import ORDER_TYPE, POSITION_EFFECT
1818
from rqalpha.model.order import Order
1919
from rqalpha.portfolio.account import Account
20-
from rqalpha.utils.logger import user_system_log
2120

2221
from rqalpha.utils.i18n import gettext as _
2322

@@ -29,9 +28,10 @@ def __init__(self, env):
2928
def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
3029
if (order.type != ORDER_TYPE.LIMIT) or (order.position_effect == POSITION_EFFECT.EXERCISE):
3130
return None
32-
31+
3332
# FIXME: it may be better to round price in data source
3433
limit_up = round(self._env.price_board.get_limit_up(order.order_book_id), 4)
34+
# 此处不使用 price_limits.reaches_limit,以为事前风控宜松不宜紧,要对挂单做无罪推定。
3535
if order.price > limit_up:
3636
reason = _(
3737
"Order Creation Failed: limit order price {limit_price} is higher "
@@ -57,4 +57,4 @@ def validate_submission(self, order: Order, account: Optional[Account] = None) -
5757
return None
5858

5959
def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
60-
return None
60+
return None

rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
# 详细的授权流程,请联系 public@ricequant.com 获取。
1717
import datetime
1818
from collections import defaultdict
19-
import math
2019
from rqalpha.const import MATCHING_TYPE, ORDER_TYPE, POSITION_EFFECT, SIDE
2120
from rqalpha.environment import Environment
2221
from rqalpha.core.events import EVENT, Event
@@ -25,26 +24,12 @@
2524
from rqalpha.model.tick import TickObject
2625
from rqalpha.portfolio.account import Account
2726
from rqalpha.utils import is_valid_price
28-
from rqalpha.interface import AbstractPriceBoard
27+
from rqalpha.utils.price_limits import reaches_limit
2928
from typing import Dict
3029
from rqalpha.utils.i18n import gettext as _
3130
from .slippage import SlippageDecider
3231

3332

34-
LIMIT_PRICE_VALID_THRESHOLD = 1e-7
35-
36-
37-
def _price_reaches_limit(order_book_id: str, side: SIDE, deal_price: float, price_board: AbstractPriceBoard):
38-
if side == SIDE.BUY:
39-
limit_price = price_board.get_limit_up(order_book_id)
40-
return deal_price >= limit_price or math.isclose(deal_price, limit_price, abs_tol=LIMIT_PRICE_VALID_THRESHOLD)
41-
elif side == SIDE.SELL:
42-
limit_price = price_board.get_limit_down(order_book_id)
43-
return deal_price <= limit_price or math.isclose(deal_price, limit_price, abs_tol=LIMIT_PRICE_VALID_THRESHOLD)
44-
else:
45-
raise ValueError(f"Unsupport side: {side}")
46-
47-
4833
class AbstractMatcher:
4934
def match(self, account, order, open_auction):
5035
# type: (Account, Order, bool) -> None
@@ -125,6 +110,7 @@ def match(self, account, order, open_auction):
125110
raise NotImplementedError
126111
order_book_id = order.order_book_id
127112
instrument = self._env.get_instrument(order_book_id)
113+
tick_size = self._env.data_proxy.get_tick_size(order_book_id)
128114

129115
deal_price = self._get_deal_price(order, open_auction)
130116

@@ -154,11 +140,11 @@ def match(self, account, order, open_auction):
154140
return
155141
# 是否限制涨跌停不成交
156142
if self._price_limit:
157-
if _price_reaches_limit(order_book_id, order.side, deal_price, price_board):
143+
if reaches_limit(order_book_id, deal_price, order.side, price_board, tick_size):
158144
return
159145
else:
160146
if self._price_limit:
161-
if _price_reaches_limit(order_book_id, order.side, deal_price, price_board):
147+
if reaches_limit(order_book_id, deal_price, order.side, price_board, tick_size):
162148
reason = _(
163149
"Order Cancelled: current bar [{order_book_id}] reach the {limit_up_or_down} price."
164150
).format(order_book_id=order.order_book_id, limit_up_or_down="limit_up" if order.side == SIDE.BUY else "limit_down")
@@ -330,6 +316,7 @@ def match(self, account, order, open_auction): # type: (Account, Order, bool) -
330316
# 标的信息
331317
order_book_id = order.order_book_id
332318
instrument = self._env.get_instrument(order_book_id)
319+
tick_size = self._env.data_proxy.get_tick_size(order_book_id)
333320

334321
# 获取tick数据
335322
_cur_tick = self._cur_tick.get(order_book_id)
@@ -375,7 +362,7 @@ def match(self, account, order, open_auction): # type: (Account, Order, bool) -
375362
return
376363
# 是否限制涨跌停不成交
377364
if self._price_limit:
378-
if _price_reaches_limit(order_book_id, order.side, deal_price, price_board):
365+
if reaches_limit(order_book_id, deal_price, order.side, price_board, tick_size):
379366
return
380367
if self._liquidity_limit:
381368
if order.side == SIDE.BUY and price_board.get_a1(order_book_id) == 0:
@@ -384,7 +371,7 @@ def match(self, account, order, open_auction): # type: (Account, Order, bool) -
384371
return
385372
else:
386373
if self._price_limit:
387-
if _price_reaches_limit(order_book_id, order.side, deal_price, price_board):
374+
if reaches_limit(order_book_id, deal_price, order.side, price_board, tick_size):
388375
reason = _(
389376
"Order Cancelled: current tick [{order_book_id}] reach the {limit_up_or_down} price."
390377
).format(order_book_id=order.order_book_id, limit_up_or_down="limit_up" if order.side == SIDE.BUY else "limit_down")

rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from rqalpha.utils.logger import user_system_log
2525
from rqalpha.utils.i18n import gettext as _
2626
from rqalpha.utils import is_valid_price
27+
from rqalpha.utils.price_limits import reaches_limit
2728
from rqalpha.core.events import EVENT, Event
2829
from rqalpha.model.trade import Trade
2930
from rqalpha.model.order import ALGO_ORDER_STYLES
@@ -59,6 +60,7 @@ def cancel_order(self, order):
5960
def _match(self, account, order):
6061
order_book_id = order.order_book_id
6162
price_board = self._env.price_board
63+
tick_size = self._env.data_proxy.get_tick_size(order_book_id)
6264

6365
last_price = price_board.get_last_price(order_book_id)
6466

@@ -91,19 +93,19 @@ def _match(self, account, order):
9193

9294
if self._price_limit:
9395
if order.position_effect != POSITION_EFFECT.EXERCISE:
94-
if order.side == SIDE.BUY and deal_price >= price_board.get_limit_up(order_book_id):
96+
if reaches_limit(
97+
order_book_id,
98+
deal_price,
99+
order.side,
100+
price_board,
101+
tick_size,
102+
):
95103
order.mark_rejected(_(
96-
"Order Cancelled: current bar [{order_book_id}] reach the limit_up price."
97-
).format(order_book_id=order.order_book_id))
98-
self._env.event_bus.publish_event(Event(
99-
EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=copy(order)
104+
"Order Cancelled: current bar [{order_book_id}] reach the {limit_up_or_down} price."
105+
).format(
106+
order_book_id=order.order_book_id,
107+
limit_up_or_down="limit_up" if order.side == SIDE.BUY else "limit_down",
100108
))
101-
return
102-
103-
if order.side == SIDE.SELL and deal_price <= price_board.get_limit_down(order_book_id):
104-
order.mark_rejected(_(
105-
"Order Cancelled: current bar [{order_book_id}] reach the limit_down price."
106-
).format(order_book_id=order.order_book_id))
107109
self._env.event_bus.publish_event(Event(
108110
EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=copy(order)
109111
))

rqalpha/model/instrument.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -397,8 +397,10 @@ def min_order_quantity(self):
397397
@cached_property
398398
def order_step_size(self):
399399
# 下单递进股数
400-
if self.board_type == "KSH" or self.board_type == "BJS":
401-
return 1
400+
if self.type == INSTRUMENT_TYPE.CS:
401+
# 对于其他资产类型 instrument 中(如可转债)可能没有 board_type 字段
402+
if self.board_type == "KSH" or self.board_type == "BJS":
403+
return 1
402404
return self.round_lot
403405

404406
def during_call_auction(self, dt):
@@ -445,7 +447,7 @@ def tick_size(self):
445447
# type: () -> float
446448
if self.type in (INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.INDX):
447449
return 0.01
448-
elif self.type in ("ETF", "LOF"):
450+
elif self.type in ("ETF", "LOF", "REITs"):
449451
return 0.001
450452
elif self.type == INSTRUMENT_TYPE.FUTURE:
451453
return self._futures_tick_size_getter(self)

rqalpha/utils/price_limits.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from typing import Union
2+
3+
from pandas import Series
4+
5+
from rqalpha.const import SIDE
6+
from rqalpha.interface import AbstractPriceBoard
7+
from rqalpha.utils import is_valid_price
8+
9+
PriceLike = Union[int, float]
10+
PRICE_LIMIT_TOLERANCE = 1e-6
11+
12+
13+
def reaches_limit_up(price: PriceLike, limit_up: PriceLike, tick_size: PriceLike) -> bool:
14+
if not (is_valid_price(price) and is_valid_price(limit_up)):
15+
return False
16+
return price >= limit_up - tick_size + PRICE_LIMIT_TOLERANCE
17+
18+
19+
def reaches_limit_down(price: PriceLike, limit_down: PriceLike, tick_size: PriceLike) -> bool:
20+
if not (is_valid_price(price) and is_valid_price(limit_down)):
21+
return False
22+
return price <= limit_down + tick_size - PRICE_LIMIT_TOLERANCE
23+
24+
25+
def reaches_limit(
26+
order_book_id: str,
27+
price: PriceLike,
28+
side: SIDE,
29+
price_board: AbstractPriceBoard,
30+
tick_size: PriceLike,
31+
) -> bool:
32+
if side == SIDE.BUY:
33+
return reaches_limit_up(price, price_board.get_limit_up(order_book_id), tick_size)
34+
if side == SIDE.SELL:
35+
return reaches_limit_down(price, price_board.get_limit_down(order_book_id), tick_size)
36+
raise ValueError("unsupport side: {}".format(side))
37+
38+
39+
def _is_valid_price_series(values: Series) -> Series:
40+
return values.notna() & values.gt(0)
41+
42+
43+
def reaches_limit_up_vectorized(prices: Series, limit_up: Series, tick_sizes: Series) -> Series:
44+
valid_price = _is_valid_price_series(prices)
45+
valid_limit = _is_valid_price_series(limit_up)
46+
47+
thresholds = limit_up - tick_sizes + PRICE_LIMIT_TOLERANCE
48+
return valid_price & valid_limit & prices.ge(thresholds)
49+
50+
51+
def reaches_limit_down_vectorized(prices: Series, limit_down: Series, tick_sizes: Series) -> Series:
52+
valid_price = _is_valid_price_series(prices)
53+
valid_limit = _is_valid_price_series(limit_down)
54+
55+
thresholds = limit_down + tick_sizes - PRICE_LIMIT_TOLERANCE
56+
return valid_price & valid_limit & prices.le(thresholds)

0 commit comments

Comments
 (0)