diff --git a/messages.pot b/messages.pot index cf814f535..f5774a55d 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-01-19 17:56+0800\n" +"POT-Creation-Date: 2024-04-12 15:23+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,13 +17,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" -#: rqalpha/environment.py:70 +#: rqalpha/environment.py:74 msgid "" "Environment has not been created. Please Use `Environment.get_instance()`" " after RQAlpha init" msgstr "" -#: rqalpha/environment.py:163 +#: rqalpha/environment.py:180 msgid "No such transaction cost decider, order_book_id = {}" msgstr "" @@ -39,42 +39,42 @@ msgid "" "persist" msgstr "" -#: rqalpha/main.py:126 +#: rqalpha/main.py:127 msgid "rqdatac init failed, some apis will not function properly: {}" msgstr "" -#: rqalpha/main.py:219 +#: rqalpha/main.py:220 msgid "system restored" msgstr "" -#: rqalpha/main.py:249 +#: rqalpha/main.py:250 msgid "strategy run successfully, normal exit" msgstr "" -#: rqalpha/main.py:254 +#: rqalpha/main.py:255 msgid "strategy execute exception" msgstr "" -#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:286 -#: rqalpha/apis/api_base.py:322 +#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:283 +#: rqalpha/apis/api_base.py:319 msgid "unsupported order_book_id type" msgstr "" #: rqalpha/apis/api_base.py:164 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:60 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "" #: rqalpha/apis/api_base.py:168 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:62 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "" -#: rqalpha/apis/api_base.py:174 +#: rqalpha/apis/api_base.py:173 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:104 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:142 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:346 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:103 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:140 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:343 msgid "Order Creation Failed: [{order_book_id}] No market data" msgstr "" @@ -247,19 +247,19 @@ msgstr "" msgid "deprecated parameter[bar_dict] in before_trading function." msgstr "" -#: rqalpha/data/bundle.py:454 +#: rqalpha/data/bundle.py:460 msgid "" "RQAlpha already supports backtesting using futures historical margins and" " rates, please upgrade RQDatac to version 2.11.12 and above to use it" msgstr "" -#: rqalpha/data/bundle.py:464 rqalpha/data/bundle.py:522 +#: rqalpha/data/bundle.py:470 rqalpha/data/bundle.py:531 msgid "" "Futures historical trading parameters data is being updated, please " "wait......" msgstr "" -#: rqalpha/data/bundle.py:516 +#: rqalpha/data/bundle.py:522 rqalpha/data/bundle.py:706 msgid "" "File {} update failed, if it is using, please update later, or you can " "delete then update again" @@ -285,12 +285,12 @@ msgid "" msgstr "" #: rqalpha/data/base_data_source/storages.py:97 -#: rqalpha/data/base_data_source/storages.py:123 +#: rqalpha/data/base_data_source/storages.py:124 msgid "unsupported future instrument {}" msgstr "" -#: rqalpha/data/base_data_source/storages.py:194 -#: rqalpha/data/base_data_source/storages.py:204 +#: rqalpha/data/base_data_source/storages.py:195 +#: rqalpha/data/base_data_source/storages.py:205 msgid "" "open data bundle failed, you can remove {} and try to regenerate bundle: " "{}" @@ -318,18 +318,18 @@ msgstr "" msgid "mod tear_down [END] {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:314 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:318 msgid "{order_book_id} is expired, close all positions by system" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:41 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:40 msgid "" "Order Creation Failed: not enough today position {order_book_id} to " "close, target quantity is {quantity}, closable today quantity is " "{closable}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:50 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:49 msgid "" "Order Creation Failed: not enough position {order_book_id} to close or " "exercise, target sell quantity is {quantity}, closable quantity is " @@ -338,28 +338,28 @@ msgstr "" #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:50 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:112 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:158 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:157 msgid "Order Creation Failed: 0 order quantity, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:55 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:56 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:100 msgid "Limit order price should not be nan." msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:78 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:76 msgid "" "Order Creation Failed: close today amount {amount} is larger than today " "closable quantity {quantity}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:89 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:88 msgid "" "Order Creation Failed: close amount {amount} is larger than position " "quantity {quantity}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:113 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:111 msgid "" "Order was separated, original order: {original_order_repr}, new orders: " "[{new_orders_repr}]" @@ -371,39 +371,39 @@ msgid "" "again!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:120 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:119 msgid "insufficient cash, use all remaining cash({}) to create order" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:334 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:332 msgid "" "function order_target_portfolio: invalid keys of target_portfolio, " "expected order_book_ids or Instrument objects, got {} (type: {})" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:339 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:337 msgid "" "function order_target_portfolio: invalid instrument type, excepted " "CS/ETF/LOF/INDX, got {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:354 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:351 msgid "" "function order_target_portfolio: invalid values of target_portfolio, " "excepted float between 0 and 1, got {} (key: {})" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:363 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:360 msgid "total percent should be lower than 1, current: {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:383 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:380 msgid "" "Adjust position of {id_or_ins} Failed: Invalid close/open price " "{close_price}/{open_price}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:698 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:693 msgid "in get_dividend, start_date {} is later than the previous test day {}" msgstr "" @@ -455,27 +455,31 @@ msgstr "" msgid "[sys_analyser] Generate report from strategy output file" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:112 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:113 msgid "" "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' " "instead" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:143 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:151 msgid "benchmark {} not exists, please entry correct order_book_id" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:147 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:163 +msgid "benchmark {} missing data between backtest start date {} and end date {}" +msgstr "" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:175 msgid "" -"benchmark {} listed date {} > backtest start date {} or de_listed date {}" -" <= backtest end date {}" +"benchmark {} available data start date {} > backtest start date {} or end" +" date {} <= backtest end date {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:210 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:243 msgid "invalid init benchmark {}, should be in format 'order_book_id:weight'" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:215 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:248 msgid "invalid weight for instrument {order_book_id}: {weight}" msgstr "" @@ -685,33 +689,33 @@ msgstr "" msgid "ExcessWinRate" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:34 msgid "" "Order Creation Failed: not enough money to buy {order_book_id}, needs " "{cost_money:.2f}, cash {cash:.2f}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:32 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:35 msgid "Order Creation Failed: {order_book_id} is not listing!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:38 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:40 msgid "Order Creation Failed: security {order_book_id} is suspended on {date}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:34 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:36 msgid "" "Order Creation Failed: limit order price {limit_price} is higher than " "limit up {limit_up}, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:47 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:48 msgid "" "Order Creation Failed: limit order price {limit_price} is lower than " "limit down {limit_down}, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py:32 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py:34 msgid "" "Create order failed, there are active orders leading to the risk of self-" "trade: [{}...]" diff --git a/rqalpha/apis/api_base.py b/rqalpha/apis/api_base.py index 7b2aff5a4..1b3cfc9ad 100644 --- a/rqalpha/apis/api_base.py +++ b/rqalpha/apis/api_base.py @@ -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) diff --git a/rqalpha/data/base_data_source/data_source.py b/rqalpha/data/base_data_source/data_source.py index 62145e33d..33755cba9 100644 --- a/rqalpha/data/base_data_source/data_source.py +++ b/rqalpha/data/base_data_source/data_source.py @@ -42,7 +42,7 @@ ExchangeTradingCalendarStore, FutureDayBarStore, FutureInfoStore, FuturesTradingParametersStore,InstrumentStore, ShareTransformationStore, SimpleFactorStore, - YieldCurveStore) + YieldCurveStore, FuturesTradingParameters) BAR_RESAMPLE_FIELD_METHODS = { diff --git a/rqalpha/data/data_proxy.py b/rqalpha/data/data_proxy.py index daff3425e..0a0d3e5c8 100644 --- a/rqalpha/data/data_proxy.py +++ b/rqalpha/data/data_proxy.py @@ -326,5 +326,3 @@ def get_algo_bar(self, id_or_ins, order_style, dt): 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/environment.py b/rqalpha/environment.py index 2c32fc9c7..a2e8b8758 100644 --- a/rqalpha/environment.py +++ b/rqalpha/environment.py @@ -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): @@ -114,9 +117,7 @@ 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 @@ -124,9 +125,24 @@ 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() @@ -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 diff --git a/rqalpha/interface.py b/rqalpha/interface.py index a92f419a0..6017a3f8f 100644 --- a/rqalpha/interface.py +++ b/rqalpha/interface.py @@ -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 @@ -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 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 c4e13a4c0..03793c6b8 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py @@ -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.")) @@ -62,23 +63,21 @@ 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 @@ -86,10 +85,9 @@ def _submit_order(id_or_ins, amount, side, position_effect, style): 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: 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 6cea19437..89e6a1d07 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py @@ -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 @@ -100,8 +100,8 @@ 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)): @@ -109,14 +109,13 @@ def _submit_order(ins, amount, side, position_effect, style, current_quantity, a 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)) @@ -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) @@ -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)) @@ -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: @@ -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) @@ -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) @@ -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) @@ -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) diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/component_validator.py b/rqalpha/mod/rqalpha_mod_sys_accounts/component_validator.py index b44d30890..6fa4b8ca8 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/component_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/component_validator.py @@ -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 diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py b/rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py index 8af82f932..cb94c7ecd 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py @@ -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 diff --git a/rqalpha/mod/rqalpha_mod_sys_accounts/validator.py b/rqalpha/mod/rqalpha_mod_sys_accounts/validator.py index 5f74cc548..3121316bb 100644 --- a/rqalpha/mod/rqalpha_mod_sys_accounts/validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_accounts/validator.py @@ -9,16 +9,12 @@ class MarginInstrumentValidator(AbstractFrontendValidator): """ 融资下单品种限制: 当开启股票池限制且有融资余额时只能交易股票和ETF """ - 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: - user_system_log.warn("Order Creation Failed: cash liabilities > 0, {} not support submit order".format( - order.order_book_id) - ) - return False + reason = "Order Creation Failed: cash liabilities > 0, {} not support submit order".format(order.order_book_id) + return reason else: - return True + return None + + def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]: + return None \ No newline at end of file diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py index 587773139..f9a5548d4 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py @@ -33,6 +33,7 @@ from rqalpha.interface import AbstractMod, AbstractPosition from rqalpha.utils.i18n import gettext as _ from rqalpha.utils import INST_TYPE_IN_STOCK_ACCOUNT +from rqalpha.utils.datetime_func import convert_int_to_date from rqalpha.utils.logger import user_system_log from rqalpha.const import DAYS_CNT from rqalpha.api import export_as_api @@ -131,23 +132,60 @@ def get_benchmark_daily_returns(self): daily_return_list.append((bar.close / bar.prev_close - 1.0, benchmark[1])) weights += benchmark[1] return sum([daily[0] * daily[1] / weights for daily in daily_return_list]) - - def _subscribe_events(self, event): - if self._benchmark: - _s = self._env.config.base.start_date - _e = self._env.config.base.end_date - for order_book_id, weight in self._benchmark: - ins = self._env.data_proxy.instrument(order_book_id) - if ins is None: - raise RuntimeError( - _("benchmark {} not exists, please entry correct order_book_id").format(order_book_id) - ) - if ins.listed_date.date() > _s or ins.de_listed_date.date() <= _e: - raise RuntimeError( - _("benchmark {} listed date {} > backtest start date {} or de_listed date {} <= backtest end " - "date {}").format(order_book_id, ins.listed_date.date(), _s, ins.de_listed_date.date(), _e) + + def generate_benchmark_daily_returns_and_portfolio(self, event): + _s = self._env.config.base.start_date + _e = self._env.config.base.end_date + trading_dates = self._env.data_proxy.get_trading_dates(_s, _e) + if self._benchmark is None: + self._benchmark_daily_returns = list(np.full(len(trading_dates), np.nan)) + return + + # generate benchmerk daily returns + self._benchmark_daily_returns = np.zeros(len(trading_dates)) + weights = 0 + for order_book_id, weight in self._benchmark: + ins = self._env.data_proxy.instrument(order_book_id) + if ins is None: + raise RuntimeError( + _("benchmark {} not exists, please entry correct order_book_id").format(order_book_id) + ) + bars = self._env.data_proxy.history_bars( + order_book_id = order_book_id, + bar_count = len(trading_dates) + 1, # Get an extra day for calculation + frequency = "1d", + field = ["datetime", "close"], + dt = _e, + skip_suspended=False, + ) + if len(bars) == len(trading_dates) + 1: + if convert_int_to_date(bars[1]['datetime']).date() != _s: + raise RuntimeError(_( + "benchmark {} missing data between backtest start date {} and end date {}").format(order_book_id, _s, _e) ) + daily_returns = (bars['close'] / np.roll(bars['close'], 1) - 1.0)[1: ] + self._benchmark_daily_returns = self._benchmark_daily_returns + daily_returns * weight + weights += weight + else: + if len(bars) == 0: + (available_s, available_e) = (ins.listed_date, ins.de_listed_date) + else: + (available_s, available_e) = (convert_int_to_date(bars[0]['datetime']).date(), convert_int_to_date(bars[-1]['datetime']).date()) + raise RuntimeError( + _("benchmark {} available data start date {} >= backtest start date {} or end date {} <= backtest end " + "date {}").format(order_book_id, available_s, _s, available_e, _e) + ) + self._benchmark_daily_returns = self._benchmark_daily_returns / weight + + # generate benchmark portfolio + unit_net_value = (self._benchmark_daily_returns + 1).cumprod() + self._total_benchmark_portfolios = { + "date": trading_dates, + "unit_net_value": unit_net_value + } + def _subscribe_events(self, event): + self._env.event_bus.add_listener(EVENT.BEFORE_STRATEGY_RUN, self.generate_benchmark_daily_returns_and_portfolio) self._env.event_bus.add_listener(EVENT.TRADE, self._collect_trade) self._env.event_bus.add_listener(EVENT.ORDER_CREATION_PASS, self._collect_order) self._env.event_bus.prepend_listener(EVENT.POST_SETTLEMENT, self._collect_daily) @@ -164,11 +202,6 @@ def _collect_daily(self, _): self._portfolio_daily_returns.append(portfolio.daily_returns) self._total_portfolios.append(self._to_portfolio_record(date, portfolio)) - self._benchmark_daily_returns.append(self.get_benchmark_daily_returns()) - self._total_benchmark_portfolios.append({ - "date": date, - "unit_net_value": (np.array(self._benchmark_daily_returns) + 1).prod() - }) self._daily_pnl.append(portfolio.daily_pnl) for account_type, account in self._env.portfolio.accounts.items(): diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/report/excel_template.py b/rqalpha/mod/rqalpha_mod_sys_analyser/report/excel_template.py index de68d19e6..2e3d5d276 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/report/excel_template.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/report/excel_template.py @@ -70,9 +70,10 @@ def fill_worksheet(self, ws: Worksheet, data: Dict[str, List]): if not data: for key, (row, column, style) in self._cell_map.items(): self._write_cell(ws, row, column, None, style) - for key, (row, column, style) in self._cell_map.items(): - for i, item in enumerate(data.get(key, [])): - self._write_cell(ws, row + i, column, item, style) + else: + for key, (row, column, style) in self._cell_map.items(): + for i, item in enumerate(data.get(key, [])): + self._write_cell(ws, row + i, column, item, style) class SummaryTemplate(ExcelTemplate): @@ -83,6 +84,7 @@ class SummaryTemplate(ExcelTemplate): "月度收益": VerticalSeriesSchema, "月度超额收益(几何)": VerticalSeriesSchema, "个股权重": VerticalSeriesSchema, + "压力测试": VerticalSeriesSchema, } diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/report/report.py b/rqalpha/mod/rqalpha_mod_sys_analyser/report/report.py index 8617677e9..8d2b244a1 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/report/report.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/report/report.py @@ -17,19 +17,29 @@ import os from typing import Dict, Optional +import datetime import numpy import pandas -from collections import ChainMap +from collections import ChainMap, defaultdict from pandas import Series, DataFrame from rqrisk import Risk from rqrisk import WEEKLY, MONTHLY +from rqalpha.environment import Environment from rqalpha.mod.rqalpha_mod_sys_analyser.plot.utils import max_dd as _max_dd from rqalpha.mod.rqalpha_mod_sys_analyser.report.excel_template import generate_xlsx_reports +PRESSURE_TEST_PERIOD = { + "打击壳价值": (datetime.date(2016, 11, 1), datetime.date(2018, 2, 1)), + "公募基金抱团": (datetime.date(2020, 10, 9), datetime.date(2021, 3, 1)), + "行业风格切换": (datetime.date(2021, 9, 1), datetime.date(2021, 12, 31)), + "小盘踩踏危机": (datetime.date(2024, 1, 5), datetime.date(2024, 2, 8)), +} + + def _returns(unit_net_value: Series): return (unit_net_value / unit_net_value.shift(1).fillna(1)).fillna(0) - 1 @@ -37,13 +47,7 @@ def _returns(unit_net_value: Series): def _yearly_indicators( p_nav: Series, p_returns: Series, b_nav: Optional[Series], b_returns: Optional[Series], risk_free_rates: Dict ): - data = {field: [] for field in [ - "year", "returns", "benchmark_returns", "geometric_excess_return", "geometric_excess_drawdown", - "geometric_excess_drawdown_days", "excess_annual_volatility", "annual_volatility", "sharpe_ratio", - "excess_sharpe", - "information_ratio", "annual_tracking_error", "weekly_excess_win_rate", "monthly_excess_win_rate", - "max_drawdown", "max_drawdown_days" - ]} + data = defaultdict(list) for year, p_year_returns in p_returns.groupby(p_returns.index.year): # noqa year_slice = p_returns.index.year == year # noqa @@ -114,6 +118,44 @@ def _gen_positions_weight(df): return df.reset_index().rename(columns=rename).to_dict(orient="list") +def _pressure_test( + p_nav: Series, p_returns: Series, b_nav: Optional[Series], b_returns: Optional[Series] + ): + env = Environment.get_instance() + # data = {field: [] for field in [ + # "title", "start_date", "end_date", "returns", "annual_return", "geometric_excess_return", "max_drawdown", + # "geometric_excess_drawdown", "sharpe", "excess_sharpe" + # ]} + data = defaultdict(list) + + for title, (start, end) in PRESSURE_TEST_PERIOD.items(): + p_period_returns = p_returns.loc[start: end] + if (p_period_returns is None or p_period_returns.empty): + continue + # 当且仅当回测周期完整包含压力测试的一个区间时才展示该区间的表现 + if len(p_period_returns) != len(env.data_proxy.get_trading_dates(start, end)): + continue + if b_nav is not None: + b_period_returns = b_returns.loc[start: end] + else: + b_period_returns = Series(index=p_period_returns.index) + risk_free_rate = env.data_proxy.get_risk_free_rate(start, end) + risk = Risk(p_period_returns, b_period_returns, risk_free_rate) + data["title"].append(title) + data["start_date"].append(str(start)) + data["end_date"].append(str(end)) + data["returns"].append(risk.return_rate) + data["annual_return"].append(risk.annual_return) + data["geometric_excess_return"].append(risk.geometric_excess_return) + data["max_drawdown"].append(risk.max_drawdown) + data["geometric_excess_drawdown"].append(risk.geometric_excess_drawdown) + data["sharpe"].append(risk.sharpe) + data["excess_sharpe"].append(risk.excess_sharpe) + if not data["title"]: + return None + return data + + def generate_report(result_dict, output_path): from six import StringIO @@ -138,6 +180,7 @@ def generate_report(result_dict, output_path): "月度收益": _monthly_returns(p_returns), "月度超额收益(几何)": _monthly_geometric_excess_returns(p_returns, b_returns), "个股权重": _gen_positions_weight(result_dict["positions_weight"]), + "压力测试": _pressure_test(p_nav, p_returns, b_nav, b_returns), }, output_path) for name in ["portfolio", "stock_account", "future_account", diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/report/templates/summary.xlsx b/rqalpha/mod/rqalpha_mod_sys_analyser/report/templates/summary.xlsx index 4758a4bfd..7dd8c7bd4 100644 Binary files a/rqalpha/mod/rqalpha_mod_sys_analyser/report/templates/summary.xlsx and b/rqalpha/mod/rqalpha_mod_sys_analyser/report/templates/summary.xlsx differ diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py index 43c2d1503..1029d8e0d 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py @@ -14,40 +14,39 @@ # 否则米筐科技有权追究相应的知识产权侵权责任。 # 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。 # 详细的授权流程,请联系 public@ricequant.com 获取。 +from typing import Optional from rqalpha.interface import AbstractFrontendValidator from rqalpha.const import POSITION_EFFECT -from rqalpha.utils.logger import user_system_log +from rqalpha.model.order import Order +from rqalpha.portfolio.account import Account +from rqalpha.environment import Environment from rqalpha.utils.i18n import gettext as _ -def is_cash_enough(env, order, cash, warn=False): +def validate_cash(env: Environment, order: Order, cash: float) -> Optional[str]: instrument = env.data_proxy.instrument(order.order_book_id) cost_money = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction, order.trading_datetime.date()) cost_money += env.get_order_transaction_cost(order) if cost_money <= cash: - return True - if warn: - user_system_log.warn( - _("Order Creation Failed: not enough money to buy {order_book_id}, needs {cost_money:.2f}," - " cash {cash:.2f}").format( - order_book_id=order.order_book_id, - cost_money=cost_money, - cash=cash, - ) - ) - return False + return None + reason = _("Order Creation Failed: not enough money to buy {order_book_id}, needs {cost_money:.2f}," + " cash {cash:.2f}").format( + order_book_id=order.order_book_id, + cost_money=cost_money, + cash=cash) + return reason class CashValidator(AbstractFrontendValidator): def __init__(self, env): self._env = env - def can_submit_order(self, order, account=None): + 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) or (order.position_effect != POSITION_EFFECT.OPEN): - return True - return is_cash_enough(self._env, order, account.cash, warn=True) - - def can_cancel_order(self, order, account=None): - return True + return None + return validate_cash(self._env, order, account.cash) diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py index 46da9915e..c7854feae 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py @@ -14,9 +14,12 @@ # 否则米筐科技有权追究相应的知识产权侵权责任。 # 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。 # 详细的授权流程,请联系 public@ricequant.com 获取。 +from typing import Optional from rqalpha.interface import AbstractFrontendValidator +from rqalpha.model.order import Order +from rqalpha.portfolio.account import Account from rqalpha.utils.logger import user_system_log from rqalpha.utils.i18n import gettext as _ from rqalpha.const import INSTRUMENT_TYPE @@ -25,23 +28,22 @@ class IsTradingValidator(AbstractFrontendValidator): def __init__(self, env): self._env = env - - def can_submit_order(self, order, account=None): + + def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]: instrument = self._env.data_proxy.instrument(order.order_book_id) if instrument.type != INSTRUMENT_TYPE.INDX and not instrument.listing_at(self._env.trading_dt): - user_system_log.warn(_(u"Order Creation Failed: {order_book_id} is not listing!").format( - order_book_id=order.order_book_id, - )) - return False + reason = _(u"Order Creation Failed: {order_book_id} is not listing!").format( + order_book_id=order.order_book_id) + return reason if instrument.type == 'CS' and self._env.data_proxy.is_suspended(order.order_book_id, self._env.trading_dt): - user_system_log.warn(_(u"Order Creation Failed: security {order_book_id} is suspended on {date}").format( + reason = _(u"Order Creation Failed: security {order_book_id} is suspended on {date}").format( order_book_id=order.order_book_id, date=self._env.trading_dt.date() - )) - return False - - return True + ) + return reason - def can_cancel_order(self, order, account=None): - return True + return None + + def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]: + return None diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py index 353a9466f..5c44af8b0 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py @@ -11,10 +11,12 @@ # 未经米筐科技授权,任何个人不得出于任何商业目的使用本软件(包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务),任何法人或其他组织不得出于任何目的使用本软件,否则米筐科技有权追究相应的知识产权侵权责任。 # 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。 # 详细的授权流程,请联系 public@ricequant.com 获取。 - +from typing import Optional from rqalpha.interface import AbstractFrontendValidator from rqalpha.const import ORDER_TYPE, POSITION_EFFECT +from rqalpha.model.order import Order +from rqalpha.portfolio.account import Account from rqalpha.utils.logger import user_system_log from rqalpha.utils.i18n import gettext as _ @@ -23,11 +25,11 @@ class PriceValidator(AbstractFrontendValidator): def __init__(self, env): self._env = env - - def can_submit_order(self, order, account=None): + + def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]: if (order.type != ORDER_TYPE.LIMIT) or (order.position_effect == POSITION_EFFECT.EXERCISE): - return True - + return None + # FIXME: it may be better to round price in data source limit_up = round(self._env.price_board.get_limit_up(order.order_book_id), 4) if order.price > limit_up: @@ -39,8 +41,7 @@ def can_submit_order(self, order, account=None): limit_price=order.price, limit_up=limit_up ) - user_system_log.warn(reason) - return False + return reason limit_down = round(self._env.price_board.get_limit_down(order.order_book_id), 4) if order.price < limit_down: @@ -52,10 +53,8 @@ def can_submit_order(self, order, account=None): limit_price=order.price, limit_down=limit_down ) - user_system_log.warn(reason) - return False - - return True + return reason + return None - def can_cancel_order(self, order, account=None): - return True + def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]: + return None \ No newline at end of file diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py index 4f610d295..6505b5396 100644 --- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py @@ -11,38 +11,37 @@ # 未经米筐科技授权,任何个人不得出于任何商业目的使用本软件(包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务),任何法人或其他组织不得出于任何目的使用本软件,否则米筐科技有权追究相应的知识产权侵权责任。 # 在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。 # 详细的授权流程,请联系 public@ricequant.com 获取。 - +from typing import Optional from rqalpha.interface import AbstractFrontendValidator from rqalpha.const import ORDER_TYPE, SIDE, POSITION_EFFECT from rqalpha.utils.i18n import gettext as _ from rqalpha.utils.logger import user_system_log +from rqalpha.model.order import Order +from rqalpha.portfolio.account import Account class SelfTradeValidator(AbstractFrontendValidator): def __init__(self, env): self._env = env - def can_submit_order(self, order, account=None): + def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]: open_orders = [o for o in self._env.get_open_orders(order.order_book_id) if ( o.side != order.side and o.position_effect != POSITION_EFFECT.EXERCISE )] if len(open_orders) == 0: - return True + return None reason = _("Create order failed, there are active orders leading to the risk of self-trade: [{}...]") if order.type == ORDER_TYPE.MARKET: - user_system_log.warn(reason.format(open_orders[0])) - return False + return reason.format(open_orders[0]) if order.side == SIDE.BUY: for open_order in open_orders: if order.price >= open_order.price: - user_system_log.warn(reason.format(open_order)) - return False + return reason.format(open_order) else: for open_order in open_orders: if order.price <= open_order.price: - user_system_log.warn(reason.format(open_order)) - return False - - def can_cancel_order(self, order, account=None): - return True + return reason.format(open_order) + + def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]: + return None diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py b/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py index 9df8ff3b2..936732a82 100644 --- a/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py +++ b/rqalpha/mod/rqalpha_mod_sys_simulation/validator.py @@ -1,6 +1,8 @@ +from typing import Optional -from rqalpha.model.order import ALGO_ORDER_STYLES +from rqalpha.model.order import ALGO_ORDER_STYLES, Order from rqalpha.interface import AbstractFrontendValidator +from rqalpha.portfolio.account import Account class OrderStyleValidator(AbstractFrontendValidator): @@ -8,11 +10,11 @@ class OrderStyleValidator(AbstractFrontendValidator): def __init__(self, frequency): self._frequency = frequency - def can_submit_order(self, order, account=None): + def validate_submission(self, order: Order, account: Optional[Account] = None) -> Optional[str]: 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 - + raise RuntimeError("algo order no support 1m and tick frequency") + return None + + def validate_cancellation(self, order: Order, account: Optional[Account] = None) -> Optional[str]: + return None + diff --git a/rqalpha/portfolio/account.py b/rqalpha/portfolio/account.py index c5e1d6a11..0e3488b0e 100644 --- a/rqalpha/portfolio/account.py +++ b/rqalpha/portfolio/account.py @@ -101,7 +101,6 @@ def register_event(self): EVENT.TRADE, lambda e: self.apply_trade(e.trade, e.order) if e.account == self else None ) event_bus.add_listener(EVENT.ORDER_PENDING_NEW, self._on_order_pending_new) - event_bus.add_listener(EVENT.ORDER_CREATION_REJECT, self._on_order_unsolicited_update) event_bus.add_listener(EVENT.ORDER_UNSOLICITED_UPDATE, self._on_order_unsolicited_update) event_bus.add_listener(EVENT.ORDER_CANCELLATION_PASS, self._on_order_unsolicited_update) diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo index 54398e717..f52afca10 100644 Binary files a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo and b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.mo differ diff --git a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po index 39dfd419c..153806315 100644 --- a/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po +++ b/rqalpha/utils/translations/zh_Hans_CN/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-01-19 17:56+0800\n" +"POT-Creation-Date: 2024-04-12 15:23+0800\n" "PO-Revision-Date: 2016-10-24 21:20+0800\n" "Last-Translator: FULL NAME \n" "Language: zh_Hans_CN\n" @@ -18,13 +18,13 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.13.1\n" -#: rqalpha/environment.py:70 +#: rqalpha/environment.py:74 msgid "" "Environment has not been created. Please Use `Environment.get_instance()`" " after RQAlpha init" msgstr "Environment 并没有被创建,请再 RQAlpha 初始化之后使用 `Environment.get_instance() 函数`" -#: rqalpha/environment.py:163 +#: rqalpha/environment.py:180 msgid "No such transaction cost decider, order_book_id = {}" msgstr "" @@ -40,44 +40,44 @@ msgid "" "persist" msgstr "Persist provider 未加载,加载 Persist provider 方可使用 persist 功能。" -#: rqalpha/main.py:126 +#: rqalpha/main.py:127 msgid "rqdatac init failed, some apis will not function properly: {}" msgstr "" "rqdatac 初始化失败,部分扩展 API 可能无法使用,错误信息:{}。您可以访问 " "https://www.ricequant.com/welcome/rqdata 获取 rqdatac" -#: rqalpha/main.py:219 +#: rqalpha/main.py:220 msgid "system restored" msgstr "" -#: rqalpha/main.py:249 +#: rqalpha/main.py:250 msgid "strategy run successfully, normal exit" msgstr "策略运行成功,正常退出" -#: rqalpha/main.py:254 +#: rqalpha/main.py:255 msgid "strategy execute exception" msgstr "策略运行产生异常" -#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:286 -#: rqalpha/apis/api_base.py:322 +#: rqalpha/apis/api_base.py:69 rqalpha/apis/api_base.py:283 +#: rqalpha/apis/api_base.py:319 msgid "unsupported order_book_id type" msgstr "不支持该 order_book_id 类型" #: rqalpha/apis/api_base.py:164 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:60 msgid "Main Future contracts[88] are not supported in paper trading." msgstr "期货主力连续合约[88] 在实盘模拟中暂不支持。" #: rqalpha/apis/api_base.py:168 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:62 msgid "Index Future contracts[99] are not supported in paper trading." msgstr "期货指数连续合约[99] 在实盘模拟中暂不支持。" -#: rqalpha/apis/api_base.py:174 +#: rqalpha/apis/api_base.py:173 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:104 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:142 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:346 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:103 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:140 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:343 msgid "Order Creation Failed: [{order_book_id}] No market data" msgstr "订单创建失败: 当前合约[{order_book_id}]没有市场数据。" @@ -263,19 +263,19 @@ msgstr "策略暂停中,取消执行 {}({}, {})" msgid "deprecated parameter[bar_dict] in before_trading function." msgstr "[Deprecated]在before_trading函数中,第二个参数bar_dict已经不再使用了。" -#: rqalpha/data/bundle.py:454 +#: rqalpha/data/bundle.py:460 msgid "" "RQAlpha already supports backtesting using futures historical margins and" " rates, please upgrade RQDatac to version 2.11.12 and above to use it" msgstr "RQAlpha 已支持使用期货历史保证金和费率进行回测,请将 RQDatac 升级至 2.11.12 及以上版本进行使用" -#: rqalpha/data/bundle.py:464 rqalpha/data/bundle.py:522 +#: rqalpha/data/bundle.py:470 rqalpha/data/bundle.py:531 msgid "" "Futures historical trading parameters data is being updated, please " "wait......" msgstr "正在更新期货历史交易参数,请稍后......" -#: rqalpha/data/bundle.py:516 +#: rqalpha/data/bundle.py:522 rqalpha/data/bundle.py:706 msgid "" "File {} update failed, if it is using, please update later, or you can " "delete then update again" @@ -305,12 +305,12 @@ msgstr "" "将其更新至最新后再进行使用" #: rqalpha/data/base_data_source/storages.py:97 -#: rqalpha/data/base_data_source/storages.py:123 +#: rqalpha/data/base_data_source/storages.py:124 msgid "unsupported future instrument {}" msgstr "不支持的期货标的{}" -#: rqalpha/data/base_data_source/storages.py:194 -#: rqalpha/data/base_data_source/storages.py:204 +#: rqalpha/data/base_data_source/storages.py:195 +#: rqalpha/data/base_data_source/storages.py:205 msgid "" "open data bundle failed, you can remove {} and try to regenerate bundle: " "{}" @@ -338,18 +338,18 @@ msgstr "" msgid "mod tear_down [END] {}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:314 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_model.py:318 msgid "{order_book_id} is expired, close all positions by system" msgstr "{order_book_id} 已退市/交割,系统自动平仓" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:41 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:40 msgid "" "Order Creation Failed: not enough today position {order_book_id} to " "close, target quantity is {quantity}, closable today quantity is " "{closable}" msgstr "订单创建失败:{order_book_id} 的今仓不足,目标平今仓量为 {quantity},可平今仓量为 {closable}" -#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:50 +#: rqalpha/mod/rqalpha_mod_sys_accounts/position_validator.py:49 msgid "" "Order Creation Failed: not enough position {order_book_id} to close or " "exercise, target sell quantity is {quantity}, closable quantity is " @@ -358,28 +358,28 @@ msgstr "订单创建失败:{order_book_id} 的仓位不足,目标平仓/行 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:50 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:112 -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:158 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:157 msgid "Order Creation Failed: 0 order quantity, order_book_id={order_book_id}" msgstr "{order_book_id} 的订单创建失败:下单数量为 0" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:55 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:56 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:100 msgid "Limit order price should not be nan." msgstr "限价单价格不能为 nan。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:78 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:76 msgid "" "Order Creation Failed: close today amount {amount} is larger than today " "closable quantity {quantity}" msgstr "订单创建失败: 平今仓量 {amount} 大于当前可平今仓位 {quantity} " -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:89 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:88 msgid "" "Order Creation Failed: close amount {amount} is larger than position " "quantity {quantity}" msgstr "订单创建失败: 平仓量 {amount} 大于当前持仓量 {quantity}" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:113 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:111 msgid "" "Order was separated, original order: {original_order_repr}, new orders: " "[{new_orders_repr}]" @@ -391,11 +391,11 @@ msgid "" "again!" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:120 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:119 msgid "insufficient cash, use all remaining cash({}) to create order" msgstr "现金不足,使用当前剩余资金({})创建订单" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:334 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:332 #, fuzzy msgid "" "function order_target_portfolio: invalid keys of target_portfolio, " @@ -404,30 +404,29 @@ msgstr "" "函数 order_target_portfolio:不合法的参数。target_portfolio 的键应为合约代码或 Instrument " "对象,实际传入了 {}(类型:{})。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:339 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:337 msgid "" "function order_target_portfolio: invalid instrument type, excepted " "CS/ETF/LOF/INDX, got {}" msgstr "函数 order_target_portfolio:不合法的资产类型。应传入股票、ETF、LOF 或指数,实际传入了 {}。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:354 -#, fuzzy +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:351 msgid "" "function order_target_portfolio: invalid values of target_portfolio, " "excepted float between 0 and 1, got {} (key: {})" msgstr "函数 order_target_portfolio:不合法的目标权重,应传入 0 和 1 之间的浮点数,实际传入了 {}(类型:{})。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:363 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:360 msgid "total percent should be lower than 1, current: {}" msgstr "目标持仓占比总和应小于 1,当前值:{}。" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:383 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:380 msgid "" "Adjust position of {id_or_ins} Failed: Invalid close/open price " "{close_price}/{open_price}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:698 +#: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:693 msgid "in get_dividend, start_date {} is later than the previous test day {}" msgstr "在 get_dividend 函数中,start_date {} 晚于当前回测时间的上一个交易日 {}。" @@ -479,27 +478,31 @@ msgstr "" msgid "[sys_analyser] Generate report from strategy output file" msgstr "【sys_analyser】使用策略输出的文件生成报告" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:112 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:113 msgid "" "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' " "instead" msgstr "配置'base.benchmark'已被弃用,使用'mod.sys_analyser.benchmark'代替" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:143 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:151 msgid "benchmark {} not exists, please entry correct order_book_id" -msgstr "基准标的({})信息不存在,请输入正确的标的代码" +msgstr "基准标的 {} 信息不存在,请输入正确的标的代码" + +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:163 +msgid "benchmark {} missing data between backtest start date {} and end date {}" +msgstr "基准标的 {} 在回测起始日期 {} 结束日期 {} 间缺失数据" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:147 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:175 msgid "" -"benchmark {} listed date {} > backtest start date {} or de_listed date {}" -" <= backtest end date {}" -msgstr "基准标的({})上市日期({})晚于回测起始日期({})或退市日期({})早于回测结束日期({})" +"benchmark {} available data start date {} >= backtest start date {} or end" +" date {} <= backtest end date {}" +msgstr "基准标的 {} 的可用行情数据开始日期 {} >= 回测起始日期 {} 或结束日期 {} <= 回测结束日期 {}" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:210 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:243 msgid "invalid init benchmark {}, should be in format 'order_book_id:weight'" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:215 +#: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:248 msgid "invalid weight for instrument {order_book_id}: {weight}" msgstr "" @@ -709,34 +712,33 @@ msgstr "月度波动率" msgid "ExcessWinRate" msgstr "超额胜率" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:34 msgid "" "Order Creation Failed: not enough money to buy {order_book_id}, needs " "{cost_money:.2f}, cash {cash:.2f}" msgstr "订单创建失败: 可用资金不足。当前资金: {cash:.2f},{order_book_id} 下单所需资金: {cost_money:.2f}" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:32 -#, fuzzy +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:35 msgid "Order Creation Failed: {order_book_id} is not listing!" msgstr "订单创建失败: {order_book_id} 未上市或已退市" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:38 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/is_trading_validator.py:40 msgid "Order Creation Failed: security {order_book_id} is suspended on {date}" msgstr "订单创建失败: {order_book_id} 在 {date} 时停牌" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:34 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:36 msgid "" "Order Creation Failed: limit order price {limit_price} is higher than " "limit up {limit_up}, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:47 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/price_validator.py:48 msgid "" "Order Creation Failed: limit order price {limit_price} is lower than " "limit down {limit_down}, order_book_id={order_book_id}" msgstr "" -#: rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py:32 +#: rqalpha/mod/rqalpha_mod_sys_risk/validators/self_trade_validator.py:34 msgid "" "Create order failed, there are active orders leading to the risk of self-" "trade: [{}...]" diff --git a/setup.cfg b/setup.cfg index 690f7f17a..0cbbaa59f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ [metadata] name = rqalpha -version = 5.3.8 +version = 5.3.9 [versioneer] VCS = git diff --git a/tests/outs/test_s_scheduler.pkl b/tests/outs/test_s_scheduler.pkl index 272556ab7..cd49e7be9 100644 Binary files a/tests/outs/test_s_scheduler.pkl and b/tests/outs/test_s_scheduler.pkl differ