From 395faf389e7812e6b69bdc7005052e57c4f03ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=98=89=E4=BF=8A?= <654181984@qq.com> Date: Fri, 26 May 2023 17:01:54 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=8D=95:=20TVWPOrder=20=E5=92=8C=20VWAPOrder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rqalpha/apis/api_abstract.py | 95 +++++--- rqalpha/apis/api_base.py | 44 ++-- rqalpha/const.py | 6 + rqalpha/data/base_data_source/data_source.py | 3 + rqalpha/data/data_proxy.py | 10 + rqalpha/interface.py | 4 + .../api/api_future.py | 43 ++-- .../rqalpha_mod_sys_accounts/api/api_stock.py | 209 +++++++++--------- .../mod/rqalpha_mod_sys_simulation/matcher.py | 35 ++- rqalpha/mod/rqalpha_mod_sys_simulation/mod.py | 3 + .../__init__.py | 4 +- .../rqalpha_mod_sys_transaction_cost/mod.py | 2 +- rqalpha/model/order.py | 39 +++- rqalpha/utils/__init__.py | 5 + rqalpha/utils/arg_checker.py | 17 +- tests/api_tests/test_api_stock.py | 17 +- 16 files changed, 341 insertions(+), 195 deletions(-) diff --git a/rqalpha/apis/api_abstract.py b/rqalpha/apis/api_abstract.py index 154507ae9..ea2d2f806 100644 --- a/rqalpha/apis/api_abstract.py +++ b/rqalpha/apis/api_abstract.py @@ -15,17 +15,30 @@ # 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。 # 详细的授权流程,请联系 public@ricequant.com 获取。 -from typing import Union, Optional, List +from typing import Union, Optional, List, Tuple from rqalpha.api import export_as_api from rqalpha.core.execution_context import ExecutionContext from rqalpha.const import EXECUTION_PHASE from rqalpha.model.instrument import Instrument -from rqalpha.model.order import MarketOrder, LimitOrder, OrderStyle, Order +from rqalpha.model.order import MarketOrder, LimitOrder, OrderStyle, Order, ALL_ORDER_STYPES from rqalpha.utils.arg_checker import apply_rules, verify_that from rqalpha.utils.functools import instype_singledispatch +common_rules = ( + verify_that('price', pre_check=True).deprecated("please use price_or_style instead.").is_greater_than(0), + verify_that('style', pre_check=True).deprecated("please use price_or_style instead.").is_instance_of( + (*ALL_ORDER_STYPES, type(None)) + ), + verify_that("price_or_style").is_instance_of((float, type(None), *ALL_ORDER_STYPES)), +) + + +PRICE_OR_STYLE_TYPE = Union[float, OrderStyle, None] +TUPLE_PRICE_OR_STYLE_TYPE = Union[float, OrderStyle, None, Tuple[PRICE_OR_STYLE_TYPE, PRICE_OR_STYLE_TYPE]] + + @export_as_api @ExecutionContext.enforce_phase( EXECUTION_PHASE.OPEN_AUCTION, @@ -36,11 +49,11 @@ ) @apply_rules( verify_that('amount').is_number(), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def order_shares(id_or_ins, amount, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> Optional[Order] +def order_shares(id_or_ins, amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 指定股数的买/卖单,最常见的落单方式之一。如有需要落单类型当做一个参量传入,如果忽略掉落单类型,那么默认是市价单(market order)。 @@ -48,6 +61,7 @@ def order_shares(id_or_ins, amount, price=None, style=None): :param amount: 下单量, 正数代表买入,负数代表卖出。将会根据一手xx股来向下调整到一手的倍数,比如中国A股就是调整成100股的倍数。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -73,11 +87,11 @@ def order_shares(id_or_ins, amount, price=None, style=None): ) @apply_rules( verify_that('cash_amount').is_number(), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def order_value(id_or_ins, cash_amount, price=None, style=None): - # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle]) -> Optional[Order] +def order_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 使用想要花费的金钱买入/卖出股票,而不是买入/卖出想要的股数,正数代表买入,负数代表卖出。股票的股数总是会被调整成对应的100的倍数(在A中国A股市场1手是100股)。 如果资金不足,该API将会使用最大可用资金发单。 @@ -90,6 +104,7 @@ def order_value(id_or_ins, cash_amount, price=None, style=None): :param cash_amount: 需要花费现金购买/卖出证券的数目。正数代表买入,负数代表卖出。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -113,11 +128,11 @@ def order_value(id_or_ins, cash_amount, price=None, style=None): ) @apply_rules( verify_that('percent', pre_check=True).is_number().is_greater_or_equal_than(-1).is_less_or_equal_than(1), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def order_percent(id_or_ins, percent, price=None, style=None): - # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle]) -> Optional[Order] +def order_percent(id_or_ins, percent, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 发送一个花费价值等于目前投资组合(市场价值和目前现金的总和)一定百分比现金的买/卖单,正数代表买,负数代表卖。股票的股数总是会被调整成对应的一手的股票数的倍数(1手是100股)。百分比是一个小数,并且小于或等于1(<=100%),0.5表示的是50%.需要注意,如果资金不足,该API将会使用最大可用资金发单。 @@ -130,6 +145,7 @@ def order_percent(id_or_ins, percent, price=None, style=None): :param percent: 占有现有的投资组合价值的百分比。正数表示买入,负数表示卖出。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -151,11 +167,11 @@ def order_percent(id_or_ins, percent, price=None, style=None): ) @apply_rules( verify_that('cash_amount').is_number(), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def order_target_value(id_or_ins, cash_amount, price=None, style=None): - # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle]) -> Optional[Order] +def order_target_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], TUPLE_PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 买入/卖出并且自动调整该证券的仓位到一个目标价值。 加仓时,cash_amount 代表现有持仓的价值加上即将花费(包含税费)的现金的总价值。 @@ -167,6 +183,7 @@ def order_target_value(id_or_ins, cash_amount, price=None, style=None): :param cash_amount: 最终的该证券的仓位目标价值。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -188,11 +205,11 @@ def order_target_value(id_or_ins, cash_amount, price=None, style=None): ) @apply_rules( verify_that('percent', pre_check=True).is_number().is_greater_or_equal_than(0).is_less_or_equal_than(1), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def order_target_percent(id_or_ins, percent, price=None, style=None): - # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle]) -> Optional[Order] +def order_target_percent(id_or_ins, percent, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], TUPLE_PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 买入/卖出证券以自动调整该证券的仓位到占有一个目标价值。 @@ -213,6 +230,7 @@ def order_target_percent(id_or_ins, percent, price=None, style=None): :param percent: 仓位最终所占投资组合总价值的目标百分比。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -234,11 +252,11 @@ def order_target_percent(id_or_ins, percent, price=None, style=None): ) @apply_rules( verify_that('amount', pre_check=True).is_number().is_greater_or_equal_than(0), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def buy_open(id_or_ins, amount, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> Union[Order, List[Order], None] +def buy_open(id_or_ins, amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Union[Order, List[Order], None] """ 买入开仓。 @@ -246,6 +264,7 @@ def buy_open(id_or_ins, amount, price=None, style=None): :param amount: 下单手数 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -267,11 +286,11 @@ def buy_open(id_or_ins, amount, price=None, style=None): ) @apply_rules( verify_that('amount', pre_check=True).is_number().is_greater_or_equal_than(0), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def buy_close(id_or_ins, amount, price=None, style=None, close_today=False): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], Optional[bool]) -> Union[Order, List[Order], None] +def buy_close(id_or_ins, amount, price=None, style=None, price_or_style=None, close_today=False): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE, Optional[bool]) -> Union[Order, List[Order], None] """ 平卖仓 @@ -280,6 +299,7 @@ def buy_close(id_or_ins, amount, price=None, style=None, close_today=False): :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` :param close_today: 是否指定发平今仓单,默认为False,发送平仓单 + :param price_or_style: 原参数price和style的整合 :example: @@ -301,11 +321,11 @@ def buy_close(id_or_ins, amount, price=None, style=None, close_today=False): ) @apply_rules( verify_that('amount', pre_check=True).is_number().is_greater_or_equal_than(0), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def sell_open(id_or_ins, amount, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> Union[Order, List[Order], None] +def sell_open(id_or_ins, amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Union[Order, List[Order], None] """ 卖出开仓 @@ -313,6 +333,7 @@ def sell_open(id_or_ins, amount, price=None, style=None): :param amount: 下单手数 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 """ raise NotImplementedError @@ -327,11 +348,11 @@ def sell_open(id_or_ins, amount, price=None, style=None): ) @apply_rules( verify_that('amount', pre_check=True).is_number().is_greater_or_equal_than(0), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None))) + *common_rules ) @instype_singledispatch -def sell_close(id_or_ins, amount, price=None, style=None, close_today=False): - # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], Optional[bool]) -> Union[Order, List[Order], None] +def sell_close(id_or_ins, amount, price=None, style=None, price_or_style=None, close_today=False): + # type: (Union[str, Instrument], float, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE, Optional[bool]) -> Union[Order, List[Order], None] """ 平买仓 @@ -340,15 +361,16 @@ def sell_close(id_or_ins, amount, price=None, style=None, close_today=False): :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` :param close_today: 是否指定发平今仓单,默认为False,发送平仓单 + :param price_or_style: 原参数price和style的整合 """ raise NotImplementedError @export_as_api -@apply_rules(verify_that("quantity").is_number()) +@apply_rules(verify_that("quantity").is_number(), *common_rules) @instype_singledispatch -def order(order_book_id, quantity, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> List[Order] +def order(order_book_id, quantity, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> List[Order] """ 全品种通用智能调仓函数 @@ -366,6 +388,7 @@ def order(order_book_id, quantity, price=None, style=None): :param quantity: 调仓量 :param price: 下单价格 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -384,10 +407,10 @@ def order(order_book_id, quantity, price=None, style=None): @export_as_api -@apply_rules(verify_that("quantity").is_number()) +@apply_rules(verify_that("quantity").is_number(), *common_rules) @instype_singledispatch -def order_to(order_book_id, quantity, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> List[Order] +def order_to(order_book_id, quantity, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> List[Order] """ 全品种通用智能调仓函数 @@ -405,6 +428,8 @@ def order_to(order_book_id, quantity, price=None, style=None): :param int quantity: 调仓量 :param float price: 下单价格 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 + :example: .. code-block:: python3 diff --git a/rqalpha/apis/api_base.py b/rqalpha/apis/api_base.py index 722a0e638..4cfd37535 100644 --- a/rqalpha/apis/api_base.py +++ b/rqalpha/apis/api_base.py @@ -71,26 +71,40 @@ def assure_order_book_id(id_or_ins): return assure_instrument(id_or_ins).order_book_id -def cal_style(price, style): - if price is None and style is None: +def cal_style(price, style, price_or_style=None): + if price_or_style is None: + if price: + price_or_style = price + if style: + price_or_style = style + + if price_or_style is None: return MarketOrder() - if style is not None: - if not isinstance(style, OrderStyle): - raise RuntimeError - return style + if not isinstance(price_or_style, (int, float, OrderStyle)): + raise RQInvalidArgument(f"price or style or price_or_style type no support. {price_or_style}") - if isinstance(price, OrderStyle): - # 为了 order_xxx('RB1710', 10, MarketOrder()) 这种写法 - if isinstance(price, LimitOrder): - if np.isnan(price.get_limit_price()): - raise RQInvalidArgument(_(u"Limit order price should not be nan.")) - return price + if isinstance(price_or_style, OrderStyle): + return price_or_style - if np.isnan(price): - raise RQInvalidArgument(_(u"Limit order price should not be nan.")) + return LimitOrder(price_or_style) - return LimitOrder(price) + +def calc_open_close_style(price, style, price_or_style): + if isinstance(price_or_style, tuple): + _length = len(price_or_style) + if _length == 0: + o, c = None, None + elif _length == 1: + o, c = price_or_style[0], price_or_style[0] + else: + o, c = price_or_style[0], price_or_style[1] + open_style = cal_style(price, style, o) + close_style = cal_style(price, style, c) + else: + open_style = cal_style(price, style, price_or_style) + close_style = open_style + return open_style, close_style @export_as_api diff --git a/rqalpha/const.py b/rqalpha/const.py index be92a3bc8..2eba03a2e 100644 --- a/rqalpha/const.py +++ b/rqalpha/const.py @@ -94,6 +94,12 @@ class MATCHING_TYPE(CustomEnum): class ORDER_TYPE(CustomEnum): MARKET = "MARKET" LIMIT = "LIMIT" + ALGO = "ALGO" + + +class ALGO(CustomEnum): + TWAP = "TWAP" + VWAP = "VWAP" # noinspection PyPep8Naming diff --git a/rqalpha/data/base_data_source/data_source.py b/rqalpha/data/base_data_source/data_source.py index ba0e9fc98..fe42cc091 100644 --- a/rqalpha/data/base_data_source/data_source.py +++ b/rqalpha/data/base_data_source/data_source.py @@ -367,3 +367,6 @@ def get_merge_ticks(self, order_book_id_list, trading_date, last_dt=None): def history_ticks(self, instrument, count, dt): raise NotImplementedError + + def get_algo_bar(self, id_or_ins, start_min, end_min, dt): + raise NotImplementedError("open source rqalpha not support algo order") diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index f7e416e29..8962f3edf 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -303,3 +303,13 @@ def get_trading_period(self, sym_or_ids, default_trading_period=None): def is_night_trading(self, sym_or_ids): # type: (StrOrIter) -> bool return any((instrument.trade_at_night for instrument in self.instruments(sym_or_ids))) + + def get_algo_bar(self, id_or_ins, order_style, dt): + if not isinstance(id_or_ins, Instrument): + id_or_ins = self.instrument(id_or_ins) + if id_or_ins is None: + return np.nan, 0 + bar = self._data_source.get_algo_bar(id_or_ins, order_style.start_min, order_style.end_min, dt) + return (bar[order_style.TYPE], bar["volume"]) if bar else (np.nan, 0) + + diff --git a/rqalpha/interface.py b/rqalpha/interface.py index 3519069f5..a6bd1ca49 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -445,6 +445,10 @@ def is_st_stock(self, order_book_id, dates): # type: (str, Sequence[DateLike]) -> Sequence[bool] raise NotImplementedError + def get_algo_bar(self, id_or_ins, start_min, end_min, dt): + # type: (Union[str, Instrument], int, int, datetime) -> Optional[numpy.ndarray] + raise NotImplementedError + class AbstractBroker(with_metaclass(abc.ABCMeta)): """ diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py index d5afb76c7..d5de40b4f 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py @@ -18,13 +18,15 @@ from __future__ import division from typing import Union, Optional, List +import numpy as np + from rqalpha.api import export_as_api from rqalpha.apis.api_base import assure_instrument from rqalpha.apis.api_abstract import order, order_to, buy_open, buy_close, sell_open, sell_close from rqalpha.apis.api_base import cal_style from rqalpha.apis.api_rqdatac import futures from rqalpha.environment import Environment -from rqalpha.model.order import Order, LimitOrder, OrderStyle +from rqalpha.model.order import Order, LimitOrder, OrderStyle, ALGO_ORDER_STYLES from rqalpha.const import SIDE, POSITION_EFFECT, ORDER_TYPE, RUN_TYPE, INSTRUMENT_TYPE, POSITION_DIRECTION from rqalpha.model.instrument import Instrument from rqalpha.portfolio.position import Position @@ -35,8 +37,7 @@ from rqalpha.utils.arg_checker import apply_rules, verify_that -__all__ = [ -] +__all__ = [] def _submit_order(id_or_ins, amount, side, position_effect, style): @@ -50,9 +51,8 @@ def _submit_order(id_or_ins, amount, side, position_effect, style): u"Order Creation Failed: 0 order quantity, order_book_id={order_book_id}" ).format(order_book_id=order_book_id)) return None - if isinstance(style, LimitOrder) and style.get_limit_price() <= 0: - raise RQInvalidArgument(_(u"Limit order price should be positive")) - + if isinstance(style, LimitOrder) and np.isnan(style.get_limit_price()): + raise RQInvalidArgument(_(u"Limit order price should not be nan.")) if env.config.base.run_type != RUN_TYPE.BACKTEST and instrument.type == INSTRUMENT_TYPE.FUTURE: if "88" in order_book_id: @@ -61,7 +61,8 @@ def _submit_order(id_or_ins, amount, side, position_effect, style): raise RQInvalidArgument(_(u"Index Future contracts[99] are not supported in paper trading.")) price = env.get_last_price(order_book_id) - if not is_valid_price(price): + algo_price = env.data_proxy.get_algo_bar(instrument, style, env.calendar_dt)[0] if isinstance(style, ALGO_ORDER_STYLES) else 1 + if not (is_valid_price(price) and is_valid_price(algo_price)): user_system_log.warn( _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=order_book_id) ) @@ -176,37 +177,37 @@ def _order(order_book_id, quantity, style, target): @order.register(INSTRUMENT_TYPE.FUTURE) -def future_order(order_book_id, quantity, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> List[Order] - return _order(order_book_id, quantity, cal_style(price, style), False) +def future_order(order_book_id, quantity, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], Optional[Union[float, OrderStyle]]) -> List[Order] + return _order(order_book_id, quantity, cal_style(price, style, price_or_style), False) @order_to.register(INSTRUMENT_TYPE.FUTURE) -def future_order_to(order_book_id, quantity, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> List[Order] - return _order(order_book_id, quantity, cal_style(price, style), True) +def future_order_to(order_book_id, quantity, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], Optional[Union[float, OrderStyle]]) -> List[Order] + return _order(order_book_id, quantity, cal_style(price, style, price_or_style), True) @buy_open.register(INSTRUMENT_TYPE.FUTURE) -def future_buy_open(id_or_ins, amount, price=None, style=None): - return _submit_order(id_or_ins, amount, SIDE.BUY, POSITION_EFFECT.OPEN, cal_style(price, style)) +def future_buy_open(id_or_ins, amount, price=None, style=None, price_or_style=None): + return _submit_order(id_or_ins, amount, SIDE.BUY, POSITION_EFFECT.OPEN, cal_style(price, style, price_or_style)) @buy_close.register(INSTRUMENT_TYPE.FUTURE) -def future_buy_close(id_or_ins, amount, price=None, style=None, close_today=False): +def future_buy_close(id_or_ins, amount, price=None, style=None, close_today=False, price_or_style=None): position_effect = POSITION_EFFECT.CLOSE_TODAY if close_today else POSITION_EFFECT.CLOSE - return _submit_order(id_or_ins, amount, SIDE.BUY, position_effect, cal_style(price, style)) + return _submit_order(id_or_ins, amount, SIDE.BUY, position_effect, cal_style(price, style, price_or_style)) @sell_open.register(INSTRUMENT_TYPE.FUTURE) -def future_sell_open(id_or_ins, amount, price=None, style=None): - return _submit_order(id_or_ins, amount, SIDE.SELL, POSITION_EFFECT.OPEN, cal_style(price, style)) +def future_sell_open(id_or_ins, amount, price=None, style=None, price_or_style=None): + return _submit_order(id_or_ins, amount, SIDE.SELL, POSITION_EFFECT.OPEN, cal_style(price, style, price_or_style)) @sell_close.register(INSTRUMENT_TYPE.FUTURE) -def future_sell_close(id_or_ins, amount, price=None, style=None, close_today=False): +def future_sell_close(id_or_ins, amount, price=None, style=None, close_today=False, price_or_style=None): position_effect = POSITION_EFFECT.CLOSE_TODAY if close_today else POSITION_EFFECT.CLOSE - return _submit_order(id_or_ins, amount, SIDE.SELL, position_effect, cal_style(price, style)) + return _submit_order(id_or_ins, amount, SIDE.SELL, position_effect, cal_style(price, style, price_or_style)) @export_as_api diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py index d064697d5..1acac6c0c 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py @@ -26,9 +26,10 @@ from rqalpha.apis.api_abstract import (order, order_percent, order_shares, order_target_percent, order_target_value, order_to, - order_value) + order_value, + common_rules, TUPLE_PRICE_OR_STYLE_TYPE, PRICE_OR_STYLE_TYPE) from rqalpha.apis.api_base import (assure_instrument, assure_order_book_id, - cal_style) + cal_style, calc_open_close_style) from rqalpha.const import (DEFAULT_ACCOUNT_TYPE, EXECUTION_PHASE, INSTRUMENT_TYPE, ORDER_TYPE, POSITION_DIRECTION, POSITION_EFFECT, SIDE) @@ -40,7 +41,7 @@ from rqalpha.model.instrument import IndustryCodeItem, Instrument from rqalpha.model.instrument import SectorCode as sector_code from rqalpha.model.instrument import SectorCodeItem -from rqalpha.model.order import LimitOrder, MarketOrder, Order, OrderStyle +from rqalpha.model.order import LimitOrder, MarketOrder, Order, OrderStyle, ALGO_ORDER_STYLES from rqalpha.utils import INST_TYPE_IN_STOCK_ACCOUNT, is_valid_price from rqalpha.utils.arg_checker import apply_rules, verify_that from rqalpha.utils.datetime_func import to_date @@ -80,13 +81,26 @@ def _round_order_quantity(ins, quantity) -> int: except ValueError: raise + +def _get_order_style_price(order_book_id, style): + if isinstance(style, LimitOrder): + return style.get_limit_price() + env = Environment.get_instance() + if isinstance(style, MarketOrder): + return env.data_proxy.get_last_price(order_book_id) + if isinstance(style, ALGO_ORDER_STYLES): + price, _ = env.data_proxy.get_algo_bar(order_book_id, style, env.calendar_dt) + return price + raise RuntimeError(f"no support {style} order style") + + def _submit_order(ins, amount, side, position_effect, style, current_quantity, auto_switch_order_value): env = Environment.get_instance() - if isinstance(style, LimitOrder): - if not is_valid_price(style.get_limit_price()): - raise RQInvalidArgument(_(u"Limit order price should be positive")) + if isinstance(style, LimitOrder) and np.isnan(style.get_limit_price()): + 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): + algo_price = env.data_proxy.get_algo_bar(ins, style, env.calendar_dt)[0] if isinstance(style, ALGO_ORDER_STYLES) else 1 + if not (is_valid_price(price) and is_valid_price(algo_price)): user_system_log.warn( _(u"Order Creation Failed: [{order_book_id}] No market data").format(order_book_id=ins.order_book_id)) return @@ -118,24 +132,21 @@ def _order_shares(ins, amount, style, quantity, auto_switch_order_value): def _order_value(account, position, ins, cash_amount, style): - env = Environment.get_instance() if cash_amount > 0: cash_amount = min(cash_amount, account.cash) - if isinstance(style, LimitOrder): - price = style.get_limit_price() - 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) - ) - return + price = _get_order_style_price(ins.order_book_id, style) + 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) + ) + return amount = int(Decimal(cash_amount) / Decimal(price)) round_lot = int(ins.round_lot) if cash_amount > 0: amount = _round_order_quantity(ins, amount) while amount > 0: + env = Environment.get_instance() expected_transaction_cost = env.get_order_transaction_cost(Order.__from_create__( ins.order_book_id, amount, SIDE.BUY, LimitOrder(price), POSITION_EFFECT.OPEN )) @@ -155,59 +166,68 @@ def _order_value(account, position, ins, cash_amount, style): @order_shares.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_shares(id_or_ins, amount, price=None, style=None): +def stock_order_shares(id_or_ins, amount, price=None, style=None, price_or_style=None): auto_switch_order_value = Environment.get_instance().config.mod.sys_accounts.auto_switch_order_value account, position, ins = _get_account_position_ins(id_or_ins) - return _order_shares(assure_instrument(id_or_ins), amount, cal_style(price, style), position.quantity, - auto_switch_order_value) + return _order_shares( + assure_instrument(id_or_ins), amount, cal_style(price, style, price_or_style), position.quantity, + auto_switch_order_value + ) @order_value.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_value(id_or_ins, cash_amount, price=None, style=None): +def stock_order_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=None): account, position, ins = _get_account_position_ins(id_or_ins) - return _order_value(account, position, ins, cash_amount, cal_style(price, style)) + return _order_value(account, position, ins, cash_amount, cal_style(price, style, price_or_style)) @order_percent.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_percent(id_or_ins, percent, price=None, style=None): +def stock_order_percent(id_or_ins, percent, price=None, style=None, price_or_style=None): account, position, ins = _get_account_position_ins(id_or_ins) - return _order_value(account, position, ins, account.total_value * percent, cal_style(price, style)) + return _order_value(account, position, ins, account.total_value * percent, cal_style(price, style, price_or_style)) @order_target_value.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_target_value(id_or_ins, cash_amount, price=None, style=None): +def stock_order_target_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=None): account, position, ins = _get_account_position_ins(id_or_ins) + open_style, close_style = calc_open_close_style(price, style, price_or_style) if cash_amount == 0: - return _submit_order(ins, position.closable, SIDE.SELL, POSITION_EFFECT.CLOSE, cal_style(price, style), - position.quantity, False) - return _order_value(account, position, ins, cash_amount - position.market_value, cal_style(price, style)) + return _submit_order( + ins, position.closable, SIDE.SELL, POSITION_EFFECT.CLOSE, close_style, position.quantity, False + ) + _delta = cash_amount - position.market_value + _style = open_style if _delta > 0 else close_style + return _order_value(account, position, ins, _delta, _style) @order_target_percent.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_target_percent(id_or_ins, percent, price=None, style=None): +def stock_order_target_percent(id_or_ins, percent, price=None, style=None, price_or_style=None): account, position, ins = _get_account_position_ins(id_or_ins) + open_style, close_style = calc_open_close_style(price, style, price_or_style) if percent == 0: - return _submit_order(ins, position.closable, SIDE.SELL, POSITION_EFFECT.CLOSE, cal_style(price, style), - position.quantity, False) - else: - return _order_value( - account, position, ins, account.total_value * percent - position.market_value, cal_style(price, style) + return _submit_order( + ins, position.closable, SIDE.SELL, POSITION_EFFECT.CLOSE, close_style, position.quantity, False ) + _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) @order.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order(order_book_id, quantity, price=None, style=None): - result_order = stock_order_shares(order_book_id, quantity, price, style) +def stock_order(order_book_id, quantity, price=None, style=None, price_or_style=None): + result_order = stock_order_shares(order_book_id, quantity, price, style, price_or_style) if result_order: return [result_order] return [] @order_to.register(INST_TYPE_IN_STOCK_ACCOUNT) -def stock_order_to(order_book_id, quantity, price=None, style=None): +def stock_order_to(order_book_id, quantity, price=None, style=None, price_or_style=None): position = Environment.get_instance().portfolio.get_position(order_book_id, POSITION_DIRECTION.LONG) + open_style, close_style = calc_open_close_style(price, style, price_or_style) quantity = quantity - position.quantity - result_order = stock_order_shares(order_book_id, quantity, price, style) + _style = open_style if quantity > 0 else close_style + result_order = stock_order_shares(order_book_id, quantity, price, _style, price_or_style) if result_order: return [result_order] return [] @@ -221,11 +241,9 @@ def stock_order_to(order_book_id, quantity, price=None, style=None): EXECUTION_PHASE.SCHEDULED, EXECUTION_PHASE.GLOBAL ) -@apply_rules(verify_that('id_or_ins').is_valid_stock(), - verify_that('amount').is_number(), - verify_that('style').is_instance_of((MarketOrder, LimitOrder, type(None)))) -def order_lots(id_or_ins, amount, price=None, style=None): - # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle]) -> Optional[Order] +@apply_rules(verify_that('id_or_ins').is_valid_stock(), verify_that('amount').is_number(), *common_rules) +def order_lots(id_or_ins, amount, price=None, style=None, price_or_style=None): + # type: (Union[str, Instrument], int, Optional[float], Optional[OrderStyle], PRICE_OR_STYLE_TYPE) -> Optional[Order] """ 指定手数发送买/卖单。如有需要落单类型当做一个参量传入,如果忽略掉落单类型,那么默认是市价单(market order)。 @@ -233,6 +251,7 @@ def order_lots(id_or_ins, amount, price=None, style=None): :param int amount: 下单量, 正数代表买入,负数代表卖出。将会根据一手xx股来向下调整到一手的倍数,比如中国A股就是调整成100股的倍数。 :param float price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` + :param price_or_style: 原参数price和style的整合 :example: @@ -246,12 +265,15 @@ def order_lots(id_or_ins, amount, price=None, style=None): """ auto_switch_order_value = Environment.get_instance().config.mod.sys_accounts.auto_switch_order_value account, position, ins = _get_account_position_ins(id_or_ins) - return _order_shares(ins, amount * int(ins.round_lot), cal_style(price, style), position.quantity, - auto_switch_order_value) + return _order_shares( + ins, amount * int(ins.round_lot), cal_style(price, style, price_or_style), position.quantity, + auto_switch_order_value + ) ORDER_TARGET_PORTFOLIO_SUPPORTED_INS_TYPES = { - INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF, INSTRUMENT_TYPE.LOF, INSTRUMENT_TYPE.INDX} + INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF, INSTRUMENT_TYPE.LOF, INSTRUMENT_TYPE.INDX +} @export_as_api @@ -263,28 +285,22 @@ def order_lots(id_or_ins, amount, price=None, style=None): EXECUTION_PHASE.GLOBAL ) def order_target_portfolio( - target_portfolio: Dict[str, Union[float, Tuple[float, float], Tuple[float, float, float]]], - raise_exception_for_invalid_prices: bool = True + target_portfolio: Dict[str, float], price_or_styles: Dict[str, TUPLE_PRICE_OR_STYLE_TYPE] = dict({}), ) -> List[Order]: """ 批量调整股票仓位至目标权重。注意:股票账户中未出现在 target_portfolio 中的资产将被平仓! - 该 API 的参数 target_portfolio 为字典,key 为 order_book_id 或 instrument,value 有三种数据类型可选: - - * value 为权重。此时将根据股票最新价计算目标持仓数量并发出市价单调仓。 - - * value 为权重和价格组成的 tuple。此时将根据该价格计算目标持仓数量并发出限价单(Signal 模式下将使用该价格撮合)。 + 该 API 的参数 target_portfolio 为字典,key 为 order_book_id 或 instrument,value 为权重。 + 此时将根据参数 price_or_styles 中设置的价格来计算目标持仓数量并调仓。 - * value 为权重、平仓价和开仓价组成的 tuple。此时将根据平仓价计算目标持仓数量并分别用两种价格发出平仓和开仓单。 - - :param target_portfolio: 目标权重字典,key 为 order_book_id,value 为权重或权重和价格组成的 tuple。 - :param raise_exception_for_invalid_prices: 遇到无效的价格是否抛出异常。设为 False 表示遇到无效的价格时不交易该标的而不是抛出异常。 + :param target_portfolio: 目标权重字典,key 为 order_book_id,value 为权重。 + :param price_or_styles: 目标下单价格字典,key 为 order_book_id, value 为价格或订单类型或订单类型和价格组成的 tuple :example: .. code-block:: python - # 调整仓位,以使平安银行和万科 A 的持仓占比分别达到 10% 和 15% + # 调整仓位,以使平安银行和万科 A 的持仓占比分别达到 10% 和 15%, 同时发送市价单 order_target_portfolio({ '000001.XSHE': 0.1, '000002.XSHE': 0.15 @@ -292,22 +308,27 @@ def order_target_portfolio( # 调整仓位,分别以 14 和 26 元发出限价单,目标是使平安银行和万科 A 的持仓占比分别达到 10% 和 15% order_target_portfolio({ - '000001.XSHE': (0.1, 14), - '000002.XSHE': (0.15, 26) + '000001.XSHE': 0.1, + '000002.XSHE': 0.15 + }, { + '000001.XSHE': 14, + '000002.XSHE': 26, }) # 调整仓位,使平安银行和万科 A 的持仓占比分别达到 10% 和 15%。 # 其中平安银行的平仓价为 14 元,开仓价为 15 元;万科 A 的平仓价为 26 元,开仓价为 27 元。 order_target_portfolio({ - '000001.XSHE': (0.1, 14, 15), - '000002.XSHE': (0.15, 26, 27) - + '000001.XSHE': 0.1, + '000002.XSHE': 0.15 + }, { + '000001.XSHE': (15, 14), + '000002.XSHE': (27, 26) + }) """ env = Environment.get_instance() - # order_book_id -> (target_weight, close_price, open_price, last_price) target: Dict[str, Tuple[float, float, float, float]] = {} - for id_or_ins, target_quantity_price in target_portfolio.items(): + for id_or_ins, percent in target_portfolio.items(): ins = assure_instrument(id_or_ins) if not ins: raise RQInvalidArgument(_( @@ -326,26 +347,16 @@ def order_target_portfolio( ) continue - if isinstance(target_quantity_price, (int, float)): - target_percent = target_quantity_price - open_price = close_price = last_price - else: - target_percent, *target_prices = target_quantity_price - if not target_prices: - open_price = close_price = last_price - else: - try: - close_price, open_price = target_prices - except ValueError: - close_price = open_price = target_prices[0] - - if target_percent < 0: + price_or_style = price_or_styles.get(ins.order_book_id) + open_style, close_style = calc_open_close_style(price=None, style=None, price_or_style=price_or_style) + + if percent < 0: raise RQInvalidArgument(_( "function order_target_portfolio: invalid values of target_portfolio, " "excepted float between 0 and 1, got {} (key: {})" - ).format(target_percent, id_or_ins)) + ).format(percent, id_or_ins)) - target[order_book_id] = target_percent, close_price, open_price, last_price + target[order_book_id] = percent, open_style, close_style, last_price total_percent = sum(p for p, *__ in target.values()) if total_percent > 1 and not np.isclose(total_percent, 1): @@ -365,38 +376,32 @@ def order_target_portfolio( account_value = account.total_value close_orders, open_orders = [], [] - for order_book_id, (target_percent, close_price, open_price, last_price) in target.items(): + for order_book_id, (target_percent, open_style, close_style, last_price) in target.items(): + 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)): - if raise_exception_for_invalid_prices: - raise RQInvalidArgument(_( - "function order_target_portfolio: " - "invalid close/open price {close_price}/{open_price} of {id_or_ins}" - ).format(id_or_ins=order_book_id, close_price=close_price, open_price=open_price)) - else: - 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 - ) + 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 ) - continue - delta_quantity = (account_value * target_percent / (close_price or last_price)) \ - - current_quantities.get(order_book_id, 0) + ) + continue + + delta_quantity = (account_value * target_percent / close_price) - current_quantities.get(order_book_id, 0) delta_quantity = _round_order_quantity(env.data_proxy.instrument(order_book_id), delta_quantity) if delta_quantity == 0: continue elif delta_quantity > 0: quantity, side, position_effect = delta_quantity, SIDE.BUY, POSITION_EFFECT.OPEN order_list = open_orders - target_price = open_price + target_style = open_style else: quantity, side, position_effect = abs(delta_quantity), SIDE.SELL, POSITION_EFFECT.CLOSE order_list = close_orders - target_price = close_price - if target_price: - order = Order.__from_create__(order_book_id, quantity, side, LimitOrder(target_price), position_effect) - else: - order = Order.__from_create__(order_book_id, quantity, side, MarketOrder(), position_effect) + target_style = close_style + order = Order.__from_create__(order_book_id, quantity, side, target_style, position_effect) + if isinstance(target_style, MarketOrder): order.set_frozen_price(last_price) order_list.append(order) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py index e92e6f085..96e2706f3 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py @@ -19,7 +19,7 @@ from rqalpha.const import MATCHING_TYPE, ORDER_TYPE, POSITION_EFFECT, SIDE from rqalpha.environment import Environment from rqalpha.core.events import EVENT, Event -from rqalpha.model.order import Order +from rqalpha.model.order import Order, ALGO_ORDER_STYLES from rqalpha.model.trade import Trade from rqalpha.model.tick import TickObject from rqalpha.portfolio.account import Account @@ -83,6 +83,26 @@ def _open_auction_deal_price_decider(self, order_book_id, _): SUPPORT_POSITION_EFFECTS = (POSITION_EFFECT.OPEN, POSITION_EFFECT.CLOSE, POSITION_EFFECT.CLOSE_TODAY) SUPPORT_SIDES = (SIDE.BUY, SIDE.SELL) + def _get_bar_volume(self, order, open_auction=False): + if open_auction: + volume = self._env.data_proxy.get_open_auction_bar(order.order_book_id, self._env.calendar_dt).volume + else: + if isinstance(order.style, ALGO_ORDER_STYLES): + _, volume = self._env.data_proxy.get_algo_bar(order.order_book_id, order.style, self._env.calendar_dt) + else: + volume = self._env.get_bar(order.order_book_id).volume + return volume + + def _get_deal_price(self, order, open_auction=False): + if open_auction: + deal_price = self._open_auction_deal_price_decider(order.order_book_id, order.side) + else: + if isinstance(order.style, ALGO_ORDER_STYLES): + deal_price = order.price + else: + deal_price = self._deal_price_decider(order.order_book_id, order.side) + return deal_price + def match(self, account, order, open_auction): # type: (Account, Order, bool) -> None if not (order.position_effect in self.SUPPORT_POSITION_EFFECTS and order.side in self.SUPPORT_SIDES): @@ -90,10 +110,7 @@ def match(self, account, order, open_auction): order_book_id = order.order_book_id instrument = self._env.get_instrument(order_book_id) - if open_auction: - deal_price = self._open_auction_deal_price_decider(order_book_id, order.side) - else: - deal_price = self._deal_price_decider(order_book_id, order.side) + deal_price = self._get_deal_price(order, open_auction) if not is_valid_price(deal_price): listed_date = instrument.listed_date.date() @@ -138,17 +155,14 @@ def match(self, account, order, open_auction): return if self._inactive_limit: - bar_volume = self._env.get_bar(order_book_id).volume + bar_volume = self._get_bar_volume(order, open_auction=open_auction) if bar_volume == 0: reason = _(u"Order Cancelled: {order_book_id} bar no volume").format(order_book_id=order.order_book_id) order.mark_cancelled(reason) return if self._volume_limit: - if open_auction: - volume = self._env.data_proxy.get_open_auction_bar(order_book_id, self._env.calendar_dt).volume - else: - volume = self._env.get_bar(order_book_id).volume + volume = self._get_bar_volume(order, open_auction=open_auction) if volume == volume: volume_limit = round(volume * self._volume_percent) - self._turnover[order.order_book_id] @@ -171,6 +185,7 @@ def match(self, account, order, open_auction): fill = order.unfilled_quantity ct_amount = account.calc_close_today_amount(order_book_id, fill, order.position_direction) + if open_auction: price = deal_price else: diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py index a72f8462e..510204571 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py @@ -38,6 +38,9 @@ def start_up(self, env, mod_config): mod_config.matching_type = self.parse_matching_type(mod_config.matching_type, env.config.base.frequency) + if mod_config.matching_type == MATCHING_TYPE.VWAP: + user_system_log.warning("matching_type: vwap is deprecated") + if env.config.base.margin_multiplier <= 0: raise patch_user_exc(ValueError(_(u"invalid margin multiplier value: value range is (0, +∞]"))) diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py index 9a0b4de33..fd5c8925f 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/__init__.py @@ -18,9 +18,11 @@ __config__ = { # 股票最小手续费,单位元 "cn_stock_min_commission": 5, - # 佣金倍率,即在默认的手续费率基础上按该倍数进行调整,股票的默认佣金为万八,期货默认佣金因合约而异 + # 佣金倍率(即将废弃) "commission_multiplier": None, + # 股票佣金倍率,即在默认的手续费率基础上按该倍数进行调整,股票的默认佣金为万八 "stock_commission_multiplier": 1, + # 期货佣金倍率,即在默认的手续费率基础上按该倍数进行调整,期货默认佣金因合约而异 "futures_commission_multiplier": 1, # 印花倍率,即在默认的印花税基础上按该倍数进行调整,股票默认印花税为千分之一,单边收取 "tax_multiplier": 1, diff --git a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py index 854d94152..5cc56e1c4 100644 --- a/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_transaction_cost/mod.py @@ -27,7 +27,7 @@ class TransactionCostMod(AbstractMod): def start_up(self, env, mod_config): - if mod_config.commission_multiplier: + if mod_config.commission_multiplier is not None: stock_commission_multiplier = mod_config.commission_multiplier futures_commission_multiplier = mod_config.commission_multiplier user_log.warning( diff --git a/rqalpha/model/order.py b/rqalpha/model/order.py index 12191b718..0aaac717c 100644 --- a/rqalpha/model/order.py +++ b/rqalpha/model/order.py @@ -20,7 +20,7 @@ import numpy as np -from rqalpha.const import ORDER_STATUS, ORDER_TYPE, SIDE, POSITION_EFFECT, POSITION_DIRECTION +from rqalpha.const import ORDER_STATUS, ORDER_TYPE, SIDE, POSITION_EFFECT, POSITION_DIRECTION, ALGO from rqalpha.utils import id_gen, decimal_rounding_floor, get_position_direction from rqalpha.utils.repr import property_repr, properties from rqalpha.utils.logger import user_system_log @@ -50,6 +50,7 @@ def __init__(self): self._type = None self._avg_price = None self._transaction_cost = None + self._style = None self._kwargs = {} @staticmethod @@ -109,12 +110,16 @@ def __from_create__(cls, order_book_id, quantity, side, style, position_effect, order._message = "" order._filled_quantity = 0 order._status = ORDER_STATUS.PENDING_NEW + order._style = style if isinstance(style, LimitOrder): if env.config.base.round_price: tick_size = env.data_proxy.get_tick_size(order_book_id) style.round_price(tick_size) order._frozen_price = style.get_limit_price() order._type = ORDER_TYPE.LIMIT + elif isinstance(style, ALGO_ORDER_STYLES): + order._frozen_price, _ = env.data_proxy.get_algo_bar(order_book_id, style, env.calendar_dt) + order._type = ORDER_TYPE.ALGO else: order._frozen_price = 0. order._type = ORDER_TYPE.MARKET @@ -237,6 +242,13 @@ def type(self): """ return self._type + @property + def style(self): + """ + [ORDER_STYLE] 订单类型 + """ + return self._style + @property def avg_price(self): """ @@ -338,6 +350,27 @@ def get_limit_price(self): raise NotImplementedError +class AlgoOrder(OrderStyle): + __repr__ = ORDER_TYPE.ALGO.__repr__ + + def __init__(self, start_min, end_min): + self.start_min = start_min + self.end_min = end_min + + def get_limit_price(self): + return None + + +class TWAPOrder(AlgoOrder): + TYPE = ALGO.TWAP + __repr__ = ALGO.TWAP.__repr__ + + +class VWAPOrder(AlgoOrder): + TYPE = ALGO.VWAP + __repr__ = ALGO.VWAP.__repr__ + + class MarketOrder(OrderStyle): __repr__ = ORDER_TYPE.MARKET.__repr__ @@ -362,3 +395,7 @@ def round_price(self, tick_size): self.limit_price = float((limit_price_decimal / tick_size_decimal).to_integral() * tick_size_decimal) else: user_system_log.warn('Invalid tick size: {}'.format(tick_size)) + + +ALGO_ORDER_STYLES = (VWAPOrder, TWAPOrder) +ALL_ORDER_STYPES = (LimitOrder, MarketOrder, TWAPOrder, VWAPOrder) diff --git a/rqalpha/utils/__init__.py b/rqalpha/utils/__init__.py index b9c693aaf..dc6fcc9c8 100644 --- a/rqalpha/utils/__init__.py +++ b/rqalpha/utils/__init__.py @@ -236,3 +236,8 @@ def get_trading_period(universe, accounts): from rqalpha.environment import Environment trading_period = STOCK_TRADING_PERIOD if DEFAULT_ACCOUNT_TYPE.STOCK in accounts else [] return Environment.get_instance().data_proxy.get_trading_period(universe, trading_period) + + +def safe_div(dividend, divisor): + return dividend / divisor if divisor else np.nan + diff --git a/rqalpha/utils/arg_checker.py b/rqalpha/utils/arg_checker.py index b96c094b9..ec7a0c55d 100644 --- a/rqalpha/utils/arg_checker.py +++ b/rqalpha/utils/arg_checker.py @@ -31,6 +31,7 @@ from rqalpha.utils import unwrapper, INST_TYPE_IN_STOCK_ACCOUNT from rqalpha.utils.i18n import gettext as _ from rqalpha.utils.exception import patch_system_exc, EXC_EXT_NAME +from rqalpha.utils.logger import user_system_log main_contract_warning_flag = True @@ -141,6 +142,14 @@ def is_number(self): self._rules.append(self._is_number) return self + def deprecated(self, hint="deprecated"): + def inner(func_name, value): + if value is not None: + content = "{} param {} is deprecated. {}".format(func_name, self._arg_name, hint) + user_system_log.warning(content) + self._rules.append(inner) + return self + def is_in(self, valid_values, ignore_none=True): def check_is_in(func_name, value): if ignore_none and value is None: @@ -236,7 +245,7 @@ def check_is_valid_date(func_name, value): def is_greater_or_equal_than(self, low): def check_greater_or_equal_than(func_name, value): - if value < low: + if isinstance(value, (int, float)) and value < low: raise RQInvalidArgument( _(u"function {}: invalid {} argument, expect a value >= {}, got {} (type: {})").format( func_name, self._arg_name, low, value, type(value) @@ -246,7 +255,7 @@ def check_greater_or_equal_than(func_name, value): def is_greater_than(self, low): def check_greater_than(func_name, value): - if value <= low: + if isinstance(value, (int, float)) and value <= low: raise RQInvalidArgument( _(u"function {}: invalid {} argument, expect a value > {}, got {} (type: {})").format( func_name, self._arg_name, low, value, type(value) @@ -256,7 +265,7 @@ def check_greater_than(func_name, value): def is_less_or_equal_than(self, high): def check_less_or_equal_than(func_name, value): - if value > high: + if isinstance(value, (int, float)) and value > high: raise RQInvalidArgument( _(u"function {}: invalid {} argument, expect a value <= {}, got {} (type: {})").format( func_name, self._arg_name, high, value, type(value) @@ -267,7 +276,7 @@ def check_less_or_equal_than(func_name, value): def is_less_than(self, high): def check_less_than(func_name, value): - if value >= high: + if isinstance(value, (int, float)) and value >= high: raise RQInvalidArgument( _(u"function {}: invalid {} argument, expect a value < {}, got {} (type: {})").format( func_name, self._arg_name, high, value, type(value) diff --git a/tests/api_tests/test_api_stock.py b/tests/api_tests/test_api_stock.py index 45984072e..e30f8e30f 100644 --- a/tests/api_tests/test_api_stock.py +++ b/tests/api_tests/test_api_stock.py @@ -166,9 +166,13 @@ def handle_bar(context, bar_dict): assert get_position("000004.XSHE").quantity == 10500 # (1000000 * 0.2) / 18.92 = 10570.82 elif context.counter == 2: order_target_portfolio({ - "000004.XSHE": (0.1, 18, 18.5), - "000005.XSHE": (0.2, 2.92), - "600519.XSHG": (0.6, 980, 970), + "000004.XSHE": 0.1, + "000005.XSHE": 0.2, + "600519.XSHG": 0.6, + }, { + "000004.XSHE": (18.5, 18), + "000005.XSHE": (2.92, ), + "600519.XSHG": (970, 980), }) assert get_position("000001.XSHE").quantity == 0 # 清仓 assert get_position("000004.XSHE").quantity == 5600 # (993695.7496 * 0.1) / 18 = 5520.53 @@ -201,8 +205,11 @@ def handle_bar(context, handle_bar): context.counter += 1 if context.counter == 1: order_target_portfolio({ - "000001.XSHE": (0.1, 14), - "000004.XSHE": (0.2, 10), + "000001.XSHE": 0.1, + "000004.XSHE": 0.2, + }, { + "000001.XSHE": 14, + "000004.XSHE": 10, }) assert get_position("000001.XSHE").quantity == 7100 # (1000000 * 0.1) / 14.37 = 7142.86 assert get_position("000004.XSHE").quantity == 0 # 价格低过跌停价,被拒单 From e106b767cedb6cc5ce239d59468bdd357e73242c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=98=89=E4=BF=8A?= <654181984@qq.com> Date: Tue, 30 May 2023 16:35:44 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=AE=97=E6=B3=95?= =?UTF-8?q?=E5=8D=95;get=5Fprev=5Fsettelemnt=E4=BD=BF=E7=94=A8prev=5Fsettl?= =?UTF-8?q?ement=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rqalpha/apis/api_abstract.py | 76 ++++++++++++++----- rqalpha/data/data_proxy.py | 12 ++- rqalpha/interface.py | 3 +- .../api/api_future.py | 5 +- .../rqalpha_mod_sys_accounts/api/api_stock.py | 25 +++--- .../mod/rqalpha_mod_sys_simulation/matcher.py | 4 +- rqalpha/mod/rqalpha_mod_sys_simulation/mod.py | 6 +- .../signal_broker.py | 9 +++ .../rqalpha_mod_sys_simulation/validator.py | 18 +++++ rqalpha/model/order.py | 5 +- 10 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 rqalpha/mod/rqalpha_mod_sys_simulation/validator.py diff --git a/rqalpha/apis/api_abstract.py b/rqalpha/apis/api_abstract.py index ea2d2f806..0d11cf90d 100644 --- a/rqalpha/apis/api_abstract.py +++ b/rqalpha/apis/api_abstract.py @@ -31,12 +31,14 @@ verify_that('style', pre_check=True).deprecated("please use price_or_style instead.").is_instance_of( (*ALL_ORDER_STYPES, type(None)) ), - verify_that("price_or_style").is_instance_of((float, type(None), *ALL_ORDER_STYPES)), + verify_that("price_or_style", pre_check=True).is_instance_of((float, type(None), tuple, *ALL_ORDER_STYPES)), ) PRICE_OR_STYLE_TYPE = Union[float, OrderStyle, None] -TUPLE_PRICE_OR_STYLE_TYPE = Union[float, OrderStyle, None, Tuple[PRICE_OR_STYLE_TYPE, PRICE_OR_STYLE_TYPE]] +TUPLE_PRICE_OR_STYLE_TYPE = Union[ + float, OrderStyle, None, Tuple, Tuple[PRICE_OR_STYLE_TYPE], Tuple[PRICE_OR_STYLE_TYPE, PRICE_OR_STYLE_TYPE] +] @export_as_api @@ -61,7 +63,8 @@ def order_shares(id_or_ins, amount, price=None, style=None, price_or_style=None) :param amount: 下单量, 正数代表买入,负数代表卖出。将会根据一手xx股来向下调整到一手的倍数,比如中国A股就是调整成100股的倍数。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -71,8 +74,12 @@ def order_shares(id_or_ins, amount, price=None, style=None, price_or_style=None) order_shares('000001.XSHE', 2000) #卖出2000股的平安银行股票,并以市价单发送: order_shares('000001.XSHE', -2000) + #购买1000股的平安银行股票,并以限价单发送,价格为¥11: + order_shares('000001.XSHG', 1000, price_or_style=11) #购买1000股的平安银行股票,并以限价单发送,价格为¥10: - order_shares('000001.XSHG', 1000, style=LimitOrder(10)) + order_shares('000001.XSHG', 1000, price_or_style=LimitOrder(10)) + #购买1000股的平安银行股票,并以 9:31 到 9:45 的VWAP价格发送: + order_shares('000001.XSHG', 1000, price_or_style=VWAPOrder(931, 945)) """ raise NotImplementedError @@ -104,7 +111,8 @@ def order_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=N :param cash_amount: 需要花费现金购买/卖出证券的数目。正数代表买入,负数代表卖出。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -112,8 +120,8 @@ def order_value(id_or_ins, cash_amount, price=None, style=None, price_or_style=N #花费最多¥10000买入平安银行股票,并以市价单发送。具体下单的数量与您策略税费相关的配置有关。 order_value('000001.XSHE', 10000) - #卖出价值¥10000的现在持有的平安银行: - order_value('000001.XSHE', -10000) + #卖出价值¥10000的现在持有的平安银行, 以10¥价格发出限价单: + order_value('000001.XSHE', -10000, price_or_style=10) """ raise NotImplementedError @@ -145,7 +153,8 @@ def order_percent(id_or_ins, percent, price=None, style=None, price_or_style=Non :param percent: 占有现有的投资组合价值的百分比。正数表示买入,负数表示卖出。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -153,6 +162,8 @@ def order_percent(id_or_ins, percent, price=None, style=None, price_or_style=Non #花费等于现有投资组合50%价值的现金买入平安银行股票: order_percent('000001.XSHG', 0.5) + #花费等于现有投资组合50%价值的现金买入平安银行股票, 以10¥限价单: + order_percent('000001.XSHG', 0.5, price_or_style=10) """ raise NotImplementedError @@ -183,7 +194,8 @@ def order_target_value(id_or_ins, cash_amount, price=None, style=None, price_or_ :param cash_amount: 最终的该证券的仓位目标价值。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -191,6 +203,9 @@ def order_target_value(id_or_ins, cash_amount, price=None, style=None, price_or_ #如果现在的投资组合中持有价值¥3000的平安银行股票的仓位,以下代码范例会发送花费 ¥7000 现金的平安银行买单到市场。(向下调整到最接近每手股数即100的倍数的股数): order_target_value('000001.XSHE', 10000) + #如果现在的投资组合中持有价值¥3000的平安银行股票的仓位,以下代码范例会发送10¥限价单共花费 ¥7000 现金的平安银行买单到市场 + #或者如果现在的投资组合中持有价值¥13000的平安银行股票的仓位,以下代码范例会发送11¥限价单共花费 ¥3000 现金的平安银行卖单到市场 + order_target_value('000001.XSHE', 10000, price_or_style=(10, 11) """ raise NotImplementedError @@ -230,7 +245,8 @@ def order_target_percent(id_or_ins, percent, price=None, style=None, price_or_st :param percent: 仓位最终所占投资组合总价值的目标百分比。 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -238,6 +254,9 @@ def order_target_percent(id_or_ins, percent, price=None, style=None, price_or_st #如果投资组合中已经有了平安银行股票的仓位,并且占据目前投资组合的10%的价值,那么以下代码会消耗相当于当前投资组合价值5%的现金买入平安银行股票: order_target_percent('000001.XSHE', 0.15) + #如果投资组合中已经有了平安银行股票的仓位,并且占据目前投资组合的10%的价值,那么以下代码会消耗相当于当前投资组合价值5%的现金以10¥限价单买入平安银行股票: + #或者如果投资组合中已经有了平安银行股票的仓位,并且占据目前投资组合的20%的价值,那么以下代码会消耗相当于当前投资组合价值5%的现金以11¥限价单卖出平安银行股票: + order_target_percent('000001.XSHE', 0.15, price_or_style=(10, 11)) """ raise NotImplementedError @@ -264,14 +283,15 @@ def buy_open(id_or_ins, amount, price=None, style=None, price_or_style=None): :param amount: 下单手数 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: .. code-block:: python #以价格为3500的限价单开仓买入2张上期所AG1607合约: - buy_open('AG1607', amount=2, price=3500)) + buy_open('AG1607', amount=2, price_or_style=3500)) """ raise NotImplementedError @@ -299,7 +319,8 @@ def buy_close(id_or_ins, amount, price=None, style=None, price_or_style=None, cl :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` :param close_today: 是否指定发平今仓单,默认为False,发送平仓单 - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -333,7 +354,16 @@ def sell_open(id_or_ins, amount, price=None, style=None, price_or_style=None): :param amount: 下单手数 :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder + + :example: + + .. code-block:: python + + # 以3100发出限价单将现有IF1603卖出开仓2张: + sell_open('IF1603', 2, price_or_style=3100) + """ raise NotImplementedError @@ -361,7 +391,17 @@ def sell_close(id_or_ins, amount, price=None, style=None, price_or_style=None, c :param price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` :param close_today: 是否指定发平今仓单,默认为False,发送平仓单 - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder + + :example: + + .. code-block:: python + + # 以市价单单将现有IF1603买入平仓2张: + sell_close('IF1603', 2, price_or_style=MarketOrder()) + + """ raise NotImplementedError @@ -388,7 +428,8 @@ def order(order_book_id, quantity, price=None, style=None, price_or_style=None): :param quantity: 调仓量 :param price: 下单价格 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: @@ -428,7 +469,8 @@ def order_to(order_book_id, quantity, price=None, style=None, price_or_style=Non :param int quantity: 调仓量 :param float price: 下单价格 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index 8962f3edf..2eab1f09d 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -16,7 +16,7 @@ # 详细的授权流程,请联系 public@ricequant.com 获取。 from datetime import datetime, date -from typing import Union, List, Sequence, Optional +from typing import Union, List, Sequence, Optional, Tuple import six import numpy as np @@ -28,6 +28,7 @@ from rqalpha.model.bar import BarObject, NANDict, PartialBarObject from rqalpha.model.tick import TickObject from rqalpha.model.instrument import Instrument +from rqalpha.model.order import TWAPOrder, VWAPOrder, ALGO_ORDER_STYLES from rqalpha.utils.functools import lru_cache from rqalpha.utils.datetime_func import convert_int_to_datetime, convert_date_to_int from rqalpha.utils.typing import DateLike, StrOrIter @@ -130,9 +131,9 @@ def get_prev_close(self, order_book_id, dt): @lru_cache(10240) def _get_prev_settlement(self, instrument, dt): - prev_trading_date = self.get_previous_trading_date(dt) - bar = self._data_source.history_bars(instrument, 1, '1d', 'settlement', prev_trading_date, - skip_suspended=False, adjust_orig=dt) + bar = self._data_source.history_bars( + instrument, 1, '1d', fields='prev_settlement', dt=dt, skip_suspended=False, adjust_orig=dt + ) if bar is None or len(bar) == 0: return np.nan return bar[0] @@ -305,6 +306,9 @@ def is_night_trading(self, sym_or_ids): return any((instrument.trade_at_night for instrument in self.instruments(sym_or_ids))) def get_algo_bar(self, id_or_ins, order_style, dt): + # type: (Union[str, Instrument], Union[*ALGO_ORDER_STYLES], datetime) -> Tuple[float, int] + if not isinstance(order_style, ALGO_ORDER_STYLES): + raise RuntimeError("get_algo_bar only support VWAPOrder and TWAPOrder") if not isinstance(id_or_ins, Instrument): id_or_ins = self.instrument(id_or_ins) if id_or_ins is None: diff --git a/rqalpha/interface.py b/rqalpha/interface.py index a6bd1ca49..91261a495 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -446,7 +446,8 @@ def is_st_stock(self, order_book_id, dates): raise NotImplementedError def get_algo_bar(self, id_or_ins, start_min, end_min, dt): - # type: (Union[str, Instrument], int, int, datetime) -> Optional[numpy.ndarray] + # type: (Union[str, Instrument], int, int, datetime) -> Optional[numpy.void] + # 格式: (date, VWAP, TWAP, volume) -> 案例 (20200102, 16.79877183, 16.83271429, 144356044) raise NotImplementedError diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py index d5de40b4f..8b93fc857 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py @@ -61,8 +61,7 @@ def _submit_order(id_or_ins, amount, side, position_effect, style): raise RQInvalidArgument(_(u"Index Future contracts[99] are not supported in paper trading.")) price = env.get_last_price(order_book_id) - algo_price = env.data_proxy.get_algo_bar(instrument, style, env.calendar_dt)[0] if isinstance(style, ALGO_ORDER_STYLES) else 1 - if not (is_valid_price(price) and is_valid_price(algo_price)): + 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) ) @@ -122,8 +121,6 @@ def _submit_order(id_or_ins, amount, side, position_effect, style): ) for o in orders: - if o.type == ORDER_TYPE.MARKET: - o.set_frozen_price(price) if env.can_submit_order(o): env.broker.submit_order(o) else: diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py index 1acac6c0c..494949bb2 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py @@ -99,8 +99,7 @@ def _submit_order(ins, amount, side, position_effect, style, current_quantity, a if isinstance(style, LimitOrder) and np.isnan(style.get_limit_price()): raise RQInvalidArgument(_(u"Limit order price should not be nan.")) price = env.data_proxy.get_last_price(ins.order_book_id) - algo_price = env.data_proxy.get_algo_bar(ins, style, env.calendar_dt)[0] if isinstance(style, ALGO_ORDER_STYLES) else 1 - if not (is_valid_price(price) and is_valid_price(algo_price)): + 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)) return @@ -114,8 +113,6 @@ def _submit_order(ins, amount, side, position_effect, style, current_quantity, a ).format(order_book_id=ins.order_book_id)) return order = Order.__from_create__(ins.order_book_id, abs(amount), side, style, position_effect) - if order.type == ORDER_TYPE.MARKET: - order.set_frozen_price(price) 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): @@ -132,21 +129,24 @@ def _order_shares(ins, amount, style, quantity, auto_switch_order_value): def _order_value(account, position, ins, cash_amount, style): + env = Environment.get_instance() if cash_amount > 0: cash_amount = min(cash_amount, account.cash) - price = _get_order_style_price(ins.order_book_id, style) - 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) - ) - return + if isinstance(style, LimitOrder): + price = style.get_limit_price() + 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) + ) + return amount = int(Decimal(cash_amount) / Decimal(price)) round_lot = int(ins.round_lot) if cash_amount > 0: amount = _round_order_quantity(ins, amount) while amount > 0: - env = Environment.get_instance() expected_transaction_cost = env.get_order_transaction_cost(Order.__from_create__( ins.order_book_id, amount, SIDE.BUY, LimitOrder(price), POSITION_EFFECT.OPEN )) @@ -251,7 +251,8 @@ def order_lots(id_or_ins, amount, price=None, style=None, price_or_style=None): :param int amount: 下单量, 正数代表买入,负数代表卖出。将会根据一手xx股来向下调整到一手的倍数,比如中国A股就是调整成100股的倍数。 :param float price: 下单价格,默认为None,表示 :class:`~MarketOrder`, 此参数主要用于简化 `style` 参数。 :param style: 下单类型, 默认是市价单。目前支持的订单类型有 :class:`~LimitOrder` 和 :class:`~MarketOrder` - :param price_or_style: 原参数price和style的整合 + :param price_or_style: 默认为None,表示市价单,可设置价格,表示限价单,也可以直接设置订单类型,有如下选项:MarketOrder、LimitOrder、 + TWAPOrder、VWAPOrder :example: diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py index 96e2706f3..a6cac6ae2 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py @@ -98,7 +98,7 @@ def _get_deal_price(self, order, open_auction=False): deal_price = self._open_auction_deal_price_decider(order.order_book_id, order.side) else: if isinstance(order.style, ALGO_ORDER_STYLES): - deal_price = order.price + deal_price, v = self._env.data_proxy.get_algo_bar(order.order_book_id, order.style, self._env.calendar_dt) else: deal_price = self._deal_price_decider(order.order_book_id, order.side) return deal_price @@ -121,6 +121,8 @@ def match(self, account, order, open_auction): order_book_id=order.order_book_id, listed_date=listed_date, ) + elif isinstance(order.style, ALGO_ORDER_STYLES): + reason = _(u"Order Cancelled: {order_book_id} bar no volume").format(order_book_id=order.order_book_id) else: reason = _(u"Order Cancelled: current bar [{order_book_id}] miss market data.").format( order_book_id=order.order_book_id) diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py index 510204571..4c021a91f 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/mod.py @@ -38,9 +38,6 @@ def start_up(self, env, mod_config): mod_config.matching_type = self.parse_matching_type(mod_config.matching_type, env.config.base.frequency) - if mod_config.matching_type == MATCHING_TYPE.VWAP: - user_system_log.warning("matching_type: vwap is deprecated") - if env.config.base.margin_multiplier <= 0: raise patch_user_exc(ValueError(_(u"invalid margin multiplier value: value range is (0, +∞]"))) @@ -76,6 +73,9 @@ def start_up(self, env, mod_config): event_source = SimulationEventSource(env) env.set_event_source(event_source) + from .validator import OrderStyleValidator + env.add_frontend_validator(OrderStyleValidator(env.config.base.frequency)) + def tear_down(self, code, exception=None): pass diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py index 9d66f2a7d..832c75f37 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py @@ -18,12 +18,15 @@ from copy import copy +import numpy as np + from rqalpha.interface import AbstractBroker from rqalpha.utils.logger import user_system_log from rqalpha.utils.i18n import gettext as _ from rqalpha.utils import is_valid_price from rqalpha.core.events import EVENT, Event from rqalpha.model.trade import Trade +from rqalpha.model.order import ALGO_ORDER_STYLES from rqalpha.const import SIDE, ORDER_TYPE, POSITION_EFFECT from .slippage import SlippageDecider @@ -77,6 +80,12 @@ def _match(self, account, order): if order.type == ORDER_TYPE.LIMIT: deal_price = order.frozen_price + elif isinstance(order.style, ALGO_ORDER_STYLES): + deal_price, v = self._env.data_proxy.get_algo_bar(order.order_book_id, order.style, self._env.calendar_dt) + if np.isnan(deal_price): + reason = _(u"Order Cancelled: {order_book_id} bar no volume").format(order_book_id=order.order_book_id) + order.mark_rejected(reason) + return else: deal_price = last_price diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py b/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py new file mode 100644 index 000000000..9df8ff3b2 --- /dev/null +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py @@ -0,0 +1,18 @@ + +from rqalpha.model.order import ALGO_ORDER_STYLES +from rqalpha.interface import AbstractFrontendValidator + + +class OrderStyleValidator(AbstractFrontendValidator): + + def __init__(self, frequency): + self._frequency = frequency + + def can_submit_order(self, order, account=None): + if isinstance(order.style, ALGO_ORDER_STYLES) and self._frequency in ["1m", "tick"]: + raise RuntimeError("algo order no support 1m or tick frequency") + return True + + def can_cancel_order(self, order, account=None): + return True + diff --git a/rqalpha/model/order.py b/rqalpha/model/order.py index 0aaac717c..dde9b1a04 100644 --- a/rqalpha/model/order.py +++ b/rqalpha/model/order.py @@ -118,10 +118,11 @@ def __from_create__(cls, order_book_id, quantity, side, style, position_effect, order._frozen_price = style.get_limit_price() order._type = ORDER_TYPE.LIMIT elif isinstance(style, ALGO_ORDER_STYLES): - order._frozen_price, _ = env.data_proxy.get_algo_bar(order_book_id, style, env.calendar_dt) + algo_price, _ = env.data_proxy.get_algo_bar(order_book_id, style, env.calendar_dt) + order._frozen_price = env.get_last_price(order_book_id) if np.isnan(algo_price) else algo_price order._type = ORDER_TYPE.ALGO else: - order._frozen_price = 0. + order._frozen_price = env.get_last_price(order_book_id) order._type = ORDER_TYPE.MARKET order._avg_price = 0 order._transaction_cost = 0 From 25260b7fe4574701f288dd20d563a5b3eb20b7ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E5=98=89=E4=BF=8A?= <654181984@qq.com> Date: Tue, 30 May 2023 18:27:30 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/source/api/base_api.rst | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/source/api/base_api.rst b/docs/source/api/base_api.rst index 2ad07a6c8..80f314595 100644 --- a/docs/source/api/base_api.rst +++ b/docs/source/api/base_api.rst @@ -141,6 +141,56 @@ after_trading - 盘后 交易接口 ================= +OrderStyle - 订单类型 +------------------------------------------------------ + +该类型可供后续下单接口中 price_or_style 参数使用 + +.. module:: rqalpha.model.order + +.. _order_style: + +.. autoclass:: MarketOrder + + .. code-block:: python + + order_shares("000001.XSHE", amount=100, price_or_style=MarketOrder()) + +市价单 + +.. autoclass:: LimitOrder + + :param float limit_price: 价格 + + .. code-block:: python + + order_shares("000001.XSHE", amount=100, price_or_style=LimitOrder(10)) + +限价单 + +.. autoclass:: TWAPOrder + + :param int start_min: 分钟起始时间 + :param int end_min: 分钟结束时间 + + .. code-block:: python + + order_shares("000001.XSHE", amount=100, price_or_style=TWAPOrder(931, 945)) + +算法时间加权价格订单 + +.. autoclass:: VWAPOrder + + :param int start_min: 分钟起始时间 + :param int end_min: 分钟结束时间 + + .. code-block:: python + + order_shares("000001.XSHE", amount=100, price_or_style=VWAPOrder(931, 945)) + + +算法成交量加权价格订单 + .. module:: rqalpha.api submit_order - 自由参数下单「通用」 @@ -651,6 +701,8 @@ Order - 订单 ------------------------------------------------------ .. module:: rqalpha.model.order +.. _order: + .. autoclass:: Order :members: :show-inheritance: @@ -878,6 +930,10 @@ ORDER_TYPE - 订单类型 限价单 + .. py:attribute:: ALGO + + 算法单 + ORDER_STATUS - 订单状态 ------------------------------------------------------