Skip to content

Commit

Permalink
下单失败时发布 ORDER_CREATION_REJECT 事件 (#863)
Browse files Browse the repository at this point in the history
* develop

* developing

* in development

* in development

* pr update

* 修改AbstractFrontendValidate及其子类的abstractmethod

* revise annotation

* pr update

* pr update

* update version

* pr update

---------

Co-authored-by: Cuizi7 <Cuizi7@users.noreply.github.com>
  • Loading branch information
Lin-Dongzhao and Cuizi7 committed Apr 3, 2024
1 parent d889589 commit 8b865d9
Show file tree
Hide file tree
Showing 15 changed files with 175 additions and 168 deletions.
7 changes: 2 additions & 5 deletions rqalpha/apis/api_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,8 @@ def submit_order(id_or_ins, amount, side, price=None, position_effect=None):
style = cal_style(price, None)
market_price = env.get_last_price(order_book_id)
if not is_valid_price(market_price):
user_system_log.warn(
_(u"Order Creation Failed: [{order_book_id}] No market data").format(
order_book_id=order_book_id
)
)
reason = _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)
env.order_creation_failed(order_book_id, reason)
return

amount = int(amount)
Expand Down
41 changes: 32 additions & 9 deletions rqalpha/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
from datetime import datetime
from typing import Optional, Dict, List
from itertools import chain
from typing import TYPE_CHECKING

import rqalpha
from rqalpha.core.events import EventBus
from rqalpha.core.events import EventBus, Event, EVENT
from rqalpha.const import INSTRUMENT_TYPE
from rqalpha.utils.logger import system_log, user_log, user_system_log
from rqalpha.core.global_var import GlobalVars
from rqalpha.utils.i18n import gettext as _
if TYPE_CHECKING:
from rqalpha.model.order import Order


class Environment(object):
Expand Down Expand Up @@ -114,19 +117,32 @@ def _get_frontend_validators(self, instrument_type):
return chain(self._frontend_validators.get(instrument_type, []), self._default_frontend_validators)

def submit_order(self, order):
instrument_type = self.data_proxy.instrument(order.order_book_id).type
account = self.portfolio.get_account(order.order_book_id)
if all(v.can_submit_order(order, account) for v in self._get_frontend_validators(instrument_type)):
if self.can_submit_order(order):
self.broker.submit_order(order)
return order

def can_cancel_order(self, order):
instrument_type = self.data_proxy.instrument(order.order_book_id).type
account = self.portfolio.get_account(order.order_book_id)
for v in chain(self._frontend_validators.get(instrument_type, []), self._default_frontend_validators):
if not v.can_cancel_order(order, account):
return False
try:
reason = v.validate_cancellation(order, account)
if reason:
self.order_cancellation_failed(order_book_id=order.order_book_id, reason=reason)
return False
except NotImplementedError:
# 避免由于某些 mod 版本未更新,Validator method 未修改
if not v.can_cancel_order(order, account):
return False
return True

def order_creation_failed(self, order_book_id, reason):
user_system_log.warn(reason)
self.event_bus.publish_event(Event(EVENT.ORDER_CREATION_REJECT, order_book_id=order_book_id, reason=reason))

def order_cancellation_failed(self, order_book_id, reason):
user_system_log.warn(reason)
self.event_bus.publish_event(Event(EVENT.ORDER_CANCELLATION_REJECT, order_book_id=order_book_id, reason=reason))

def get_universe(self):
return self._universe.get()
Expand Down Expand Up @@ -179,11 +195,18 @@ def update_time(self, calendar_dt, trading_dt):
self.calendar_dt = calendar_dt
self.trading_dt = trading_dt

