diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 24cb8225a..4bc819c5d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ CHANGELOG ================== +4.12.0 +================== +- 调整分析指标中部分指标的显示格式为百分比 +- 将分析指标中的超额收益波动率从日度改为年化 +- 入金支持延迟到账 + 4.11.3 ================== - 分析指标新增胜率和盈亏比,调整了作图中指标的布局 diff --git a/rqalpha/apis/api_base.py b/rqalpha/apis/api_base.py index 9dd858bf3..722a0e638 100644 --- a/rqalpha/apis/api_base.py +++ b/rqalpha/apis/api_base.py @@ -865,17 +865,17 @@ def symbol(order_book_id, sep=", "): EXECUTION_PHASE.SCHEDULED, EXECUTION_PHASE.GLOBAL ) -def deposit(account_type, amount): - # type: (str, float) -> None +def deposit(account_type: str, amount: float, receiving_days: int = 0): """ 入金(增加账户资金) :param account_type: 账户类型 - :param amount: 增加金额 + :param amount: 入金金额 + :param receiving_days: 入金到账天数,0 表示立刻到账,1 表示资金在下一个交易日盘前到账 :return: None """ env = Environment.get_instance() - return env.portfolio.deposit_withdraw(account_type, amount) + return env.portfolio.deposit_withdraw(account_type, amount, receiving_days) @export_as_api diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py index 963a24b63..874ede664 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py @@ -323,7 +323,7 @@ def tear_down(self, code, exception=None): 'tracking_error': risk.annual_tracking_error, 'sortino': risk.sortino, 'volatility': risk.annual_volatility, - 'excess_volatility': risk.excess_volatility, + 'excess_volatility': risk.excess_annual_volatility, 'excess_annual_volatility': risk.excess_annual_volatility, 'max_drawdown': risk.max_drawdown, 'excess_max_drawdown': risk.excess_max_drawdown, @@ -368,7 +368,8 @@ def tear_down(self, code, exception=None): df = pd.DataFrame(self._total_portfolios) df['date'] = pd.to_datetime(df['date']) total_portfolios = df.set_index('date').sort_index() - weekly_nav = df.resample("W", on="date").last().set_index("date").unit_net_value.dropna() + df.index = df['date'] + weekly_nav = df.resample("W").last().set_index("date").unit_net_value.dropna() weekly_returns = (weekly_nav / weekly_nav.shift(1).fillna(1)).fillna(0) - 1 # 最长回撤持续期 @@ -403,7 +404,8 @@ def tear_down(self, code, exception=None): df = pd.DataFrame(self._total_benchmark_portfolios) df['date'] = pd.to_datetime(df['date']) benchmark_portfolios = df.set_index('date').sort_index() - weekly_b_nav = df.resample("W", on="date").last().set_index("date").unit_net_value.dropna() + df.index = df['date'] + weekly_b_nav = df.resample("W").last().set_index("date").unit_net_value.dropna() weekly_b_returns = (weekly_b_nav / weekly_b_nav.shift(1).fillna(1)).fillna(0) - 1 result_dict['benchmark_portfolio'] = benchmark_portfolios # 超额收益最长回撤持续期 diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py index 2b90ccdc8..9da49710b 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py @@ -79,16 +79,16 @@ ], [ IndicatorInfo("benchmark_total_returns", _("BenchmarkReturns"), BLUE, "{0:.3%}", 11), IndicatorInfo("benchmark_annualized_returns", _("BenchmarkAnnual"), BLUE, "{0:.3%}", 11), - IndicatorInfo("volatility", _("Volatility"), BLACK, "{0:.4}", 11), - IndicatorInfo("tracking_error", _("TrackingError"), BLACK, "{0:.4}", 11), + IndicatorInfo("volatility", _("Volatility"), BLACK, "{0:.3%}", 11), + IndicatorInfo("tracking_error", _("TrackingError"), BLACK, "{0:.3%}", 11), IndicatorInfo("downside_risk", _("DownsideRisk"), BLACK, "{0:.4}", 11), IndicatorInfo("information_ratio", _("InformationRatio"), BLACK, "{0:.4}", 11), ], [ IndicatorInfo("excess_cum_returns", _("ExcessCumReturns"), BLACK, "{0:.3%}", 11), - IndicatorInfo("win_rate", _("WinRate"), BLACK, "{0:.4}", 11), - IndicatorInfo("weekly_win_rate", _("WeeklyWinRate"), BLACK, "{0:.4}", 11), + IndicatorInfo("win_rate", _("WinRate"), BLACK, "{0:.3%}", 11), + IndicatorInfo("weekly_win_rate", _("WeeklyWinRate"), BLACK, "{0:.3%}", 11), IndicatorInfo("profit_loss_rate", _("ProfitLossRate"), BLACK, "{0:.4}", 11), - IndicatorInfo("max_drawdown", _("MaxDrawDown"), BLACK, "{0:.4}", 11), + IndicatorInfo("max_drawdown", _("MaxDrawDown"), BLACK, "{0:.3%}", 11), IndicatorInfo("max_dd_ddd", _("MaxDD/MaxDDD"), BLACK, "{}", 6), ]] @@ -97,15 +97,15 @@ IndicatorInfo("weekly_beta", _("WeeklyBeta"), BLACK, "{0:.4}", 11), IndicatorInfo("weekly_sharpe", _("WeeklySharpe"), BLACK, "{0:.4}", 11), IndicatorInfo("weekly_information_ratio", _("WeeklyInfoRatio"), BLACK, "{0:.4}", 11), - IndicatorInfo("weekly_tracking_error", _("WeeklyTrackingError"), BLACK, "{0:.4}", 11), - IndicatorInfo("weekly_max_drawdown", _("WeeklyMaxDrawdown"), BLACK, "{0:.4}", 11), + IndicatorInfo("weekly_tracking_error", _("WeeklyTrackingError"), BLACK, "{0:.3%}", 11), + IndicatorInfo("weekly_max_drawdown", _("WeeklyMaxDrawdown"), BLACK, "{0:.3%}", 11), ] EXCESS_INDICATORS = [ IndicatorInfo("excess_returns", _("ExcessReturns"), BLACK, "{0:.3%}", 11), IndicatorInfo("excess_annual_returns", _("ExcessAnnual"), BLACK, "{0:.3%}", 11), IndicatorInfo("excess_sharpe", _("ExcessSharpe"), BLACK, "{0:.4}", 11), - IndicatorInfo("excess_volatility", _("ExcessVolatility"), BLACK, "{0:.4}", 11), - IndicatorInfo("excess_max_drawdown", _("ExcessMaxDD"), BLACK, "{0:.4}", 11), + IndicatorInfo("excess_volatility", _("ExcessVolatility"), BLACK, "{0:.3%}", 11), + IndicatorInfo("excess_max_drawdown", _("ExcessMaxDD"), BLACK, "{0:.3%}", 11), IndicatorInfo("excess_max_dd_ddd", _("ExcessMaxDD/ExcessMaxDDD"), BLACK, "{}", 6), ] diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py index 4d70d659a..47c376960 100644 --- a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py +++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py @@ -74,8 +74,7 @@ def max_ddd(arr: array, index: DatetimeIndex) -> IndexRange: def weekly_returns(portfolio: DataFrame) -> Series: - return portfolio.unit_net_value.reset_index().resample( - "W", on="date").last().set_index("date").unit_net_value.dropna() - 1 + return portfolio.unit_net_value.resample("W").last().dropna() - 1 def trading_dates_index(trades: DataFrame, position_effect, index: DatetimeIndex): diff --git a/rqalpha/portfolio/__init__.py b/rqalpha/portfolio/__init__.py index 051810508..18deab5a8 100644 --- a/rqalpha/portfolio/__init__.py +++ b/rqalpha/portfolio/__init__.py @@ -270,16 +270,16 @@ def cash_liabilities(self): def _pre_before_trading(self, _): self._static_unit_net_value = self.unit_net_value - def deposit_withdraw(self, account_type, amount): + def deposit_withdraw(self, account_type, amount, receiving_days=0): """出入金""" # 入金 现金增加,份额增加,总权益增加,单位净值不变 # 出金 现金减少,份额减少,总权益减少,单位净值不变 if account_type not in self._accounts: raise ValueError(_("invalid account type {}, choose in {}".format(account_type, list(self._accounts.keys())))) unit_net_value = self.unit_net_value - self._accounts[account_type].deposit_withdraw(amount) + self._accounts[account_type].deposit_withdraw(amount, receiving_days) _units = self.total_value / unit_net_value - user_log.info(_("Cash add {}. units {} become to {}".format(amount, self._units ,_units))) + user_log.info(_("Cash add {}. units {} become to {}".format(amount, self._units, _units))) self._units = _units def finance_repay(self, amount, account_type): diff --git a/rqalpha/portfolio/account.py b/rqalpha/portfolio/account.py index ebad4c8c4..df7ace100 100644 --- a/rqalpha/portfolio/account.py +++ b/rqalpha/portfolio/account.py @@ -16,6 +16,7 @@ # 详细的授权流程,请联系 public@ricequant.com 获取。 from itertools import chain +from datetime import date from typing import Callable, Dict, Iterable, List, Optional, Union, Tuple import six @@ -60,10 +61,12 @@ def __init__( ): self._type = account_type self._total_cash = total_cash # 包含保证金的总资金 + self._env = Environment.get_instance() self._positions: Dict[str, Dict[POSITION_DIRECTION, Position]] = {} self._backward_trade_set = set() self._frozen_cash = 0 + self._pending_deposit_withdraw: List[Tuple[date, float]] = [] self._cash_liabilities = 0 # 现金负债 @@ -91,7 +94,7 @@ def __repr__(self): ) def register_event(self): - event_bus = Environment.get_instance().event_bus + event_bus = self._env.event_bus event_bus.add_listener( EVENT.TRADE, lambda e: self.apply_trade(e.trade, e.order) if e.account == self else None ) @@ -276,12 +279,14 @@ def equity(self): return sum(p.equity for p in self._iter_pos()) @property - def total_value(self): - # type: () -> float + def total_value(self) -> float: """ 账户总权益 """ - return self._total_cash + self.equity - self.cash_liabilities - self.cash_liabilities_interest + total_value = self._total_cash + self.equity - self.cash_liabilities - self.cash_liabilities_interest + if self._pending_deposit_withdraw: + total_value += sum(amount for _, amount in self._pending_deposit_withdraw) + return total_value @property def total_cash(self): @@ -312,7 +317,11 @@ def _on_before_trading(self, _): if all(p.quantity == 0 and p.equity == 0 for p in six.itervalues(positions)): del self._positions[order_book_id] - trading_date = Environment.get_instance().trading_dt.date() + trading_date = self._env.trading_dt.date() + while self._pending_deposit_withdraw and self._pending_deposit_withdraw[0][0] <= trading_date: + _, amount = self._pending_deposit_withdraw.pop(0) + self._total_cash += amount + for position in self._iter_pos(): self._total_cash += position.before_trading(trading_date) @@ -321,7 +330,7 @@ def _on_before_trading(self, _): self._cash_liabilities += self.cash_liabilities_interest def _on_settlement(self, event): - trading_date = Environment.get_instance().trading_dt.date() + trading_date = self._env.trading_dt.date() for order_book_id, positions in list(self._positions.items()): for position in six.itervalues(positions): @@ -335,7 +344,7 @@ def _on_settlement(self, event): self._total_cash -= fee # 如果 total_value <= 0 则认为已爆仓,清空仓位,资金归0 - forced_liquidation = Environment.get_instance().config.base.forced_liquidation + forced_liquidation = self._env.config.base.forced_liquidation if self.total_value <= 0 and forced_liquidation: if self._positions: user_system_log.warn(_("Trigger Forced Liquidation, current total_value is 0")) @@ -395,7 +404,6 @@ def _get_or_create_pos( init_price : Optional[float] = None ) -> Position: if order_book_id not in self._positions: - env = Environment.get_instance() if direction == POSITION_DIRECTION.LONG: long_quantity, short_quantity = init_quantity, 0 else: @@ -405,7 +413,7 @@ def _get_or_create_pos( POSITION_DIRECTION.SHORT: Position(order_book_id, POSITION_DIRECTION.SHORT, short_quantity, init_price) }) if not init_price: - last_price = env.get_last_price(order_book_id) + last_price = self._env.get_last_price(order_book_id) for p in positions.values(): p.update_last_price(last_price) if hasattr(positions[direction], "margin") and hasattr(self.__class__, "_margin"): @@ -426,21 +434,19 @@ def _on_tick(self, event): position.update_last_price(tick.last) def _on_bar(self, _): - env = Environment.get_instance() for order_book_id, positions in self._positions.items(): - price = env.get_last_price(order_book_id) + price = self._env.get_last_price(order_book_id) if price == price: for position in six.itervalues(positions): position.update_last_price(price) def _frozen_cash_of_order(self, order): - env = Environment.get_instance() if order.position_effect == POSITION_EFFECT.OPEN: - instrument = env.data_proxy.instrument(order.order_book_id) + instrument = self._env.data_proxy.instrument(order.order_book_id) order_cost = instrument.calc_cash_occupation(order.frozen_price, order.quantity, order.position_direction) else: order_cost = 0 - return order_cost + env.get_order_transaction_cost(order) + return order_cost + self._env.get_order_transaction_cost(order) def _management_fee(self): # type: () -> float @@ -478,12 +484,16 @@ def management_fees(self): """该账户的管理费用总计""" return self._management_fees - def deposit_withdraw(self, amount): - # type: (float) -> None + def deposit_withdraw(self, amount: float, receiving_days: int = 0): """出入金""" if (amount < 0) and (self.cash < amount * -1): raise ValueError(_('insufficient cash, current {}, target withdrawal {}').format(self._total_cash, amount)) - self._total_cash += amount + if receiving_days >= 1: + receiving_date = self._env.data_proxy.get_next_trading_date(self._env.trading_dt.date(), n=receiving_days) + self._pending_deposit_withdraw.append((receiving_date, amount)) + self._pending_deposit_withdraw.sort(key=lambda i: i[0]) + else: + self._total_cash += amount def finance_repay(self, amount): """ 融资还款 """ diff --git a/setup.cfg b/setup.cfg index 8b0584cbc..20ad7ce09 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ [metadata] name = rqalpha -version = 4.11.3 +version = 4.12.0 [versioneer] VCS = git diff --git a/setup.py b/setup.py index ea78ca389..f60b7fdce 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ 'simplejson', 'PyYAML', 'tabulate', - 'rqrisk >=1.0.0', + 'rqrisk >=1.0.3', 'h5py', 'matplotlib >=2.2.0', "openpyxl" diff --git a/tests/api_tests/test_api_base.py b/tests/api_tests/test_api_base.py index c53f2d6eb..eaf83be4f 100644 --- a/tests/api_tests/test_api_base.py +++ b/tests/api_tests/test_api_base.py @@ -482,4 +482,10 @@ def handle_bar(context, bar_dict): else: assert False, "未报出当前账户可取出金额不足异常" + deposit("STOCK", 10000, 3) + context.cash = context.portfolio.accounts["STOCK"].cash + + elif context.counter == 9: + assert int(context.portfolio.accounts["STOCK"].cash) == int(context.cash) + 10000 + return locals() diff --git a/tests/outs/test_f_buy_and_hold.pkl b/tests/outs/test_f_buy_and_hold.pkl index 88d6c9037..f5c41e35b 100644 Binary files a/tests/outs/test_f_buy_and_hold.pkl and b/tests/outs/test_f_buy_and_hold.pkl differ diff --git a/tests/outs/test_f_macd.pkl b/tests/outs/test_f_macd.pkl index 400ff3d6b..707e7bae5 100644 Binary files a/tests/outs/test_f_macd.pkl and b/tests/outs/test_f_macd.pkl differ diff --git a/tests/outs/test_f_macd_signal.pkl b/tests/outs/test_f_macd_signal.pkl index 30bd38db3..c6f36a818 100644 Binary files a/tests/outs/test_f_macd_signal.pkl and b/tests/outs/test_f_macd_signal.pkl differ diff --git a/tests/outs/test_f_mean_reverting.pkl b/tests/outs/test_f_mean_reverting.pkl index 5621c94dc..96413aa69 100644 Binary files a/tests/outs/test_f_mean_reverting.pkl and b/tests/outs/test_f_mean_reverting.pkl differ diff --git a/tests/outs/test_s_buy_and_hold.pkl b/tests/outs/test_s_buy_and_hold.pkl index df94042cc..42ef970c5 100644 Binary files a/tests/outs/test_s_buy_and_hold.pkl and b/tests/outs/test_s_buy_and_hold.pkl differ diff --git a/tests/outs/test_s_dual_thrust.pkl b/tests/outs/test_s_dual_thrust.pkl index 71546cd43..f55e4a93b 100644 Binary files a/tests/outs/test_s_dual_thrust.pkl and b/tests/outs/test_s_dual_thrust.pkl differ diff --git a/tests/outs/test_s_scheduler.pkl b/tests/outs/test_s_scheduler.pkl index 7f16d2275..5f5f3139f 100644 Binary files a/tests/outs/test_s_scheduler.pkl and b/tests/outs/test_s_scheduler.pkl differ diff --git a/tests/outs/test_s_tick_size.pkl b/tests/outs/test_s_tick_size.pkl index e19bbea99..6aa71d528 100644 Binary files a/tests/outs/test_s_tick_size.pkl and b/tests/outs/test_s_tick_size.pkl differ diff --git a/tests/outs/test_s_turtle.pkl b/tests/outs/test_s_turtle.pkl index b5ad399c9..ede01c7b0 100644 Binary files a/tests/outs/test_s_turtle.pkl and b/tests/outs/test_s_turtle.pkl differ diff --git a/tests/outs/test_s_turtle_signal.pkl b/tests/outs/test_s_turtle_signal.pkl index 49b7e00de..510e2a674 100644 Binary files a/tests/outs/test_s_turtle_signal.pkl and b/tests/outs/test_s_turtle_signal.pkl differ diff --git a/tests/outs/test_sf_buy_and_hold.pkl b/tests/outs/test_sf_buy_and_hold.pkl index ab540dc51..7016c81f8 100644 Binary files a/tests/outs/test_sf_buy_and_hold.pkl and b/tests/outs/test_sf_buy_and_hold.pkl differ