def can_submit_order(self, order):
def can_submit_order(self, order: 'Order') -> bool:
# forward compatible
instrument_type = self.data_proxy.instrument(order.order_book_id).type
account = self.portfolio.get_account(order.order_book_id)
for v in self._get_frontend_validators(instrument_type):
if not v.can_submit_order(order, account):
return False
try:
reason = v.validate_submission(order, account)
if reason:
self.order_creation_failed(order_book_id=order.order_book_id, reason=reason)
return False
except NotImplementedError:
# 避免由于某些 mod 版本未更新,Validator method 未修改
if not v.can_submit_order(order, account):
return False
return True
19 changes: 12 additions & 7 deletions rqalpha/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

import abc
from datetime import datetime, date
from typing import Any, Union, Optional, Iterable, Dict, List, Sequence
from typing import Any, Union, Optional, Iterable, Dict, List, Sequence, TYPE_CHECKING
if TYPE_CHECKING:
from rqalpha.portfolio.account import Account

import numpy
from six import with_metaclass
Expand Down Expand Up @@ -649,18 +651,21 @@ class AbstractFrontendValidator(with_metaclass(abc.ABCMeta)):
扩展模块可以通过 env.add_frontend_validator 添加自定义的前端风控逻辑
"""

@abc.abstractmethod
def can_submit_order(self, order, account=None):
def validate_submission(self, order: Order, account: Optional['Account'] = None) -> Optional[str]:
"""
判断是否可以下单
进行下单前的验证,若通过则返回 None
:return: `Optional[str]`
"""
raise NotImplementedError

@abc.abstractmethod
def can_cancel_order(self, order, account=None):
def validate_cancellation(self, order: Order, account: Optional['Account'] = None) -> Optional[str]:
"""
判读是否可以撤单
进行撤销订单前的验证,若通过则返回 None
:return: `Optional[str]`
"""
raise NotImplementedError

Expand Down
28 changes: 13 additions & 15 deletions rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ def _submit_order(id_or_ins, amount, side, position_effect, style):

amount = int(amount)
if amount == 0:
user_system_log.warn(_(
u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}"
).format(order_book_id=order_book_id))
reason = _(u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}").format(
order_book_id=order_book_id
)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
return None
if isinstance(style, LimitOrder) and np.isnan(style.get_limit_price()):
raise RQInvalidArgument(_(u"Limit order price should not be nan."))
Expand All @@ -62,34 +63,31 @@ def _submit_order(id_or_ins, amount, side, position_effect, style):

price = env.get_last_price(order_book_id)
if not is_valid_price(price):
user_system_log.warn(
_(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)
)
reason = _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
return

env = Environment.get_instance()

orders = []
if position_effect in (POSITION_EFFECT.CLOSE_TODAY, POSITION_EFFECT.CLOSE):
direction = POSITION_DIRECTION.LONG if side == SIDE.SELL else POSITION_DIRECTION.SHORT
position = env.portfolio.get_position(order_book_id, direction) # type: Position
if position_effect == POSITION_EFFECT.CLOSE_TODAY:
if amount > position.today_closable:
user_system_log.warning(_(
reason = _(
"Order Creation Failed: "
"close today amount {amount} is larger than today closable quantity {quantity}"
).format(amount=amount, quantity=position.today_closable))
"close today amount {amount} is larger than today closable quantity {quantity}").format(
amount=amount, quantity=position.today_closable)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
return []
orders.append(Order.__from_create__(
order_book_id, amount, side, style, POSITION_EFFECT.CLOSE_TODAY
))
else:
quantity, old_quantity = position.quantity, position.old_quantity
if amount > quantity:
user_system_log.warn(_(
u"Order Creation Failed: close amount {amount} is larger than position quantity {quantity}").format(
amount=amount, quantity=quantity
))
reason = _(u"Order Creation Failed: close amount {amount} is larger than position quantity {quantity}").format(
amount=amount, quantity=quantity)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
return []
if amount > old_quantity:
if old_quantity != 0:
Expand Down
45 changes: 20 additions & 25 deletions rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
INSTRUMENT_TYPE, ORDER_TYPE, POSITION_DIRECTION,
POSITION_EFFECT, SIDE)
from rqalpha.core.execution_context import ExecutionContext
from rqalpha.core.events import Event, EVENT
from rqalpha.environment import Environment
from rqalpha.mod.rqalpha_mod_sys_risk.validators.cash_validator import \
is_cash_enough
from rqalpha.mod.rqalpha_mod_sys_risk.validators.cash_validator import validate_cash
from rqalpha.model.instrument import IndustryCode as industry_code
from rqalpha.model.instrument import IndustryCodeItem, Instrument
from rqalpha.model.instrument import SectorCode as sector_code
Expand Down Expand Up @@ -100,23 +100,22 @@ def _submit_order(ins, amount, side, position_effect, style, current_quantity, a
raise RQInvalidArgument(_(u"Limit order price should not be nan."))
price = env.data_proxy.get_last_price(ins.order_book_id)
if not is_valid_price(price):
user_system_log.warn(
_(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=ins.order_book_id))
reason = _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=ins.order_book_id)
env.order_creation_failed(order_book_id=ins.order_book_id, reason=reason)
return

if (side == SIDE.BUY and current_quantity != -amount) or (side == SIDE.SELL and current_quantity != abs(amount)):
# 在融券回测中,需要用买单作为平空,对于此种情况下出现的碎股,亦允许一次性申报卖出
amount = _round_order_quantity(ins, amount)

if amount == 0:
user_system_log.warn(_(
u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}"
).format(order_book_id=ins.order_book_id))
reason = _(u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}").format(order_book_id=ins.order_book_id)
env.order_creation_failed(order_book_id=ins.order_book_id, reason=reason)
return
order = Order.__from_create__(ins.order_book_id, abs(amount), side, style, position_effect)
if side == SIDE.BUY and auto_switch_order_value:
account, position, ins = _get_account_position_ins(ins)
if not is_cash_enough(env, order, account.cash):
if validate_cash(env, order, account.cash):
user_system_log.warn(_(
"insufficient cash, use all remaining cash({}) to create order"
).format(account.cash))
Expand All @@ -129,7 +128,7 @@ def _order_shares(ins, amount, style, quantity, auto_switch_order_value):
return _submit_order(ins, amount, side, position_effect, style, quantity, auto_switch_order_value)


def _order_value(account, position, ins, cash_amount, style):
def _order_value(account, position, ins, cash_amount, style, zero_amount_as_exception=True):
env = Environment.get_instance()
if cash_amount > 0:
cash_amount = min(cash_amount, account.cash)
Expand All @@ -138,9 +137,8 @@ def _order_value(account, position, ins, cash_amount, style):
else:
price = env.data_proxy.get_last_price(ins.order_book_id)
if not is_valid_price(price):
user_system_log.warn(
_(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=ins.order_book_id)
)
reason = _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=ins.order_book_id)
env.order_creation_failed(order_book_id=ins.order_book_id, reason=reason)
return

amount = int(Decimal(cash_amount) / Decimal(price))
Expand All @@ -155,9 +153,9 @@ def _order_value(account, position, ins, cash_amount, style):
break
amount -= round_lot
else:
user_system_log.warn(_(
u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}"
).format(order_book_id=ins.order_book_id))
if zero_amount_as_exception:
reason = _(u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}").format(order_book_id=ins.order_book_id)
env.order_creation_failed(order_book_id=ins.order_book_id, reason=reason)
return

if amount < 0:
Expand Down Expand Up @@ -198,7 +196,7 @@ def stock_order_target_value(id_or_ins, cash_amount, price_or_style=None, price=
)
_delta = cash_amount - position.market_value
_style = open_style if _delta > 0 else close_style
return _order_value(account, position, ins, _delta, _style)
return _order_value(account, position, ins, _delta, _style, zero_amount_as_exception=False)


@order_target_percent.register(INST_TYPE_IN_STOCK_ACCOUNT)
Expand All @@ -211,7 +209,7 @@ def stock_order_target_percent(id_or_ins, percent, price_or_style=None, price=No
)
_delta = account.total_value * percent - position.market_value
_style = open_style if _delta > 0 else close_style
return _order_value(account, position, ins, _delta, _style)
return _order_value(account, position, ins, _delta, _style, zero_amount_as_exception=False)


@order.register(INST_TYPE_IN_STOCK_ACCOUNT)
Expand Down Expand Up @@ -342,9 +340,8 @@ def order_target_portfolio(
order_book_id = ins.order_book_id
last_price = env.data_proxy.get_last_price(order_book_id)
if not is_valid_price(last_price):
user_system_log.warn(
_(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)
)
reason = _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
continue

price_or_style = price_or_styles.get(ins.order_book_id)
Expand Down Expand Up @@ -380,12 +377,10 @@ def order_target_portfolio(
open_price = _get_order_style_price(order_book_id, open_style)
close_price = _get_order_style_price(order_book_id, close_style)
if not (is_valid_price(close_price) and is_valid_price(open_price)):
user_system_log.warn(_(
"Adjust position of {id_or_ins} Failed: "
"Invalid close/open price {close_price}/{open_price}").format(
id_or_ins=order_book_id, close_price=close_price, open_price=open_price
)
reason = _("Adjust position of {id_or_ins} Failed: Invalid close/open price {close_price}/{open_price}").format(
id_or_ins=order_book_id, close_price=close_price, open_price=open_price
)
env.order_creation_failed(order_book_id=order_book_id, reason=reason)
continue

delta_quantity = (account_value * target_percent / close_price) - current_quantities.get(order_book_id, 0)
Expand Down
24 changes: 9 additions & 15 deletions rqalpha/mod/rqalpha_mod_sys_accounts/component_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,18 @@ def __init__(self, margin_type="all"):
self._margin_type = margin_type
from rqalpha.apis.api_rqdatac import get_margin_stocks
self._get_margin_stocks = get_margin_stocks

def can_cancel_order(self, order, account=None):
return True

def can_submit_order(self, order, account=None):
# type: (Order, Optional[Account]) -> bool


def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
# 没负债等于没融资,则不需要限制股票池
if account.cash_liabilities == 0:
return True

return None
symbols = self._get_margin_stocks(margin_type=self._margin_type)

# 是否在股票池中
if order.order_book_id in set(symbols):
return True
return None
else:
user_system_log.warn("Order Creation Failed: margin stock pool not contains {}.".format(
order.order_book_id)
)
return False
reason = "Order Creation Failed: margin stock pool not contains {}.".format(order.order_book_id)
return reason

def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
return None
27 changes: 13 additions & 14 deletions rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,32 +27,31 @@


class PositionValidator(AbstractFrontendValidator):
def can_cancel_order(self, order, account=None):
return True

def can_submit_order(self, order, account=None):
# type: (Order, Optional[Account]) -> bool
def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
return None

def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]:
if account is None:
return True
return None
if order.position_effect in (POSITION_EFFECT.OPEN, POSITION_EFFECT.EXERCISE):
return True
return None
position = account.get_position(order.order_book_id, order.position_direction) # type: AbstractPosition
if order.position_effect == POSITION_EFFECT.CLOSE_TODAY and order.quantity > position.today_closable:
user_system_log.warn(_(
reason = _(
"Order Creation Failed: not enough today position {order_book_id} to close, target"
" quantity is {quantity}, closable today quantity is {closable}").format(
order_book_id=order.order_book_id,
quantity=order.quantity,
closable=position.today_closable,
))
return False
)
return reason
if order.position_effect == POSITION_EFFECT.CLOSE and order.quantity > position.closable:
user_system_log.warn(_(
reason = _(
"Order Creation Failed: not enough position {order_book_id} to close or exercise, target"
" sell quantity is {quantity}, closable quantity is {closable}").format(
order_book_id=order.order_book_id,
quantity=order.quantity,
closable=position.closable,
))
return False
return True
)
return reason
return None
Loading

0 comments on commit 8b865d9

Please sign in to comment.