Repository: ricequant/rqalpha (language: python)

Issue:
Feature/plot open close point



Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 31e29a4e5..886e33e2e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -2,7 +2,16 @@
 CHANGELOG
 ==================
 
-4.5.3
+4.7.0
+==================
+- 重构绘制策略收益图的逻辑。增加绘制买卖点的选项，周度收益曲线和指标改为通过配置项激活
+- 调整 Signal 模式的逻辑。现在在 Signal 模式下开启 price_limit 选项时，超出涨跌停价格范围的订单将会被拒绝
+- 重构国际化逻辑。现在 RQAlpha 将自动检测操作系统的语言设置从而在中英文间进行切换（而不是强制使用中文）
+- 优化部分日志和错误信息的中文翻译
+- 调整了部分在 Python3.10 下会报错的代码
+
+
+4.6.0
 ==================
 - 策略运行结束后绘制的收益图中加入超额收益曲线、周度收益曲线，加入部分超额收益和周度风险指标
 - 策略运行结束后输出的结果中加入部分超额收益和周度风险指标
diff --git a/messages.pot b/messages.pot
index 963c1116a..f34e5e2a7 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: 2021-09-08 11:05+0800\n"
+"POT-Creation-Date: 2021-11-29 11:41+0800\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -43,38 +43,38 @@ msgstr ""
 msgid "rqdatac init failed, some apis will not function properly: {}"
 msgstr ""
 
-#: rqalpha/main.py:210
+#: rqalpha/main.py:211
 msgid "system restored"
 msgstr ""
 
-#: rqalpha/main.py:243
+#: rqalpha/main.py:244
 msgid "strategy run successfully, normal exit"
 msgstr ""
 
-#: rqalpha/main.py:248
+#: rqalpha/main.py:249
 msgid "strategy execute exception"
 msgstr ""
 
-#: rqalpha/apis/api_base.py:68 rqalpha/apis/api_base.py:267
-#: rqalpha/apis/api_base.py:303
+#: rqalpha/apis/api_base.py:67 rqalpha/apis/api_base.py:266
+#: rqalpha/apis/api_base.py:302
 msgid "unsupported order_book_id type"
 msgstr ""
 
-#: rqalpha/apis/api_base.py:88 rqalpha/apis/api_base.py:92
+#: rqalpha/apis/api_base.py:87 rqalpha/apis/api_base.py:91
 msgid "Limit order price should not be nan."
 msgstr ""
 
-#: rqalpha/apis/api_base.py:149
+#: rqalpha/apis/api_base.py:148
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59
 msgid "Main Future contracts[88] are not supported in paper trading."
 msgstr ""
 
-#: rqalpha/apis/api_base.py:153
+#: rqalpha/apis/api_base.py:152
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61
 msgid "Index Future contracts[99] are not supported in paper trading."
 msgstr ""
 
-#: rqalpha/apis/api_base.py:159
+#: rqalpha/apis/api_base.py:158
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:90
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:137
@@ -125,29 +125,41 @@ msgid ""
 "got {} (type: {})"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:45
+#: rqalpha/cmds/bundle.py:33
+msgid "create bundle using RQDatac"
+msgstr ""
+
+#: rqalpha/cmds/bundle.py:43
 msgid ""
 "rqdatac is required to create bundle. you can visit "
 "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha"
 " download-bundle\" to download monthly updated bundle."
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:56 rqalpha/cmds/bundle.py:86
+#: rqalpha/cmds/bundle.py:54 rqalpha/cmds/bundle.py:83
 msgid "rqdatac init failed with error: {}"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:75
+#: rqalpha/cmds/bundle.py:62
+msgid "Update bundle using RQDatac"
+msgstr ""
+
+#: rqalpha/cmds/bundle.py:72
 msgid ""
 "rqdatac is required to update bundle. you can visit "
 "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha"
 " download-bundle\" to download monthly updated bundle."
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:90
+#: rqalpha/cmds/bundle.py:87
 msgid "bundle not exist, use \"rqalpha create-bundle\" command instead"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:109
+#: rqalpha/cmds/bundle.py:94
+msgid "Download bundle (monthly updated)"
+msgstr ""
+
+#: rqalpha/cmds/bundle.py:105
 msgid ""
 "\n"
 "    [WARNING]\n"
@@ -156,24 +168,44 @@ msgid ""
 "    Are you sure to continue?"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:127
+#: rqalpha/cmds/bundle.py:123
 msgid "Data bundle download successfully in {bundle_path}"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:138
+#: rqalpha/cmds/bundle.py:134
 msgid "try {} ..."
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:150
+#: rqalpha/cmds/bundle.py:146
 msgid "downloading ..."
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:164
+#: rqalpha/cmds/bundle.py:160
 msgid ""
 "\n"
 "Download failed, retry in {} seconds."
 msgstr ""
 
+#: rqalpha/cmds/misc.py:26
+msgid "Generate example strategies to target folder"
+msgstr ""
+
+#: rqalpha/cmds/misc.py:41
+msgid "Output Version Info"
+msgstr ""
+
+#: rqalpha/cmds/misc.py:48
+msgid "Generate default config file"
+msgstr ""
+
+#: rqalpha/cmds/mod.py:29
+msgid "Mod management command"
+msgstr ""
+
+#: rqalpha/cmds/run.py:27
+msgid "Run a strategy"
+msgstr ""
+
 #: rqalpha/core/execution_context.py:111
 #, python-format
 msgid "You cannot call %s when executing %s"
@@ -288,6 +320,54 @@ msgstr ""
 msgid "in get_dividend, start_date {} is later than the previous test day {}"
 msgstr ""
 
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:64
+msgid "[sys_analyser] save report"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:69
+msgid "[sys_analyser] output result pickle file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:74
+msgid "[sys_analyser] plot result"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:79
+msgid "[sys_analyser] save plot to file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:84
+msgid "[sys_analyser] order_book_id of benchmark"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:89
+msgid "[sys_analyser] show open close points on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:94
+msgid "[sys_analyser] show weekly indicators and return curve on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:98
+msgid "[sys_analyser] Plot from strategy output file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:101
+msgid "save plot result to file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:102
+msgid "show open close points on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103
+msgid "show weekly indicators and return curve on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:113
+msgid "[sys_analyser] Generate report from strategy output file"
+msgstr ""
+
 #: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:100
 msgid ""
 "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' "
@@ -302,143 +382,151 @@ msgstr ""
 msgid "invalid weight for instrument {order_book_id}: {weight}"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:71
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:59
+msgid "Strategy"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:60
+msgid "Benchmark"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61
+msgid "Excess"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62
+msgid "Weekly"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63
+msgid "BenchmarkWeekly"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:65
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:82
+msgid "MaxDrawDown"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:66
+msgid "MaxDDD"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67
+msgid "Open"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68
+msgid "Close"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:71
 msgid "TotalReturns"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:72
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:72
 msgid "AnnualReturns"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:73
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:73
 msgid "Alpha"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:74
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:74
 msgid "Beta"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:75
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:75
 msgid "Sharpe"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:76
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:76
 msgid "Sortino"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:77
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:77
 msgid "InformationRatio"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:79
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:79
 msgid "BenchmarkReturns"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:80
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:80
 msgid "BenchmarkAnnual"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:81
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:81
 msgid "Volatility"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:82
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:199
-msgid "MaxDrawdown"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:83
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:83
 msgid "TrackingError"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:84
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:84
 msgid "DownsideRisk"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:85
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:85
 msgid "MaxDD/MaxDDD"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:87
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:89
 msgid "WeeklyAlpha"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:88
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:90
 msgid "WeeklyBeta"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:89
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:91
 msgid "WeeklySharpe"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:90
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:92
 msgid "WeeklySortino"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:91
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:93
 msgid "WeeklyInfoRatio"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:92
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:94
 msgid "WeeklyTrackingError"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:93
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:95
 msgid "WeeklyMaxDrawdown"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:97
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:99
 msgid "ExcessReturns"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:98
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:100
 msgid "ExcessAnnual"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:99
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:101
 msgid "ExcessSharpe"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:100
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:102
 msgid "ExcessVolatility"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:101
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:103
 msgid "ExcessAnnualVolatility"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:102
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:104
 msgid "ExcessMaxDD"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:103
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105
 msgid "ExcessMaxDD/ExcessMaxDDD"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:189
-msgid "Strategy"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:191
-msgid "Benchmark"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:193
-msgid "Excess"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:194
-msgid "Weekly"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:196
-msgid "BenchmarkWeekly"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:200
-msgid "MaxDDD"
-msgstr ""
-
 #: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33
 msgid ""
 "Order Creation Failed: not enough money to buy {order_book_id}, needs "
@@ -488,10 +576,12 @@ msgid "Order Cancelled: current bar [{order_book_id}] miss market data."
 msgstr ""
 
 #: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:148
+#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:89
 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price."
 msgstr ""
 
 #: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:154
+#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:98
 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price."
 msgstr ""
 
@@ -546,14 +636,6 @@ msgstr ""
 msgid "cancel_order function is not supported in signal mode"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:90
-msgid "You have traded {order_book_id} with {quantity} lots in limit up status"
-msgstr ""
-
-#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:97
-msgid "You have traded {order_book_id} with {quantity} lots in limit down status"
-msgstr ""
-
 #: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:104
 msgid "unsupported position_effect {}"
 msgstr ""
@@ -627,7 +709,7 @@ msgstr ""
 msgid "insufficient cash, current {}, target withdrawal {}"
 msgstr ""
 
-#: rqalpha/portfolio/position.py:169
+#: rqalpha/portfolio/position.py:166
 msgid "last price of position {} is not supposed to be nan"
 msgstr ""
 
@@ -720,7 +802,7 @@ msgstr ""
 #: rqalpha/utils/arg_checker.py:344
 msgid ""
 "function {}: invalid {} argument, frequency should be in form of '1m', "
-"'5m', '1d', got {} (type: {})"
+"'5m', '1d', '1w' got {} (type: {})"
 msgstr ""
 
 #: rqalpha/utils/class_helper.py:23
@@ -729,37 +811,37 @@ msgid ""
 "more information"
 msgstr ""
 
-#: rqalpha/utils/config.py:104
+#: rqalpha/utils/config.py:106
 msgid "in parse_user_config, exception: {e}"
 msgstr ""
 
-#: rqalpha/utils/config.py:206
+#: rqalpha/utils/config.py:196
 msgid "Invalid future info: underlying_symbol {} is illegal."
 msgstr ""
 
-#: rqalpha/utils/config.py:221
+#: rqalpha/utils/config.py:211
 msgid ""
 "Invalid future info: commission_type is suppose to be BY_MONEY or "
 "BY_VOLUME"
 msgstr ""
 
-#: rqalpha/utils/config.py:225
+#: rqalpha/utils/config.py:215
 msgid "Invalid future info: field {} is not valid"
 msgstr ""
 
-#: rqalpha/utils/config.py:255
+#: rqalpha/utils/config.py:245
 msgid "invalid init position {}, should be in format 'order_book_id:quantity'"
 msgstr ""
 
-#: rqalpha/utils/config.py:260
+#: rqalpha/utils/config.py:250
 msgid "invalid quantity for instrument {order_book_id}: {quantity}"
 msgstr ""
 
-#: rqalpha/utils/config.py:275
+#: rqalpha/utils/config.py:265
 msgid "unknown run type: {}"
 msgstr ""
 
-#: rqalpha/utils/config.py:288
+#: rqalpha/utils/config.py:278
 msgid "unknown persist mode: {}"
 msgstr ""
 
@@ -770,6 +852,6 @@ msgid ""
 msgstr ""
 
 #: rqalpha/utils/functools.py:55
-msgid "function {} are not supported, please check your account or mod config"
+msgid "function {} is not supported, please check your account or mod config"
 msgstr ""
 
diff --git a/rqalpha/__init__.py b/rqalpha/__init__.py
index a55557d0d..e81ef149f 100644
--- a/rqalpha/__init__.py
+++ b/rqalpha/__init__.py
@@ -20,7 +20,7 @@
 """
 from typing import Optional
 
-from rqalpha.cmds import cli
+from rqalpha.cmds import cli, inject_run_param
 from rqalpha.api import export_as_api
 from rqalpha.apis import subscribe_event
 from . import data
@@ -215,4 +215,7 @@ def handle_bar(context, bar_dict):
 del get_versions
 
 version_info = tuple(int(v) if v.isdigit() else v for v in __version__.split('.'))
-__main_version__ = "%s.%s.x" % (version_info[0], version_info[1])
+try:
+    __main_version__ = "%s.%s.x" % (version_info[0], version_info[1])
+except:
+    __main_version__ = "0.0"
\ No newline at end of file
diff --git a/rqalpha/apis/api_base.py b/rqalpha/apis/api_base.py
index 195be1372..75294db2c 100644
--- a/rqalpha/apis/api_base.py
+++ b/rqalpha/apis/api_base.py
@@ -17,9 +17,8 @@
 from __future__ import division
 
 import types
-from collections.abc import Iterable
 from datetime import date, datetime
-from typing import Callable, List, Optional, Union
+from typing import Callable, List, Optional, Union, Iterable
 
 import pandas as pd
 import numpy as np
diff --git a/rqalpha/cmds/__init__.py b/rqalpha/cmds/__init__.py
index 61c2f52e8..0084d6443 100644
--- a/rqalpha/cmds/__init__.py
+++ b/rqalpha/cmds/__init__.py
@@ -18,3 +18,4 @@
 from . import run
 from . import misc
 from .entry import cli
+from .run import inject_run_param
diff --git a/rqalpha/cmds/bundle.py b/rqalpha/cmds/bundle.py
index 90b034c7d..42163eedb 100644
--- a/rqalpha/cmds/bundle.py
+++ b/rqalpha/cmds/bundle.py
@@ -19,7 +19,6 @@
 import time
 import datetime
 import dateutil
-from six.moves.urllib.parse import urlparse
 
 import click
 import requests
@@ -31,14 +30,13 @@
 from rqalpha.utils import init_rqdatac_env
 
 
-@cli.command()
+@cli.command(help=_("create bundle using RQDatac"))
 @click.option('-d', '--data-bundle-path', default=os.path.expanduser('~/.rqalpha'), type=click.Path(file_okay=False))
 @click.option("rqdatac_uri", '--rqdatac', '--rqdatac-uri', default=None,
               help='rqdatac uri, eg user:password or tcp://user:password@ip:port')
 @click.option('--compression', default=False, is_flag=True, help='enable compression to reduce file size')
 @click.option('-c', '--concurrency', type=click.INT, default=1)
 def create_bundle(data_bundle_path, rqdatac_uri, compression, concurrency):
-    """create bundle using rqdatac"""
     try:
         import rqdatac
     except ImportError:
@@ -61,14 +59,13 @@ def create_bundle(data_bundle_path, rqdatac_uri, compression, concurrency):
     update_bundle_(os.path.join(data_bundle_path, 'bundle'), True, compression, concurrency)
 
 
-@cli.command()
+@cli.command(help=_("Update bundle using RQDatac"))
 @click.option('-d', '--data-bundle-path', default=os.path.expanduser('~/.rqalpha'), type=click.Path(file_okay=False))
 @click.option("rqdatac_uri", '--rqdatac', '--rqdatac-uri', default=None,
               help='rqdatac uri, eg user:password or tcp://user:password@ip:port')
 @click.option('--compression', default=False, type=click.BOOL, help='enable compression to reduce file size')
 @click.option('-c', '--concurrency', type=click.INT, default=1)
 def update_bundle(data_bundle_path, rqdatac_uri, compression, concurrency):
-    """update bundle using rqdatac"""
     try:
         import rqdatac
     except ImportError:
@@ -94,11 +91,10 @@ def update_bundle(data_bundle_path, rqdatac_uri, compression, concurrency):
     update_bundle_(os.path.join(data_bundle_path, 'bundle'), False, compression, concurrency)
 
 
-@cli.command()
+@cli.command(help=_("Download bundle (monthly updated)"))
 @click.option('-d', '--data-bundle-path', default=os.path.expanduser('~/.rqalpha'), type=click.Path(file_okay=False))
 @click.option('--confirm', default=True, is_flag=True)
 def download_bundle(data_bundle_path, confirm):
-    """download bundle (monthly updated)"""
     default_bundle_path = os.path.abspath(os.path.expanduser('~/.rqalpha/bundle'))
     if data_bundle_path is None:
         data_bundle_path = default_bundle_path
diff --git a/rqalpha/cmds/misc.py b/rqalpha/cmds/misc.py
index a388ce9e9..8664b14a2 100644
--- a/rqalpha/cmds/misc.py
+++ b/rqalpha/cmds/misc.py
@@ -19,16 +19,13 @@
 import click
 import six
 
+from rqalpha.utils.i18n import gettext as _
 from .entry import cli
 
 
-@cli.command()
+@cli.command(help=_("Generate example strategies to target folder"))
 @click.option('-d', '--directory', default="./", type=click.Path(), required=True)
 def examples(directory):
-    """
-    Generate example strategies to target folder
-    """
-
     import rqalpha
     source_dir = os.path.join(os.path.dirname(rqalpha.__file__), "examples")
 
@@ -41,22 +38,16 @@ def examples(directory):
             six.print_("Folder examples exists.")
 
 
-@cli.command()
+@cli.command(help=_("Output Version Info"))
 @click.option('-v', '--version', is_flag=True)
 def version(**kwargs):
-    """
-    Output Version Info
-    """
     from rqalpha import __version__
     six.print_("Current Version: ", __version__)
 
 
-@cli.command()
+@cli.command(help=_("Generate default config file"))
 @click.option('-d', '--directory', default="./", type=click.Path(), required=True)
 def generate_config(directory):
-    """
-    Generate default config file
-    """
     default_config = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "config.yml")
     target_config_path = os.path.abspath(os.path.join(directory, 'config.yml'))
     shutil.copy(default_config, target_config_path)
diff --git a/rqalpha/cmds/mod.py b/rqalpha/cmds/mod.py
index c777b1054..97c498f31 100644
--- a/rqalpha/cmds/mod.py
+++ b/rqalpha/cmds/mod.py
@@ -19,13 +19,14 @@
 import six
 import click
 
+from rqalpha.utils.i18n import gettext as _
 from rqalpha.utils.config import dump_config
 from .entry import cli
 
 
 @cli.command(context_settings=dict(
     ignore_unknown_options=True,
-))
+), help=_("Mod management command"))
 @click.help_option('-h', '--help')
 @click.argument('cmd', nargs=1, type=click.Choice(['list', 'enable', 'disable']))
 @click.argument('params', nargs=-1)
diff --git a/rqalpha/cmds/run.py b/rqalpha/cmds/run.py
index 7587e04ba..8809be82e 100644
--- a/rqalpha/cmds/run.py
+++ b/rqalpha/cmds/run.py
@@ -1,5 +1,5 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2020 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
@@ -12,17 +12,19 @@
 #         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
+
 import os
 
 import click
 
+from rqalpha.utils.i18n import gettext as _
 from rqalpha.utils.click_helper import Date
 from rqalpha.utils.config import parse_config
 
 from .entry import cli
 
 
-@cli.command()
+@cli.command(help=_("Run a strategy"))
 @click.help_option('-h', '--help')
 # -- Base Configuration
 @click.option('-d', '--data-bundle-path', 'base__data_bundle_path', type=click.Path(exists=True))
@@ -42,7 +44,7 @@
 # -- Extra Configuration
 @click.option('-l', '--log-level', 'extra__log_level', type=click.Choice(['verbose', 'debug', 'info', 'error', 'none']))
 @click.option('--logger', 'extra__logger', nargs=2, multiple=True, help='config logger, e.g. --logger system_log debug')
-@click.option('--locale', 'extra__locale', type=click.Choice(['cn', 'en']), default="cn")
+@click.option('--locale', 'extra__locale', type=click.Choice(['cn', 'en']), default=None)
 @click.option('--extra-vars', 'extra__context_vars', type=click.STRING, help="override context vars")
 @click.option("--enable-profiler", "extra__enable_profiler", is_flag=True, default=None,
               help="add line profiler to profile your strategy")
@@ -52,9 +54,6 @@
 # for compatible
 @click.option('--resume', 'base__resume_mode', is_flag=True, help="[DEPRECATED] --resume is deprecated")
 def run(**kwargs):
-    """
-    Start to run a strategy
-    """
     config_path = kwargs.get('config_path', None)
     if config_path is not None:
         config_path = os.path.abspath(config_path)
@@ -80,3 +79,7 @@ def run(**kwargs):
 
     if results is None:
         return 1
+
+
+def inject_run_param(param: click.Parameter):
+    cli.commands["run"].params.append(param)
diff --git a/rqalpha/config.yml b/rqalpha/config.yml
index fcffcfc53..85fabfee6 100644
--- a/rqalpha/config.yml
+++ b/rqalpha/config.yml
@@ -50,5 +50,5 @@ extra:
   # enable_profiler: 是否启动性能分析
   enable_profiler: false
   is_hold: false
-  locale: zh_Hans_CN
+  locale: ~
   logger: []
diff --git a/rqalpha/const.py b/rqalpha/const.py
index 4b0b58154..1a4af6cf1 100644
--- a/rqalpha/const.py
+++ b/rqalpha/const.py
@@ -25,11 +25,7 @@ def __new__(metacls, cls, bases, classdict):
         return enum_class
 
     def __contains__(cls, member):
-        if super(CustomEnumMeta, cls).__contains__(member):
-            return True
-        if isinstance(member, str):
-            return member in cls._member_reverse_map
-        return False
+        return member in cls.__members__ or member in cls._member_reverse_map
 
     def __getitem__(self, item):
         try:
diff --git a/rqalpha/data/base_data_source/storages.py b/rqalpha/data/base_data_source/storages.py
index 92d6a50a0..d216955fb 100644
--- a/rqalpha/data/base_data_source/storages.py
+++ b/rqalpha/data/base_data_source/storages.py
@@ -22,6 +22,7 @@
 import sys
 from copy import copy
 from itertools import chain
+from contextlib import contextmanager
 from typing import Dict, Iterable, Optional
 
 import h5py
@@ -134,7 +135,7 @@ def get_share_transformation(self, order_book_id):
         return transformation_data["successor"], transformation_data["share_conversion_ratio"]
 
 
-def open_h5(path, *args, **kwargs):
+def _file_path(path):
     # why do this? non-ascii path in windows!!
     if sys.platform == "win32":
         try:
@@ -142,15 +143,35 @@ def open_h5(path, *args, **kwargs):
         except TypeError:
             l = None
         if l and l.lower() == "utf-8":
-            path = path.encode("utf-8")
+            return path.encode("utf-8")
+    return path
+
+
+def open_h5(path, *args, **kwargs):
+    # forward compatible
     try:
-        return h5py.File(path, *args, **kwargs)
+        return h5py.File(_file_path(path), *args, **kwargs)
     except OSError as e:
         raise RuntimeError(_(
             "open data bundle failed, you can remove {} and try to regenerate bundle: {}"
         ).format(path, e))
 
 
+@contextmanager
+def h5_file(path, *args, mode="r", **kwargs):
+    try:
+        h5 = h5py.File(_file_path(path), *args, mode=mode, **kwargs)
+    except OSError as e:
+        raise RuntimeError(_(
+            "open data bundle failed, you can remove {} and try to regenerate bundle: {}"
+        ).format(path, e))
+    else:
+        try:
+            yield h5
+        finally:
+            h5.close()
+
+
 class DayBarStore(AbstractDayBarStore):
     DEFAULT_DTYPE = np.dtype([
         ('datetime', np.uint64),
@@ -164,20 +185,22 @@ class DayBarStore(AbstractDayBarStore):
     def __init__(self, path):
         if not os.path.exists(path):
             raise FileExistsError("File {} not exist，please update bundle.".format(path))
-        self._h5 = open_h5(path, mode="r")
+        self._path = path
 
     def get_bars(self, order_book_id):
-        try:
-            return self._h5[order_book_id][:]
-        except KeyError:
-            return np.empty(0, dtype=self.DEFAULT_DTYPE)
+        with h5_file(self._path) as h5:
+            try:
+                return h5[order_book_id][:]
+            except KeyError:
+                return np.empty(0, dtype=self.DEFAULT_DTYPE)
 
     def get_date_range(self, order_book_id):
-        try:
-            data = self._h5[order_book_id]
-            return data[0]['datetime'], data[-1]['datetime']
-        except KeyError:
-            return 20050104, 20050104
+        with h5_file(self._path) as h5:
+            try:
+                data = h5[order_book_id]
+                return data[0]['datetime'], data[-1]['datetime']
+            except KeyError:
+                return 20050104, 20050104
 
 
 class FutureDayBarStore(DayBarStore):
@@ -186,18 +209,20 @@ class FutureDayBarStore(DayBarStore):
 
 class DividendStore(AbstractDividendStore):
     def __init__(self, path):
-        self._h5 = open_h5(path, mode="r")
+        self._path = path
 
     def get_dividend(self, order_book_id):
-        try:
-            return self._h5[order_book_id][:]
-        except KeyError:
-            return None
+        with h5_file(self._path) as h5:
+            try:
+                return h5[order_book_id][:]
+            except KeyError:
+                return None
 
 
 class YieldCurveStore:
     def __init__(self, path):
-        self._data = open_h5(path, mode="r")["data"][:]
+        with h5_file(path) as h5:
+            self._data = h5["data"][:]
 
     def get_yield_curve(self, start_date, end_date, tenor):
         d1 = convert_date_to_date_int(start_date)
@@ -225,26 +250,28 @@ def get_yield_curve(self, start_date, end_date, tenor):
 
 class SimpleFactorStore(AbstractSimpleFactorStore):
     def __init__(self, path):
-        self._h5 = open_h5(path, mode="r")
+        self._path = path
 
     def get_factors(self, order_book_id):
-        try:
-            return self._h5[order_book_id][:]
-        except KeyError:
-            return None
+        with h5_file(self._path) as h5:
+            try:
+                return h5[order_book_id][:]
+            except KeyError:
+                return None
 
 
 class DateSet(AbstractDateSet):
     def __init__(self, f):
-        self._h5 = open_h5(f, mode="r")
+        self._f = f
 
     @lru_cache(None)
     def get_days(self, order_book_id):
-        try:
-            days = self._h5[order_book_id][:]
+        with h5_file(self._f) as h5:
+            try:
+                days = h5[order_book_id][:]
+            except KeyError:
+                return set()
             return set(days.tolist())
-        except KeyError:
-            return set()
 
     def contains(self, order_book_id, dates):
         date_set = self.get_days(order_book_id)
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst b/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst
index 15869efd1..df9a5aaa1 100644
--- a/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/README.rst
@@ -43,6 +43,13 @@ RQAlpha 策略分析 Mod。
         'plot': False,
         # 收益曲线图路径，若设置则将收益曲线图保存为 png 文件
         'plot_save_file': None,
+        # 收益曲线图设置
+        'plot_config': {
+            # 是否在收益图中展示买卖点
+            'open_close_points': False,
+            # 是否在收益图中展示周度指标和收益曲线
+            'weekly_indicators': False
+        },
     }
 
 您可以通过如下方式来修改模块的配置信息，比如下面的示例中介绍了如何开启显示回测收益曲线图
@@ -72,20 +79,7 @@ RQAlpha 策略分析 Mod。
 扩展命令
 ===============================
 
-在启用该 Mod 的情况下，您可以使用如下功能:
-
-*   :code:`rqalpha run` 命令增加 :code:`--report target_csv_path` 选项，您可以指定将报告以 :code:`csv` 格式输出至 :code:`target_csv_path` 路径
-*   :code:`rqalpha run` 命令增加 :code:`--output-file target_pickle_path` / :code:`-o target_pickle_path` 选项，您可以将每日  :code:`Portfolio` / :code:`Trade` 数据以 :code:`pickle` 文件格式输出到 :code:`target_pickle_path` 路径
-*   :code:`rqalpha run` 命令增加 :code:`--plot/--no-plot` / :code:`-p` 选项，您可以以图形的方式显示收益曲线图
-*   :code:`rqalpha run` 命令增加 :code:`--plot-save target_plot_img_path` 选项，您可以将收益曲线图输出至 :code:`target_plot img_path` 路径
-
-..  code-block:: bash
-
-    $ rqalpha run -f strategy.py --report target_csv_path -o target_pickle_path --plot --plot-save target_plot_img_path
-
-*   增加 :code:`rqalpha plot` 命令，根据生成的 :code:`pickle` 文件来显示收益曲线图
-    *   :code:`--show/--hide` 选项，是否显示收益曲线图
-    *   :code:`--plot-save target_plot_img_path` 选项，您可以将收益曲线图输出至 :code:`target_plot img_path` 路径
+在启用该 Mod 的情况下，您可以使用如下命令:
 
 ..  code-block:: bash
 
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py b/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py
index 69510da3a..428ccafaa 100644
--- a/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py
@@ -1,19 +1,25 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
+
 import click
-from rqalpha import cli
+
+from rqalpha.utils.i18n import gettext as _
+from rqalpha import cli, inject_run_param
 
 __config__ = {
     # 策略基准，该基准将用于风险指标计算和收益曲线图绘制
@@ -30,6 +36,13 @@
     'plot': False,
     # 收益曲线图路径，若设置则将收益曲线图保存为 png 文件
     'plot_save_file': None,
+    # 收益曲线图设置
+    'plot_config': {
+        # 是否在收益图中展示买卖点
+        'open_close_points': False,
+        # 是否在收益图中展示周度指标和收益曲线
+        'weekly_indicators': False
+    },
 }
 
 
@@ -45,65 +58,62 @@ def load_mod():
 """
 cli_prefix = "mod__sys_analyser__"
 
-cli.commands['run'].params.append(
-    click.Option(
-        ('--report', 'mod__sys_analyser__report_save_path'),
-        type=click.Path(writable=True),
-        help="[sys_analyser] save report"
-    )
-)
-cli.commands['run'].params.append(
-    click.Option(
-        ('-o', '--output-file', 'mod__sys_analyser__output_file'),
-        type=click.Path(writable=True),
-        help="[sys_analyser] output result pickle file"
-    )
-)
-cli.commands['run'].params.append(
-    click.Option(
-        ('-p', '--plot/--no-plot', 'mod__sys_analyser__plot'),
-        default=None,
-        help="[sys_analyser] plot result"
-    )
-)
-cli.commands['run'].params.append(
-    click.Option(
-        ('--plot-save', 'mod__sys_analyser__plot_save_file'),
-        default=None,
-        help="[sys_analyser] save plot to file"
-    )
-)
-cli.commands["run"].params.append(
-    click.Option(
-        ("-bm", "--benchmark", cli_prefix + "benchmark"),
-        type=click.STRING,
-        help="[sys_analyser] order_book_id of benchmark"
-    )
-)
-
-
-@cli.command()
+inject_run_param(click.Option(
+    ('--report', 'mod__sys_analyser__report_save_path'),
+    type=click.Path(writable=True),
+    help=_("[sys_analyser] save report")
+))
+inject_run_param(click.Option(
+    ('-o', '--output-file', 'mod__sys_analyser__output_file'),
+    type=click.Path(writable=True),
+    help=_("[sys_analyser] output result pickle file")
+))
+inject_run_param(click.Option(
+    ('-p', '--plot/--no-plot', 'mod__sys_analyser__plot'),
+    default=None,
+    help=_("[sys_analyser] plot result")
+))
+inject_run_param(click.Option(
+    ('--plot-save', 'mod__sys_analyser__plot_save_file'),
+    default=None,
+    help=_("[sys_analyser] save plot to file")
+))
+inject_run_param(click.Option(
+    ("-bm", "--benchmark", cli_prefix + "benchmark"),
+    type=click.STRING,
+    help=_("[sys_analyser] order_book_id of benchmark")
+))
+inject_run_param(click.Option(
+    ('--plot-open-close-points', cli_prefix + "plot_config__open_close_points"),
+    is_flag=True, default=None,
+    help=_("[sys_analyser] show open close points on plot"),
+))
+inject_run_param(click.Option(
+    ("--plot-weekly-indicators", cli_prefix + "plot_config__weekly_indicators"),
+    is_flag=True, default=None,
+    help=_("[sys_analyser] show weekly indicators and return curve on plot")
+))
+
+
+@cli.command(help=_("[sys_analyser] Plot from strategy output file"))
 @click.argument('result_pickle_file_path', type=click.Path(exists=True), required=True)
 @click.option('--show/--hide', 'show', default=True)
-@click.option('--plot-save', 'plot_save_file', default=None, type=click.Path(), help="save plot result to file")
-def plot(result_pickle_file_path, show, plot_save_file):
-    """
-    [sys_analyser] draw result DataFrame
-    """
+@click.option('--plot-save', 'plot_save_file', default=None, type=click.Path(), help=_("save plot result to file"))
+@click.option('--plot-open-close-points', is_flag=True, help=_("show open close points on plot"))
+@click.option('--plot-weekly-indicators', is_flag=True, help=_("show weekly indicators and return curve on plot"))
+def plot(result_pickle_file_path, show, plot_save_file, plot_open_close_points, plot_weekly_indicators):
     import pandas as pd
     from .plot import plot_result
 
     result_dict = pd.read_pickle(result_pickle_file_path)
-    plot_result(result_dict, show, plot_save_file)
+    print(plot_open_close_points, plot_weekly_indicators)
+    plot_result(result_dict, show, plot_save_file, plot_weekly_indicators, plot_open_close_points)
 
 
-@cli.command()
+@cli.command(help=_("[sys_analyser] Generate report from strategy output file"))
 @click.argument('result_pickle_file_path', type=click.Path(exists=True), required=True)
 @click.argument('target_report_csv_path', type=click.Path(exists=True, writable=True), required=True)
 def report(result_pickle_file_path, target_report_csv_path):
-    """
-    [sys_analyser] Generate report from backtest output file
-    """
     import pandas as pd
     result_dict = pd.read_pickle(result_pickle_file_path)
 
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py
index 44430f288..bc088b224 100644
--- a/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/mod.py
@@ -404,6 +404,10 @@ def tear_down(self, code, exception=None):
 
         if self._mod_config.plot or self._mod_config.plot_save_file:
             from .plot import plot_result
-            plot_result(result_dict, self._mod_config.plot, self._mod_config.plot_save_file)
+            plot_config = self._mod_config.plot_config
+            plot_result(
+                result_dict, self._mod_config.plot, self._mod_config.plot_save_file,
+                plot_config.weekly_indicators, plot_config.open_close_points
+            )
 
         return result_dict
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot.py
deleted file mode 100644
index e2452634d..000000000
--- a/rqalpha/mod/rqalpha_mod_sys_analyser/plot.py
+++ /dev/null
@@ -1,308 +0,0 @@
-# -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
-#
-# 除非遵守当前许可，否则不得使用本软件。
-#
-#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
-#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
-#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
-#
-#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
-#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
-#         否则米筐科技有权追究相应的知识产权侵权责任。
-#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
-#         详细的授权流程，请联系 public@ricequant.com 获取。
-import os
-from collections import namedtuple, ChainMap
-
-import numpy as np
-import pandas as pd
-from matplotlib import rcParams, gridspec, ticker, image as mpimg, pyplot as plt
-from matplotlib.font_manager import findfont, FontProperties
-
-import rqalpha
-from rqalpha.utils.logger import system_log
-from rqalpha.utils.i18n import gettext as _
-
-plt.style.use('ggplot')
-
-rcParams['font.family'] = 'sans-serif'
-rcParams['font.sans-serif'] = [
-    'Microsoft Yahei', 'Heiti SC', 'Heiti TC', 'STHeiti', 'WenQuanYi Zen Hei',
-    'WenQuanYi Micro Hei', "文泉驿微米黑", 'SimHei', 'Arial Unicode MS'
-] + rcParams['font.sans-serif']
-rcParams['axes.unicode_minus'] = False
-
-font = findfont(FontProperties(family=['sans-serif']))
-if "/matplotlib/" in font:
-    system_log.warn("PLOT: Missing Chinese fonts. Fallback to English.")
-    LABEL_FONT_SIZE = 8
-    _ = lambda txt: txt
-else:
-    LABEL_FONT_SIZE = 10
-
-# 三部分的高度
-PLOT_AREA_HEIGHT = 4
-USER_PLOT_AREA_HEIGHT = 2
-
-IMG_WIDTH = 13
-
-Heights = namedtuple("Heights", ("label", "value"))
-Indicator = namedtuple("Indicator", ("key", "label", "color", "formatter", "value_font_size"))
-
-RED = "#aa4643"
-LIGHT_CORAL = "#F08080"
-BLUE = "#4572a7"
-YELLOW = "#F3A423"
-BLACK = "#000000"
-
-# 指标的高度值
-INDICATOR_Y_POS = [
-    Heights(1.3, 1.15),
-    Heights(0.95, 0.8),
-    Heights(0.6, 0.45),
-    Heights(0.25, 0.10),
-]
-INDICATOR_X_POS = [0.15 * i for i in range(7)]
-
-INDICATORS = [[
-    Indicator("total_returns", _("TotalReturns"), RED, "{0:.3%}", 11),
-    Indicator("annualized_returns", _("AnnualReturns"), RED, "{0:.3%}", 11),
-    Indicator("alpha", _("Alpha"), BLACK, "{0:.4}", 11),
-    Indicator("beta", _("Beta"), BLACK, "{0:.4}", 11),
-    Indicator("sharpe", _("Sharpe"), BLACK, "{0:.4}", 11),
-    Indicator("sortino", _("Sortino"), BLACK, "{0:.4}", 11),
-    Indicator("information_ratio", _("InformationRatio"), BLACK, "{0:.4}", 11),
-],[
-    Indicator("benchmark_total_returns", _("BenchmarkReturns"), BLUE, "{0:.3%}", 11),
-    Indicator("benchmark_annualized_returns", _("BenchmarkAnnual"), BLUE, "{0:.3%}", 11),
-    Indicator("volatility", _("Volatility"), BLACK, "{0:.4}", 11),
-    Indicator("max_drawdown", _("MaxDrawdown"), BLACK, "{0:.4}", 11),
-    Indicator("tracking_error", _("TrackingError"), BLACK, "{0:.4}", 11),
-    Indicator("downside_risk", _("DownsideRisk"), BLACK, "{0:.4}", 11),
-    Indicator("max_dd_ddd", _("MaxDD/MaxDDD"), BLACK, "{}", 6),
-], [
-    Indicator("weekly_alpha", _("WeeklyAlpha"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_beta", _("WeeklyBeta"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_sharpe", _("WeeklySharpe"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_sortino", _("WeeklySortino"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_information_ratio", _("WeeklyInfoRatio"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_tracking_error", _("WeeklyTrackingError"), BLACK, "{0:.4}", 11),
-    Indicator("weekly_max_drawdown", _("WeeklyMaxDrawdown"), BLACK, "{0:.4}", 11),
-]]
-
-excess_indicators = [
-    Indicator("excess_returns", _("ExcessReturns"), RED, "{0:.3%}", 11),
-    Indicator("excess_annual_returns", _("ExcessAnnual"), RED, "{0:.3%}", 11),
-    Indicator("excess_sharpe", _("ExcessSharpe"), BLACK, "{0:.4}", 11),
-    Indicator("excess_volatility", _("ExcessVolatility"), BLACK, "{0:.4}", 11),
-    Indicator("excess_annual_volatility", _("ExcessAnnualVolatility"), BLACK, "{0:.4}", 11),
-    Indicator("excess_max_drawdown", _("ExcessMaxDD"), BLACK, "{0:.4}", 11),
-    Indicator("excess_max_dd_ddd", _("ExcessMaxDD/ExcessMaxDDD"), BLACK, "{}", 6),
-]
-
-
-class IndexRange(namedtuple("IndexRange", ("start", "end", "start_date", "end_date"))):
-    @classmethod
-    def new(cls, start, end, index):
-        return cls(start, end, index[start].date(), index[end].date())
-
-    @property
-    def repr(self):
-        return "{}~{}, {} days".format(self.start_date, self.end_date, (self.end_date - self.start_date).days)
-
-    def plot(self, ax, returns, marker, color, label):
-        ax.plot(
-            [self.start_date, self.end_date], [returns[self.start], returns[self.end]],
-            marker, color=color, markersize=8, alpha=.7, label=label
-        )
-
-
-def _max_dd(arr, index):
-    # type: (np.array, pd.DatetimeIndex) -> IndexRange
-    end = np.argmax(np.maximum.accumulate(arr) / arr)
-    if end == 0:
-        end = len(arr) - 1
-    start = np.argmax(arr[:end]) if end > 0 else 0
-    return IndexRange.new(start, end, index)
-
-
-def _max_ddd(arr, index):
-    # type: (np.array, pd.DatetimeIndex) -> IndexRange
-    max_seen = arr[0]
-    ddd_start, ddd_end = 0, 0
-    ddd = 0
-    start = 0
-    in_draw_down = False
-
-    i = 0
-    for i in range(len(arr)):
-        if arr[i] > max_seen:
-            if in_draw_down:
-                in_draw_down = False
-                if i - start > ddd:
-                    ddd = i - start
-                    ddd_start = start
-                    ddd_end = i - 1
-            max_seen = arr[i]
-        elif arr[i] < max_seen:
-            if not in_draw_down:
-                in_draw_down = True
-                start = i - 1
-
-    if arr[i] < max_seen:
-        if i - start > ddd:
-            return IndexRange.new(start, i, index)
-
-    return IndexRange.new(ddd_start, ddd_end, index)
-
-
-def _plot_indicators(ax, indicator_values, plot_excess_indicators):
-    ax.axis("off")
-    if plot_excess_indicators:
-        all_indicators = INDICATORS + [excess_indicators]
-    else:
-        all_indicators = [[]] + INDICATORS
-
-    for lineno, indicators in enumerate(all_indicators):
-        for index_in_line, i in enumerate(indicators):
-            x = INDICATOR_X_POS[index_in_line]
-            y = INDICATOR_Y_POS[lineno]
-            try:
-                value = i.formatter.format(indicator_values[i.key])
-            except KeyError:
-                value = "nan"
-            ax.text(x, y.label, i.label, color=i.color, fontsize=LABEL_FONT_SIZE),
-            ax.text(x, y.value, value, color=BLACK, fontsize=i.value_font_size)
-
-
-def _plot_returns(ax, returns, b_returns, excess_returns, weekly_returns, weekly_b_returns, max_dd, max_ddd):
-    ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator())
-    ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator())
-    ax.grid(b=True, which='minor', linewidth=.2)
-    ax.grid(b=True, which='major', linewidth=1)
-    ax.patch.set_alpha(0.6)
-
-    # plot lines
-    ax.plot(returns, label=_(u"Strategy"), alpha=1, linewidth=2, color=RED)
-    if b_returns is not None:
-        ax.plot(b_returns, label=_(u"Benchmark"), alpha=1, linewidth=2, color=BLUE)
-    if excess_returns is not None:
-        ax.plot(excess_returns, label=_(u"Excess"), alpha=1, linewidth=2, color=YELLOW)
-    ax.plot(weekly_returns, label=_("Weekly"), alpha=0.6, linewidth=2, color=RED)
-    if weekly_b_returns is not None:
-        ax.plot(weekly_b_returns, label=_("BenchmarkWeekly"), alpha=0.6, linewidth=2, color=BLUE)
-
-    # plot MaxDD/MaxDDD
-    max_dd.plot(ax, returns, "v", "Green", _("MaxDrawdown"))
-    max_ddd.plot(ax, returns, "D", "Blue", _("MaxDDD"))
-
-    # place legend
-    leg = plt.legend(loc="best")
-    leg.get_frame().set_alpha(0.5)
-
-    # manipulate axis
-    vals = ax.get_yticks()
-    ax.set_yticklabels(['{:3.2f}%'.format(x * 100) for x in vals])
-
-
-def _plot_user_plots(ax, plots_df):
-    ax.patch.set_alpha(0.6)
-    for column in plots_df.columns:
-        ax.plot(plots_df[column], label=column)
-
-    leg = plt.legend(loc="best")
-    leg.get_frame().set_alpha(0.5)
-
-
-def plot_result(result_dict, show_windows=True, savefile=None):
-    summary = result_dict["summary"]
-
-    if "plots" in result_dict:
-        indicator_area_height = 3
-        img_height = indicator_area_height + PLOT_AREA_HEIGHT + USER_PLOT_AREA_HEIGHT + 1
-    else:
-        indicator_area_height = 2
-        img_height = indicator_area_height + PLOT_AREA_HEIGHT
-
-    logo_file = os.path.join(
-        os.path.dirname(os.path.realpath(rqalpha.__file__)),
-        "resource", 'ricequant-logo.png')
-    logo_img = mpimg.imread(logo_file)
-    dpi = logo_img.shape[1] / IMG_WIDTH * 1.1
-
-    title = summary['strategy_file']
-    fig = plt.figure(title, figsize=(IMG_WIDTH, img_height), dpi=dpi)
-
-    gs = gridspec.GridSpec(img_height, 8)
-
-    portfolio = result_dict["portfolio"]
-    benchmark_portfolio = result_dict.get("benchmark_portfolio")
-    max_dd = _max_dd(portfolio.unit_net_value.values, portfolio.index)
-    max_ddd = _max_ddd(portfolio.unit_net_value.values, portfolio.index)
-    # 超额收益
-    if benchmark_portfolio is not None:
-        ex_returns = portfolio.unit_net_value - benchmark_portfolio.unit_net_value
-        ex_max_dd = _max_dd(ex_returns + 1, portfolio.index)
-        ex_max_ddd = _max_ddd(ex_returns + 1, portfolio.index)
-        ex_max_dd_ddd = "MaxDD {}\nMaxDDD {}".format(ex_max_dd.repr, ex_max_ddd.repr)
-        plot_excess_indicators = True
-        benchmark_weekly_portfolio = benchmark_portfolio.unit_net_value.reset_index().resample(
-            "W", on="date").last().set_index("date").unit_net_value.dropna() - 1
-    else:
-        ex_max_dd_ddd = "nan"
-        ex_returns = None
-        benchmark_weekly_portfolio = None
-        plot_excess_indicators = False
-
-    _plot_indicators(
-        plt.subplot(gs[:indicator_area_height, :-1]),
-        ChainMap(summary, {
-            "max_dd_ddd": "MaxDD {}\nMaxDDD {}".format(max_dd.repr, max_ddd.repr),
-            "excess_max_dd_ddd": ex_max_dd_ddd,
-        }),
-        plot_excess_indicators
-    )
-
-    _plot_returns(
-        plt.subplot(gs[indicator_area_height: PLOT_AREA_HEIGHT + indicator_area_height, :]),
-        portfolio.unit_net_value - 1,
-        None if benchmark_portfolio is None else benchmark_portfolio["unit_net_value"] - 1,
-        ex_returns,
-        portfolio.unit_net_value.reset_index().resample(
-            "W", on="date").last().set_index("date").unit_net_value.dropna() - 1,
-        benchmark_weekly_portfolio,
-        max_dd,
-        max_ddd
-    )
-
-    if "plots" in result_dict:
-        _plot_user_plots(
-            plt.subplot(gs[indicator_area_height + PLOT_AREA_HEIGHT + 1:, :]),
-            result_dict["plots"]
-        )
-
-    # logo as watermark
-    fig.figimage(
-        logo_img,
-        xo=((IMG_WIDTH * dpi) - logo_img.shape[1]) / 2,
-        yo=(img_height * dpi - logo_img.shape[0]) / 2,
-        alpha=0.4,
-    )
-
-    if savefile:
-        fnmame = savefile
-        if os.path.isdir(savefile):
-            fnmame = os.path.join(savefile, "{}.png".format(summary["strategy_name"]))
-        plt.savefig(fnmame, bbox_inches='tight')
-
-    if show_windows:
-        plt.show()
-
-
-if __name__ == "__main__":
-    import pickle
-    with open("result.pkl", "rb") as f:
-        plot_result(pickle.loads(f.read()))
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/__init__.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/__init__.py
new file mode 100644
index 000000000..1d0256010
--- /dev/null
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
+#
+# 除非遵守当前许可，否则不得使用本软件。
+#
+#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
+#
+#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
+#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
+#         详细的授权流程，请联系 public@ricequant.com 获取。
+
+from rqalpha.mod.rqalpha_mod_sys_analyser.plot.plot import plot_result
+
+
+if __name__ == "__main__":
+    import pickle
+    with open("result.pkl", "rb") as f:
+        plot_result(pickle.loads(f.read()), weekly_indicators=True, open_close_points=True)
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py
new file mode 100644
index 000000000..510e970e8
--- /dev/null
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
+#
+# 除非遵守当前许可，否则不得使用本软件。
+#
+#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
+#
+#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
+#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
+#         详细的授权流程，请联系 public@ricequant.com 获取。
+
+from matplotlib import rcParams, pyplot as plt
+from matplotlib.font_manager import findfont, FontProperties
+
+from rqalpha.utils.logger import system_log
+from rqalpha.utils.i18n import gettext as _
+
+from .utils import IndicatorInfo, LineInfo, SpotInfo
+
+plt.style.use('ggplot')
+
+rcParams['font.family'] = 'sans-serif'
+rcParams['font.sans-serif'] = [
+    'Microsoft Yahei', 'Heiti SC', 'Heiti TC', 'STHeiti', 'WenQuanYi Zen Hei',
+    'WenQuanYi Micro Hei', "文泉驿微米黑", 'SimHei', 'Arial Unicode MS'
+] + rcParams['font.sans-serif']
+rcParams['axes.unicode_minus'] = False
+
+font = findfont(FontProperties(family=['sans-serif']))
+if "/matplotlib/" in font:
+    system_log.warn("PLOT: Missing Chinese fonts. Fallback to English.")
+    LABEL_FONT_SIZE = 8
+    _ = lambda txt: txt
+else:
+    LABEL_FONT_SIZE = 10
+
+RED = "#aa4643"
+BLUE = "#4572a7"
+YELLOW = "#F3A423"
+BLACK = "#000000"
+
+IMG_WIDTH = 13
+# 两部分的相对高度
+INDICATOR_AREA_HEIGHT = 2
+PLOT_AREA_HEIGHT = 4
+USER_PLOT_AREA_HEIGHT = 2
+
+# 指标的宽高
+INDICATOR_WIDTH = 0.15
+INDICATOR_VALUE_HEIGHT = 0.15
+INDICATOR_LABEL_HEIGHT = 0.2
+
+LINE_STRATEGY = LineInfo(_("Strategy"), RED, 1, 2)
+LINE_BENCHMARK = LineInfo(_("Benchmark"), BLUE, 1, 2)
+LINE_EXCESS = LineInfo(_("Excess"), YELLOW, 1, 2)
+LINE_WEEKLY = LineInfo(_("Weekly"), RED, 0.6, 2)
+LINE_WEEKLY_BENCHMARK = LineInfo(_("BenchmarkWeekly"), BLUE, 0.6, 2)
+
+MAX_DD = SpotInfo(_("MaxDrawDown"), "v", "Green", 8, 0.7)
+MAX_DDD = SpotInfo(_("MaxDDD"), "D", "Blue", 8, 0.7)
+OPEN_POINT = SpotInfo(_("Open"), "P", "#FF7F50", 8, 0.9)
+CLOSE_POINT = SpotInfo(_("Close"), "X", "#008B8B", 8, 0.9)
+
+INDICATORS = [[
+    IndicatorInfo("total_returns", _("TotalReturns"), RED, "{0:.3%}", 11),
+    IndicatorInfo("annualized_returns", _("AnnualReturns"), RED, "{0:.3%}", 11),
+    IndicatorInfo("alpha", _("Alpha"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("beta", _("Beta"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("sharpe", _("Sharpe"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("sortino", _("Sortino"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("information_ratio", _("InformationRatio"), BLACK, "{0:.4}", 11),
+], [
+    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("max_drawdown", _("MaxDrawDown"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("tracking_error", _("TrackingError"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("downside_risk", _("DownsideRisk"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("max_dd_ddd", _("MaxDD/MaxDDD"), BLACK, "{}", 6),
+]]
+
+WEEKLY_INDICATORS = [
+    IndicatorInfo("weekly_alpha", _("WeeklyAlpha"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("weekly_beta", _("WeeklyBeta"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("weekly_sharpe", _("WeeklySharpe"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("weekly_sortino", _("WeeklySortino"), 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),
+]
+
+EXCESS_INDICATORS = [
+    IndicatorInfo("excess_returns", _("ExcessReturns"), RED, "{0:.3%}", 11),
+    IndicatorInfo("excess_annual_returns", _("ExcessAnnual"), RED, "{0:.3%}", 11),
+    IndicatorInfo("excess_sharpe", _("ExcessSharpe"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("excess_volatility", _("ExcessVolatility"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("excess_annual_volatility", _("ExcessAnnualVolatility"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("excess_max_drawdown", _("ExcessMaxDD"), BLACK, "{0:.4}", 11),
+    IndicatorInfo("excess_max_dd_ddd", _("ExcessMaxDD/ExcessMaxDDD"), BLACK, "{}", 6),
+]
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/plot.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/plot.py
new file mode 100644
index 000000000..3ee994051
--- /dev/null
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/plot.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
+#
+# 除非遵守当前许可，否则不得使用本软件。
+#
+#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
+#
+#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
+#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
+#         详细的授权流程，请联系 public@ricequant.com 获取。
+
+import os
+from typing import List, Mapping, Tuple, Sequence, Optional
+from collections import ChainMap
+
+import pandas as pd
+from matplotlib.axes import Axes
+from matplotlib.figure import Figure
+from matplotlib import gridspec, ticker, image as mpimg, pyplot
+
+import rqalpha
+from rqalpha.const import POSITION_EFFECT
+from .utils import IndicatorInfo, LineInfo, max_ddd as _max_ddd, max_dd as _max_dd, SpotInfo
+from .utils import weekly_returns, trading_dates_index
+from .consts import INDICATOR_WIDTH, INDICATOR_VALUE_HEIGHT, INDICATOR_LABEL_HEIGHT
+from .consts import IMG_WIDTH, INDICATOR_AREA_HEIGHT, PLOT_AREA_HEIGHT, USER_PLOT_AREA_HEIGHT
+from .consts import LABEL_FONT_SIZE, BLACK
+from .consts import INDICATORS, WEEKLY_INDICATORS, EXCESS_INDICATORS, MAX_DD, MAX_DDD, OPEN_POINT, CLOSE_POINT
+from .consts import LINE_BENCHMARK, LINE_STRATEGY, LINE_WEEKLY_BENCHMARK, LINE_WEEKLY, LINE_EXCESS
+
+
+class SubPlot:
+    height: int
+    right_pad: Optional[int] = None
+
+    def plot(self, ax: Axes):
+        raise NotImplementedError
+
+
+class IndicatorArea(SubPlot):
+    height: int = INDICATOR_AREA_HEIGHT
+    right_pad = -1
+
+    def __init__(self, indicators: List[List[IndicatorInfo]], indicator_values: Mapping[str, float]):
+        self._indicators = indicators
+        self._values = indicator_values
+
+    def plot(self, ax: Axes):
+        ax.axis("off")
+        for lineno, indicators in enumerate(self._indicators[::-1]):  # lineno: 自下而上的行号
+            for index_in_line, i in enumerate(indicators):
+                x = index_in_line * INDICATOR_WIDTH
+                y_value = lineno * (INDICATOR_VALUE_HEIGHT + INDICATOR_LABEL_HEIGHT)
+                y_label = y_value + INDICATOR_LABEL_HEIGHT
+                try:
+                    value = i.formatter.format(self._values[i.key])
+                except KeyError:
+                    value = "nan"
+                ax.text(x, y_label, i.label, color=i.color, fontsize=LABEL_FONT_SIZE),
+                ax.text(x, y_value, value, color=BLACK, fontsize=i.value_font_size)
+
+
+class ReturnPlot(SubPlot):
+    height: int = PLOT_AREA_HEIGHT
+
+    def __init__(
+            self,
+            returns,
+            lines: List[Tuple[pd.Series, LineInfo]],
+            spots_on_returns: List[Tuple[Sequence[int], SpotInfo]]
+    ):
+        self._returns = returns
+        self._lines = lines
+        self._spots_on_returns = spots_on_returns
+
+    @classmethod
+    def _plot_line(cls, ax, returns, info: LineInfo):
+        if returns is not None:
+            ax.plot(returns, label=info.label, alpha=info.alpha, linewidth=info.linewidth, color=info.color)
+
+    def _plot_spots_on_returns(self, ax, positions: Sequence[int], info: SpotInfo):
+        ax.plot(
+            self._returns.index[positions], self._returns[positions],
+            info.marker, color=info.color, markersize=info.markersize, alpha=info.alpha, label=info.label
+        )
+
+    def plot(self, ax: Axes):
+        ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator())
+        ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator())
+        ax.grid(b=True, which='minor', linewidth=.2)
+        ax.grid(b=True, which='major', linewidth=1)
+        ax.patch.set_alpha(0.6)
+
+        # plot lines
+        for returns, info in self._lines:
+            self._plot_line(ax, returns, info)
+        # plot MaxDD/MaxDDD
+        for positions, info in self._spots_on_returns:
+            self._plot_spots_on_returns(ax, positions, info)
+
+        # place legend
+        pyplot.legend(loc="best").get_frame().set_alpha(0.5)
+        # manipulate axis
+        ax.set_yticks(ax.get_yticks())  # make matplotlib happy
+        ax.set_yticklabels(['{:3.2f}%'.format(x * 100) for x in ax.get_yticks()])
+
+
+class UserPlot(SubPlot):
+    height: int = USER_PLOT_AREA_HEIGHT
+
+    def __init__(self, plots_df):
+        self._df = plots_df
+
+    def plot(self, ax: Axes):
+        ax.patch.set_alpha(0.6)
+        for column in self._df.columns:
+            ax.plot(self._df[column], label=column)
+        pyplot.legend(loc="best").get_frame().set_alpha(0.5)
+
+
+class WaterMark:
+    def __init__(self, img_width, img_height):
+        logo_file = os.path.join(
+            os.path.dirname(os.path.realpath(rqalpha.__file__)),
+            "resource", 'ricequant-logo.png')
+        self.img_width = img_width
+        self.img_height = img_height
+        self.logo_img = mpimg.imread(logo_file)
+        self.dpi = self.logo_img.shape[1] / img_width * 1.1
+
+    def plot(self, fig: Figure):
+        fig.figimage(
+            self.logo_img,
+            xo=(self.img_width * self.dpi - self.logo_img.shape[1]) / 2,
+            yo=(self.img_height * self.dpi - self.logo_img.shape[0]) / 2,
+            alpha=0.4,
+        )
+        
+
+def _plot(title: str, sub_plots: List[SubPlot]):
+    img_height = sum(s.height for s in sub_plots)
+    water_mark = WaterMark(IMG_WIDTH, img_height)
+    fig = pyplot.figure(title, figsize=(IMG_WIDTH, img_height), dpi=water_mark.dpi)
+    water_mark.plot(fig)
+
+    gs = gridspec.GridSpec(img_height, 8, figure=fig)
+    last_height = 0
+    for p in sub_plots:
+        p.plot(pyplot.subplot(gs[last_height:last_height + p.height, :p.right_pad]))
+        last_height += p.height
+
+    pyplot.tight_layout()
+    return fig
+
+
+def plot_result(result_dict, show=True, save=None, weekly_indicators: bool = False, open_close_points: bool = False):
+    summary = result_dict["summary"]
+    portfolio = result_dict["portfolio"]
+
+    return_lines: List[Tuple[pd.Series, LineInfo]] = [(portfolio.unit_net_value - 1, LINE_STRATEGY)]
+    if "benchmark_portfolio" in result_dict:
+        benchmark_portfolio = result_dict["benchmark_portfolio"]
+        ex_returns = portfolio.unit_net_value - benchmark_portfolio.unit_net_value
+        ex_max_dd_ddd = "MaxDD {}\nMaxDDD {}".format(
+            _max_dd(ex_returns + 1, portfolio.index).repr, _max_ddd(ex_returns + 1, portfolio.index).repr
+        )
+        indicators = INDICATORS + [EXCESS_INDICATORS]
+        return_lines.extend([
+            (benchmark_portfolio.unit_net_value - 1, LINE_BENCHMARK),
+            (ex_returns, LINE_EXCESS),
+        ])
+        if weekly_indicators:
+            return_lines.append((weekly_returns(benchmark_portfolio), LINE_WEEKLY_BENCHMARK))
+    else:
+        ex_max_dd_ddd = "nan"
+        indicators = INDICATORS
+    if weekly_indicators:
+        return_lines.append((weekly_returns(portfolio), LINE_WEEKLY))
+        indicators.append(WEEKLY_INDICATORS)
+    max_dd = _max_dd(portfolio.unit_net_value.values, portfolio.index)
+    max_ddd = _max_ddd(portfolio.unit_net_value.values, portfolio.index)
+    spots_on_returns: List[Tuple[Sequence[int], SpotInfo]] = [
+        ([max_dd.start, max_dd.end], MAX_DD),
+        ([max_ddd.start, max_ddd.end], MAX_DDD)
+    ]
+    if open_close_points:
+        trades: pd.DataFrame = result_dict["trades"]
+        spots_on_returns.append((trading_dates_index(trades, POSITION_EFFECT.CLOSE, portfolio.index), CLOSE_POINT))
+        spots_on_returns.append((trading_dates_index(trades, POSITION_EFFECT.OPEN, portfolio.index), OPEN_POINT))
+
+    sub_plots = [IndicatorArea(indicators, ChainMap(summary, {
+        "max_dd_ddd": "MaxDD {}\nMaxDDD {}".format(max_dd.repr, max_ddd.repr),
+        "excess_max_dd_ddd": ex_max_dd_ddd,
+    })), ReturnPlot(
+        portfolio.unit_net_value - 1, return_lines, spots_on_returns
+    )]
+    if "plots" in result_dict:
+        sub_plots.append(UserPlot(result_dict["plots"]))
+    
+    _plot(summary["strategy_file"], sub_plots)
+
+    if save:
+        file_path = save
+        if os.path.isdir(save):
+            file_path = os.path.join(save, "{}.png".format(summary["strategy_name"]))
+        pyplot.savefig(file_path, bbox_inches='tight')
+
+    if show:
+        pyplot.show()
diff --git a/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py
new file mode 100644
index 000000000..4d70d659a
--- /dev/null
+++ b/rqalpha/mod/rqalpha_mod_sys_analyser/plot/utils.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
+#
+# 除非遵守当前许可，否则不得使用本软件。
+#
+#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
+#
+#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
+#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
+#         详细的授权流程，请联系 public@ricequant.com 获取。
+
+from collections import namedtuple
+
+import numpy as np
+from numpy import array
+from pandas import DatetimeIndex, DataFrame, Series, to_datetime
+
+IndicatorInfo = namedtuple("IndicatorInfo", ("key", "label", "color", "formatter", "value_font_size"))
+LineInfo = namedtuple("LineInfo", ("label", "color", "alpha", "linewidth"))
+SpotInfo = namedtuple("SpotInfo", ("label", "marker", "color", "markersize", "alpha"))
+
+
+class IndexRange(namedtuple("IndexRange", ("start", "end", "start_date", "end_date"))):
+    @classmethod
+    def new(cls, start, end, index):
+        return cls(start, end, index[start].date(), index[end].date())
+
+    @property
+    def repr(self):
+        return "{}~{}, {} days".format(self.start_date, self.end_date, (self.end_date - self.start_date).days)
+
+
+def max_dd(arr: array, index: DatetimeIndex) -> IndexRange:
+    end = np.argmax(np.maximum.accumulate(arr) / arr)
+    if end == 0:
+        end = len(arr) - 1
+    start = np.argmax(arr[:end]) if end > 0 else 0
+    return IndexRange.new(start, end, index)
+
+
+def max_ddd(arr: array, index: DatetimeIndex) -> IndexRange:
+    max_seen = arr[0]
+    ddd_start, ddd_end = 0, 0
+    ddd = 0
+    start = 0
+    in_draw_down = False
+
+    i = 0
+    for i in range(len(arr)):
+        if arr[i] > max_seen:
+            if in_draw_down:
+                in_draw_down = False
+                if i - start > ddd:
+                    ddd = i - start
+                    ddd_start = start
+                    ddd_end = i - 1
+            max_seen = arr[i]
+        elif arr[i] < max_seen:
+            if not in_draw_down:
+                in_draw_down = True
+                start = i - 1
+
+    if arr[i] < max_seen:
+        if i - start > ddd:
+            return IndexRange.new(start, i, index)
+
+    return IndexRange.new(ddd_start, ddd_end, index)
+
+
+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
+
+
+def trading_dates_index(trades: DataFrame, position_effect, index: DatetimeIndex):
+    return index.searchsorted(
+        to_datetime(trades[trades.position_effect == position_effect].trading_datetime), side="right"
+    ) - 1
diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py b/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py
index 8c5ff4e77..7670b412d 100644
--- a/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py
+++ b/rqalpha/mod/rqalpha_mod_sys_risk/__init__.py
@@ -1,14 +1,17 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
diff --git a/rqalpha/mod/rqalpha_mod_sys_risk/validators/__init__.py b/rqalpha/mod/rqalpha_mod_sys_risk/validators/__init__.py
index bcf7634ee..b22c51e8d 100644
--- a/rqalpha/mod/rqalpha_mod_sys_risk/validators/__init__.py
+++ b/rqalpha/mod/rqalpha_mod_sys_risk/validators/__init__.py
@@ -1,14 +1,17 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py b/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py
index 3bdcd27cc..e02ad4ee3 100644
--- a/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py
+++ b/rqalpha/mod/rqalpha_mod_sys_simulation/__init__.py
@@ -24,7 +24,7 @@
     #   分钟回测的可选值有 "current_bar"|"next_bar"|"vwap"（以当前 bar 收盘价｜下一个 bar 的开盘价｜成交量加权平均价撮合)
     #   tick 回测的可选值有 "last"|"best_own"|"best_counterparty"（以最新价｜己方最优价｜对手方最优价撮合）和 "counterparty_offer"（逐档撮合）
     "matching_type": "current_bar",
-    # 开启对于处于涨跌停状态的证券的撮合限制（信号模式下该限制不生效）
+    # 开启对于处于涨跌停状态的证券的撮合限制
     "price_limit": True,
     # 开启对于对手盘无流动性的证券的撮合限制（仅在 tick 回测下生效）
     "liquidity_limit": False,
diff --git a/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py
index 86ebdb1b1..9d66f2a7d 100644
--- a/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py
+++ b/rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py
@@ -81,23 +81,24 @@ def _match(self, account, order):
             deal_price = last_price
 
         if self._price_limit:
-            """
-            在 Signal 模式下，不再阻止涨跌停是否买进，price_limit 参数表示是否给出警告提示。
-            """
             if order.position_effect != POSITION_EFFECT.EXERCISE:
                 if order.side == SIDE.BUY and deal_price >= price_board.get_limit_up(order_book_id):
-                    user_system_log.warning(
-                        _(u"You have traded {order_book_id} with {quantity} lots in limit up status").format(
-                            order_book_id=order_book_id, quantity=order.quantity
-                        )
-                    )
+                    order.mark_rejected(_(
+                        "Order Cancelled: current bar [{order_book_id}] reach the limit_up price."
+                    ).format(order_book_id=order.order_book_id))
+                    self._env.event_bus.publish_event(Event(
+                        EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=copy(order)
+                    ))
+                    return
 
                 if order.side == SIDE.SELL and deal_price <= price_board.get_limit_down(order_book_id):
-                    user_system_log.warning(
-                        _(u"You have traded {order_book_id} with {quantity} lots in limit down status").format(
-                            order_book_id=order_book_id, quantity=order.quantity
-                        )
-                    )
+                    order.mark_rejected(_(
+                        "Order Cancelled: current bar [{order_book_id}] reach the limit_down price."
+                    ).format(order_book_id=order.order_book_id))
+                    self._env.event_bus.publish_event(Event(
+                        EVENT.ORDER_UNSOLICITED_UPDATE, account=account, order=copy(order)
+                    ))
+                    return
 
         ct_amount = account.calc_close_today_amount(order_book_id, order.quantity, order.position_direction)
         trade_price = self._slippage_decider.get_trade_price(order, deal_price)
diff --git a/rqalpha/mod/utils.py b/rqalpha/mod/utils.py
index cf47c4e3d..0195aad83 100644
--- a/rqalpha/mod/utils.py
+++ b/rqalpha/mod/utils.py
@@ -4,11 +4,14 @@
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
diff --git a/rqalpha/utils/__init__.py b/rqalpha/utils/__init__.py
index dc297cd55..d098840a6 100644
--- a/rqalpha/utils/__init__.py
+++ b/rqalpha/utils/__init__.py
@@ -19,10 +19,9 @@
 import pprint
 import os
 import re
-import collections
 from decimal import getcontext, ROUND_FLOOR
 from datetime import time
-from typing import Optional
+from typing import Optional, Mapping
 
 from contextlib import contextmanager
 import numpy as np
@@ -72,7 +71,7 @@ def _update_dict_recursive(target, other):
         for k, v in other.items():
             if isinstance(v, RqAttrDict):
                 v = v.__dict__
-            if isinstance(v, collections.Mapping):
+            if isinstance(v, Mapping):
                 r = RqAttrDict._update_dict_recursive(target_dict.get(k, {}), v)
                 target_dict[k] = r
             else:
diff --git a/rqalpha/utils/config.py b/rqalpha/utils/config.py
index 0b1b7cfdf..66bb36dce 100644
--- a/rqalpha/utils/config.py
+++ b/rqalpha/utils/config.py
@@ -1,19 +1,21 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
 import os
-import locale
 import codecs
 
 import pandas as pd
@@ -23,7 +25,7 @@
 
 from rqalpha.const import RUN_TYPE, PERSIST_MODE, COMMISSION_TYPE
 from rqalpha.utils import RqAttrDict, logger
-from rqalpha.utils.i18n import gettext as _, localization
+from rqalpha.utils.i18n import gettext as _, set_locale
 from rqalpha.utils.dict_func import deep_update
 from rqalpha.utils.logger import system_log
 from rqalpha.mod.utils import mod_config_value_parse
@@ -113,18 +115,6 @@ def dump_config(config_path, config, dumper=yaml.Dumper):
         stream.write(yaml.dump(config, Dumper=dumper))
 
 
-def set_locale(lc):
-    # FIXME: It should depends on the system and locale config
-    try:
-        locale.setlocale(locale.LC_ALL, "")
-        locale.setlocale(locale.LC_CTYPE, "")
-        os.environ['TZ'] = 'Asia/Shanghai'
-    except Exception as e:
-        if os.name != 'nt':
-            raise
-    localization.set_locale([lc])
-
-
 def parse_config(config_args, config_path=None, click_type=False, source_code=None, user_funcs=None):
     conf = default_config()
     deep_update(user_config(), conf)
diff --git a/rqalpha/utils/dict_func.py b/rqalpha/utils/dict_func.py
index 1f636867a..ebe65d907 100644
--- a/rqalpha/utils/dict_func.py
+++ b/rqalpha/utils/dict_func.py
@@ -1,25 +1,26 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
-import collections
+from typing import Mapping
 
 
 def deep_update(from_dict, to_dict):
     for (key, value) in from_dict.items():
-        if (key in to_dict.keys() and
-                isinstance(to_dict[key], collections.Mapping) and
-                isinstance(value, collections.Mapping)):
+        if (key in to_dict.keys() and isinstance(to_dict[key], Mapping) and isinstance(value, Mapping)):
             deep_update(value, to_dict[key])
         else:
             to_dict[key] = value
diff --git a/rqalpha/utils/functools.py b/rqalpha/utils/functools.py
index e44169b53..3de685266 100644
--- a/rqalpha/utils/functools.py
+++ b/rqalpha/utils/functools.py
@@ -53,7 +53,7 @@ def rq_invalid_argument(arg):
             ).format(funcname, argname, [getattr(i, "name", str(i)) for i in registry], arg, type(arg)))
         else:
             return RQApiNotSupportedError(_(
-                "function {} are not supported, please check your account or mod config"
+                "function {} is not supported, please check your account or mod config"
             ).format(funcname))
 
     @lru_cache(1024)
diff --git a/rqalpha/utils/i18n.py b/rqalpha/utils/i18n.py
index 83a78bea9..b1e299bb3 100644
--- a/rqalpha/utils/i18n.py
+++ b/rqalpha/utils/i18n.py
@@ -1,56 +1,66 @@
 # -*- coding: utf-8 -*-
-# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
+# 版权所有 2021 深圳米筐科技有限公司（下称“米筐科技”）
 #
 # 除非遵守当前许可，否则不得使用本软件。
 #
 #     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
-#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
+#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
+#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
 #         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
 #
 #     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
-#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，否则米筐科技有权追究相应的知识产权侵权责任。
+#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
+#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
+#         否则米筐科技有权追究相应的知识产权侵权责任。
 #         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
+import os
 import os.path
+import ctypes
+import locale
 from gettext import NullTranslations, translation
+from typing import Optional
 
 from rqalpha.utils.logger import system_log
 
 
 class Localization(object):
 
-    def __init__(self, trans=None):
-        self.trans = NullTranslations() if trans is None else trans
+    def __init__(self, lc=None):
+        if lc is None:
+            # https://stackoverflow.com/questions/3425294/how-to-detect-the-os-default-language-in-python
+            if os.name == "nt":
+                lc = locale.windows_locale[ctypes.windll.kernel32.GetUserDefaultUILanguage()]
+            else:
+                lc = os.getenv("LANG")
+        self.trans = self.get_trans(lc)
 
-    def set_locale(self, locales, trans_dir=None):
-        if locales[0] is None or "en" in locales[0].lower():
-            self.trans = NullTranslations()
-            return
-        if "cn" in locales[0].lower():
+    @classmethod
+    def get_trans(cls, lc: Optional[str], trans_dir=None):
+        if lc is not None and "cn" in lc.lower():
             locales = ["zh_Hans_CN"]
-        try:
-            if trans_dir is None:
-                trans_dir = os.path.join(
-                    os.path.dirname(
-                        os.path.abspath(
-                            __file__,
-                        ),
-                    ),
-                    "translations"
-                )
-            self.trans = translation(
-                domain="messages",
-                localedir=trans_dir,
-                languages=locales,
-            )
-        except Exception as e:
-            system_log.debug(e)
-            self.trans = NullTranslations()
-
-
-localization = Localization()
+            try:
+                if trans_dir is None:
+                    trans_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translations")
+                return translation(domain="messages", localedir=trans_dir, languages=locales,)
+            except Exception as e:
+                system_log.debug(e)
+                return NullTranslations()
+        else:
+            return NullTranslations()
+
+
+localization: Optional[Localization] = None
 
 
 def gettext(message):
+    global localization
+    if not localization:
+        localization = Localization()
     return localization.trans.gettext(message)
+
+
+def set_locale(lc: str = None):
+    global localization
+    localization = Localization(lc)
diff --git a/rqalpha/utils/logger.py b/rqalpha/utils/logger.py
index 365f140f6..2f9ee7bbf 100644
--- a/rqalpha/utils/logger.py
+++ b/rqalpha/utils/logger.py
@@ -26,6 +26,7 @@
     "user_log",
     "system_log",
     "user_system_log",
+    "release_print"
 ]
 
 
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 6b73c1109..880f1f850 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 de6270cde..7445050ea 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: 2021-09-08 11:05+0800\n"
+"POT-Creation-Date: 2021-11-29 11:41+0800\n"
 "PO-Revision-Date: 2016-10-24 21:20+0800\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language: zh_Hans_CN\n"
@@ -46,38 +46,38 @@ msgstr ""
 "rqdatac 初始化失败，部分扩展 API 可能无法使用，错误信息：{}。您可以访问 "
 "https://www.ricequant.com/welcome/rqdata 获取 rqdatac"
 
-#: rqalpha/main.py:210
+#: rqalpha/main.py:211
 msgid "system restored"
 msgstr ""
 
-#: rqalpha/main.py:243
+#: rqalpha/main.py:244
 msgid "strategy run successfully, normal exit"
 msgstr "策略运行成功，正常退出"
 
-#: rqalpha/main.py:248
+#: rqalpha/main.py:249
 msgid "strategy execute exception"
 msgstr "策略运行产生异常"
 
-#: rqalpha/apis/api_base.py:68 rqalpha/apis/api_base.py:267
-#: rqalpha/apis/api_base.py:303
+#: rqalpha/apis/api_base.py:67 rqalpha/apis/api_base.py:266
+#: rqalpha/apis/api_base.py:302
 msgid "unsupported order_book_id type"
 msgstr "不支持该 order_book_id 类型"
 
-#: rqalpha/apis/api_base.py:88 rqalpha/apis/api_base.py:92
+#: rqalpha/apis/api_base.py:87 rqalpha/apis/api_base.py:91
 msgid "Limit order price should not be nan."
 msgstr "限价单价格不能为 nan。"
 
-#: rqalpha/apis/api_base.py:149
+#: rqalpha/apis/api_base.py:148
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:59
 msgid "Main Future contracts[88] are not supported in paper trading."
 msgstr "期货主力连续合约[88] 在实盘模拟中暂不支持。"
 
-#: rqalpha/apis/api_base.py:153
+#: rqalpha/apis/api_base.py:152
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:61
 msgid "Index Future contracts[99] are not supported in paper trading."
 msgstr "期货指数连续合约[99] 在实盘模拟中暂不支持。"
 
-#: rqalpha/apis/api_base.py:159
+#: rqalpha/apis/api_base.py:158
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_future.py:66
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:90
 #: rqalpha/mod/rqalpha_mod_sys_accounts/api/api_stock.py:137
@@ -130,7 +130,11 @@ msgid ""
 "got {} (type: {})"
 msgstr ""
 
-#: rqalpha/cmds/bundle.py:45
+#: rqalpha/cmds/bundle.py:33
+msgid "create bundle using RQDatac"
+msgstr "使用 RQDatac 创建 Bundle"
+
+#: rqalpha/cmds/bundle.py:43
 msgid ""
 "rqdatac is required to create bundle. you can visit "
 "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha"
@@ -139,11 +143,15 @@ msgstr ""
 "创建 bundle 依赖 rqdatac。您可以访问 https://www.ricequant.com/welcome/rqdata 以获取 "
 "rqdatac，或执行 \"rqalpha download-bundle\" 以下载月度更新的 bundle。"
 
-#: rqalpha/cmds/bundle.py:56 rqalpha/cmds/bundle.py:86
+#: rqalpha/cmds/bundle.py:54 rqalpha/cmds/bundle.py:83
 msgid "rqdatac init failed with error: {}"
 msgstr "rqdatac 初始化失败：{}"
 
-#: rqalpha/cmds/bundle.py:75
+#: rqalpha/cmds/bundle.py:62
+msgid "Update bundle using RQDatac"
+msgstr "使用 RQDatac 更新 Bundle"
+
+#: rqalpha/cmds/bundle.py:72
 msgid ""
 "rqdatac is required to update bundle. you can visit "
 "https://www.ricequant.com/welcome/rqdata to get rqdatac, or use \"rqalpha"
@@ -152,11 +160,15 @@ msgstr ""
 "更新 bundle 依赖 rqdatac。您可以访问 https://www.ricequant.com/welcome/rqdata 以获取 "
 "rqdatac，或执行 \"rqalpha download-bundle\" 以下载月度更新的 bundle。"
 
-#: rqalpha/cmds/bundle.py:90
+#: rqalpha/cmds/bundle.py:87
 msgid "bundle not exist, use \"rqalpha create-bundle\" command instead"
 msgstr "bundle 不存在，执行 \"rqalpha create-bundle\" 以创建 bundle"
 
-#: rqalpha/cmds/bundle.py:109
+#: rqalpha/cmds/bundle.py:94
+msgid "Download bundle (monthly updated)"
+msgstr "下载（月度更新的）Bundle"
+
+#: rqalpha/cmds/bundle.py:105
 msgid ""
 "\n"
 "    [WARNING]\n"
@@ -170,19 +182,19 @@ msgstr ""
 "    该路径下的内容将会被移除.\n"
 "    确定继续吗?"
 
-#: rqalpha/cmds/bundle.py:127
+#: rqalpha/cmds/bundle.py:123
 msgid "Data bundle download successfully in {bundle_path}"
 msgstr "数据下载成功: {bundle_path}"
 
-#: rqalpha/cmds/bundle.py:138
+#: rqalpha/cmds/bundle.py:134
 msgid "try {} ..."
 msgstr "尝试 {} ..."
 
-#: rqalpha/cmds/bundle.py:150
+#: rqalpha/cmds/bundle.py:146
 msgid "downloading ..."
 msgstr "下载中 ..."
 
-#: rqalpha/cmds/bundle.py:164
+#: rqalpha/cmds/bundle.py:160
 msgid ""
 "\n"
 "Download failed, retry in {} seconds."
@@ -190,6 +202,26 @@ msgstr ""
 "\n"
 "下载失败，{} 秒后重试。"
 
+#: rqalpha/cmds/misc.py:26
+msgid "Generate example strategies to target folder"
+msgstr "在目标文件夹生成样例策略"
+
+#: rqalpha/cmds/misc.py:41
+msgid "Output Version Info"
+msgstr "输出版本号信息"
+
+#: rqalpha/cmds/misc.py:48
+msgid "Generate default config file"
+msgstr "生成默认的配置文件"
+
+#: rqalpha/cmds/mod.py:29
+msgid "Mod management command"
+msgstr "Mod 管理命令"
+
+#: rqalpha/cmds/run.py:27
+msgid "Run a strategy"
+msgstr "运行策略"
+
 #: rqalpha/core/execution_context.py:111
 #, python-format
 msgid "You cannot call %s when executing %s"
@@ -306,6 +338,54 @@ msgstr "{} 的目标持仓需要在 0 到 1 之间，当前值：{}"
 msgid "in get_dividend, start_date {} is later than the previous test day {}"
 msgstr "在 get_dividend 函数中，start_date {} 晚于当前回测时间的上一个交易日 {}。"
 
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:64
+msgid "[sys_analyser] save report"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:69
+msgid "[sys_analyser] output result pickle file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:74
+msgid "[sys_analyser] plot result"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:79
+msgid "[sys_analyser] save plot to file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:84
+msgid "[sys_analyser] order_book_id of benchmark"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:89
+msgid "[sys_analyser] show open close points on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:94
+msgid "[sys_analyser] show weekly indicators and return curve on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:98
+msgid "[sys_analyser] Plot from strategy output file"
+msgstr "【sys_analyser】使用策略输出的文件绘制收益图"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:101
+msgid "save plot result to file"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:102
+msgid "show open close points on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:103
+msgid "show weekly indicators and return curve on plot"
+msgstr ""
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/__init__.py:113
+msgid "[sys_analyser] Generate report from strategy output file"
+msgstr "【sys_analyser】使用策略输出的文件生成报告"
+
 #: rqalpha/mod/rqalpha_mod_sys_analyser/mod.py:100
 msgid ""
 "config 'base.benchmark' is deprecated, use 'mod.sys_analyser.benchmark' "
@@ -320,143 +400,151 @@ msgstr ""
 msgid "invalid weight for instrument {order_book_id}: {weight}"
 msgstr ""
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:71
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:59
+msgid "Strategy"
+msgstr "策略收益"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:60
+msgid "Benchmark"
+msgstr "基准收益"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:61
+msgid "Excess"
+msgstr "超额收益"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:62
+msgid "Weekly"
+msgstr "策略周度收益"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:63
+msgid "BenchmarkWeekly"
+msgstr "基准周度收益"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:65
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:82
+msgid "MaxDrawDown"
+msgstr "最大回撤"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:66
+msgid "MaxDDD"
+msgstr "最长回撤持续期"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:67
+msgid "Open"
+msgstr "开仓"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:68
+msgid "Close"
+msgstr "平仓"
+
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:71
 msgid "TotalReturns"
 msgstr "策略收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:72
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:72
 msgid "AnnualReturns"
 msgstr "策略年化收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:73
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:73
 msgid "Alpha"
 msgstr "阿尔法"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:74
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:74
 msgid "Beta"
 msgstr "贝塔"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:75
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:75
 msgid "Sharpe"
 msgstr "夏普率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:76
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:76
 msgid "Sortino"
 msgstr "索提诺比率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:77
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:77
 msgid "InformationRatio"
 msgstr "信息比率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:79
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:79
 msgid "BenchmarkReturns"
 msgstr "基准收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:80
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:80
 msgid "BenchmarkAnnual"
 msgstr "基准年化收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:81
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:81
 msgid "Volatility"
 msgstr "波动率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:82
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:199
-msgid "MaxDrawdown"
-msgstr "最大回撤"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:83
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:83
 msgid "TrackingError"
 msgstr "跟踪误差"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:84
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:84
 msgid "DownsideRisk"
 msgstr "下行风险"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:85
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:85
 msgid "MaxDD/MaxDDD"
 msgstr "最大回撤/最长回撤持续期"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:87
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:89
 msgid "WeeklyAlpha"
 msgstr "周度阿尔法"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:88
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:90
 msgid "WeeklyBeta"
 msgstr "周度贝塔"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:89
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:91
 msgid "WeeklySharpe"
 msgstr "周度夏普率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:90
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:92
 msgid "WeeklySortino"
 msgstr "周度索提诺比率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:91
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:93
 msgid "WeeklyInfoRatio"
 msgstr "周度信息比率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:92
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:94
 msgid "WeeklyTrackingError"
 msgstr "周度跟踪误差"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:93
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:95
 msgid "WeeklyMaxDrawdown"
 msgstr "周度最大回撤"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:97
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:99
 msgid "ExcessReturns"
 msgstr "超额收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:98
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:100
 msgid "ExcessAnnual"
 msgstr "年化超额收益率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:99
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:101
 msgid "ExcessSharpe"
 msgstr "超额夏普率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:100
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:102
 msgid "ExcessVolatility"
 msgstr "超额收益波动率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:101
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:103
 msgid "ExcessAnnualVolatility"
 msgstr "超额收益年化波动率"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:102
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:104
 msgid "ExcessMaxDD"
 msgstr "超额收益最大回撤"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:103
+#: rqalpha/mod/rqalpha_mod_sys_analyser/plot/consts.py:105
 msgid "ExcessMaxDD/ExcessMaxDDD"
 msgstr "超额收益最大回撤/最长回撤持续期"
 
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:189
-msgid "Strategy"
-msgstr "策略收益"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:191
-msgid "Benchmark"
-msgstr "基准收益"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:193
-msgid "Excess"
-msgstr "超额收益"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:194
-msgid "Weekly"
-msgstr "策略周度收益"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:196
-msgid "BenchmarkWeekly"
-msgstr "基准周度收益"
-
-#: rqalpha/mod/rqalpha_mod_sys_analyser/plot.py:200
-msgid "MaxDDD"
-msgstr "最长回撤持续期"
-
 #: rqalpha/mod/rqalpha_mod_sys_risk/validators/cash_validator.py:33
 msgid ""
 "Order Creation Failed: not enough money to buy {order_book_id}, needs "
@@ -506,10 +594,12 @@ msgid "Order Cancelled: current bar [{order_book_id}] miss market data."
 msgstr "订单被拒单: [{order_book_id}] 当前缺失市场数据。"
 
 #: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:148
+#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:89
 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_up price."
 msgstr "订单被拒单: [{order_book_id}] 已涨停。"
 
 #: rqalpha/mod/rqalpha_mod_sys_simulation/matcher.py:154
+#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:98
 msgid "Order Cancelled: current bar [{order_book_id}] reach the limit_down price."
 msgstr "订单被拒单: [{order_book_id}] 已跌停。"
 
@@ -566,14 +656,6 @@ msgstr ""
 msgid "cancel_order function is not supported in signal mode"
 msgstr "在 Signal 模式下，不支持 cancel_order 函数"
 
-#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:90
-msgid "You have traded {order_book_id} with {quantity} lots in limit up status"
-msgstr "您在涨停状态下交易了 {quantity} 单位的 {order_book_id}"
-
-#: rqalpha/mod/rqalpha_mod_sys_simulation/signal_broker.py:97
-msgid "You have traded {order_book_id} with {quantity} lots in limit down status"
-msgstr "您在跌停状态下交易了 {quantity} 单位的 {order_book_id}"
-
 #: rqalpha/mod/rqalpha_mod_sys_simulation/simulation_broker.py:104
 msgid "unsupported position_effect {}"
 msgstr "不支持的 position_effect {}"
@@ -647,7 +729,7 @@ msgstr "触发强制清算，当前总权益为 0"
 msgid "insufficient cash, current {}, target withdrawal {}"
 msgstr "现金不足，当前现金 {}，目标出金 {}"
 
-#: rqalpha/portfolio/position.py:169
+#: rqalpha/portfolio/position.py:166
 msgid "last price of position {} is not supposed to be nan"
 msgstr "仓位 {} 最新价不应该为 nan"
 
@@ -738,10 +820,11 @@ msgid ""
 msgstr ""
 
 #: rqalpha/utils/arg_checker.py:344
+#, fuzzy
 msgid ""
 "function {}: invalid {} argument, frequency should be in form of '1m', "
-"'5m', '1d', got {} (type: {})"
-msgstr ""
+"'5m', '1d', '1w' got {} (type: {})"
+msgstr "函数 {}：不合法的参数 {}，应传入具有 {} 类型的合约代码或 Instrument 对象，实际传入了 {}（类型：{}）"
 
 #: rqalpha/utils/class_helper.py:23
 msgid ""
@@ -749,37 +832,37 @@ msgid ""
 "more information"
 msgstr "\"{}\" 已被弃用，请使用 \"{}\"，查询文档以获取更多信息"
 
-#: rqalpha/utils/config.py:104
+#: rqalpha/utils/config.py:106
 msgid "in parse_user_config, exception: {e}"
 msgstr "在执行 parse_user_config 时，产生异常: {e}"
 
-#: rqalpha/utils/config.py:206
+#: rqalpha/utils/config.py:196
 msgid "Invalid future info: underlying_symbol {} is illegal."
 msgstr "无效的 future info: 合约品种 {} 不合法"
 
-#: rqalpha/utils/config.py:221
+#: rqalpha/utils/config.py:211
 msgid ""
 "Invalid future info: commission_type is suppose to be BY_MONEY or "
 "BY_VOLUME"
 msgstr "无效的 future info: commission_type 字段应为 BY_MONEY 或 BY_VOLUME"
 
-#: rqalpha/utils/config.py:225
+#: rqalpha/utils/config.py:215
 msgid "Invalid future info: field {} is not valid"
 msgstr "无效的 future info: 字段 {} 不合法"
 
-#: rqalpha/utils/config.py:255
+#: rqalpha/utils/config.py:245
 msgid "invalid init position {}, should be in format 'order_book_id:quantity'"
 msgstr "无效的初始仓位：{}，格式应该为 'order_book_id:quantity'"
 
-#: rqalpha/utils/config.py:260
+#: rqalpha/utils/config.py:250
 msgid "invalid quantity for instrument {order_book_id}: {quantity}"
 msgstr "初始仓位中合约 {order_book_id} 的数量 {quantity} 不是有效的数字"
 
-#: rqalpha/utils/config.py:275
+#: rqalpha/utils/config.py:265
 msgid "unknown run type: {}"
 msgstr "未知的策略类型：{}"
 
-#: rqalpha/utils/config.py:288
+#: rqalpha/utils/config.py:278
 msgid "unknown persist mode: {}"
 msgstr "未知的持久化模式：{}"
 
@@ -790,8 +873,8 @@ msgid ""
 msgstr "函数 {}：不合法的参数 {}，应传入具有 {} 类型的合约代码或 Instrument 对象，实际传入了 {}（类型：{}）"
 
 #: rqalpha/utils/functools.py:55
-msgid "function {} are not supported, please check your account or mod config"
-msgstr "不支持调用函数 {}，请检查 Account 设置或 mod 配置"
+msgid "function {} is not supported, please check your account or mod config"
+msgstr "不支持 API {}。请确保您设置了该 API 所需的账户并开启了包含该 API 实现的 Mod"
 
 #~ msgid "style should be OrderStyle"
 #~ msgstr "style 应该为 OrderStyle 类型，比如MarketOrder()"
@@ -1257,3 +1340,24 @@ msgstr "不支持调用函数 {}，请检查 Account 设置或 mod 配置"
 #~ msgid "unknown market type: {}"
 #~ msgstr "未知的市场类型：{}"
 
+#~ msgid "You have traded {order_book_id} with {quantity} lots in limit up status"
+#~ msgstr "您在涨停状态下交易了 {quantity} 单位的 {order_book_id}"
+
+#~ msgid ""
+#~ "You have traded {order_book_id} with "
+#~ "{quantity} lots in limit down status"
+#~ msgstr "您在跌停状态下交易了 {quantity} 单位的 {order_book_id}"
+
+#~ msgid ""
+#~ "function {}: invalid {} argument, "
+#~ "frequency should be in form of "
+#~ "'1m', '5m', '1d', got {} (type: "
+#~ "{})"
+#~ msgstr ""
+
+#~ msgid "MaxDrawdown"
+#~ msgstr "最大回撤"
+
+#~ msgid "function {} are not supported, please check your account or mod config"
+#~ msgstr "不支持调用函数 {}，请检查 Account 设置或 mod 配置"
+
diff --git a/setup.cfg b/setup.cfg
index e21387c1a..03d1a1672 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -5,7 +5,7 @@
 
 [metadata]
 name = rqalpha
-version = 4.6.0
+version = 4.7.0
 
 [versioneer]
 VCS = git
diff --git a/setup.py b/setup.py
index f68069550..7b22ced71 100644
--- a/setup.py
+++ b/setup.py
@@ -18,7 +18,7 @@
 requirements = [
     'requests',
     'numpy',
-    'pandas',
+    'pandas >=1.0.5',
     'python-dateutil',
     'six',
     'logbook',
@@ -77,4 +77,5 @@
         'Programming Language :: Python :: 3.8',
         'Programming Language :: Python :: 3.9',
     ],
+    python_requires=">=3.6"
 )
diff --git a/versioneer.py b/versioneer.py
index 5e9c41f9a..b41b70750 100644
--- a/versioneer.py
+++ b/versioneer.py
@@ -1394,7 +1394,7 @@ def render_pep440_ricequant(pieces):
                     raise Exception("Only post release tag allowed, tag %s" % tag)
                 if pieces["distance"] > 0:
                     rendered += ".post%d" % (parsed_tag._version.post[1] + 1)
-                    rendered += ".dev%d-g%s" % (pieces["distance"], pieces["short"])
+                    rendered += ".dev%d" % (pieces["distance"])
                 elif pieces["distance"] == 0:
                     rendered += ".post%d" % parsed_tag._version.post[1]
                 if pieces["dirty"]:
@@ -1404,13 +1404,13 @@ def render_pep440_ricequant(pieces):
         # 如果最近的tag是正式版tag，那么就是在开发该系列的.post1
         elif parsed_working == parsed_tag:
             if pieces["distance"] > 0:
-                rendered += ".post1.dev%d-g%s" % (pieces["distance"], pieces["short"])
+                rendered += ".post1.dev%d" % (pieces["distance"])
             if pieces["dirty"]:
                 rendered += ".dirty"
         # 如果正在开发到是一个新的系列，那么就从该系列的.dev0开始
         else:
             if pieces["distance"] >= 0:
-                rendered += ".dev%d-g%s"  % (pieces["distance"], pieces["short"])
+                rendered += ".dev%d" % (pieces["distance"])
 
             if pieces["dirty"]:
                 rendered += ".dirty"
@@ -1428,7 +1428,6 @@ def render_pep440_ricequant(pieces):
         "distance"] == 0:
         return rendered
 
-    rendered += ".g%s" % pieces["short"]
     return rendered
 
 


Test patch:
diff --git a/test.py b/test.py
index e976b2f2e..7ae3f9ca3 100644
--- a/test.py
+++ b/test.py
@@ -26,15 +26,12 @@
 import coverage
 
 from rqalpha import run, run_func
-from rqalpha.utils.config import set_locale
 
 TEST_DIR = os.path.abspath("./tests/")
 TEST_OUT = os.path.abspath("./tests/outs/")
 
 pd.set_option("display.width", 160)
 
-set_locale("zh_Hans_CN")
-
 
 def run_tests(file_path=None):
     tests = {f.replace(".py", ""): f for f in (
diff --git a/tests/api_tests/__init__.py b/tests/api_tests/__init__.py
index 73464c7f9..081c37820 100644
--- a/tests/api_tests/__init__.py
+++ b/tests/api_tests/__init__.py
@@ -16,18 +16,16 @@
 #         详细的授权流程，请联系 public@ricequant.com 获取。
 
 import os
-import collections
 from copy import deepcopy
 from importlib import import_module
+from typing import Mapping
 
 import six
 
 
 def deep_update(from_dict, to_dict):
     for (key, value) in from_dict.items():
-        if (key in to_dict.keys() and
-                isinstance(to_dict[key], collections.Mapping) and
-                isinstance(value, collections.Mapping)):
+        if (key in to_dict.keys() and isinstance(to_dict[key], Mapping) and isinstance(value, Mapping)):
             deep_update(value, to_dict[key])
         else:
             to_dict[key] = value
diff --git a/tests/api_tests/mod/sys_simulation/test_management_fee.py b/tests/api_tests/mod/sys_simulation/test_management_fee.py
index 121649957..d464415c2 100644
--- a/tests/api_tests/mod/sys_simulation/test_management_fee.py
+++ b/tests/api_tests/mod/sys_simulation/test_management_fee.py
@@ -17,7 +17,7 @@
 
 __config__ = {
     "base": {
-        "start_date": "2015-04-10",
+        "start_date": "2015-04-13",
         "end_date": "2015-05-10",
         "frequency": "1d",
         "accounts": {
diff --git a/tests/api_tests/mod/sys_simulation/test_signal_broker.py b/tests/api_tests/mod/sys_simulation/test_signal_broker.py
index bcf815a5e..1c8116676 100644
--- a/tests/api_tests/mod/sys_simulation/test_signal_broker.py
+++ b/tests/api_tests/mod/sys_simulation/test_signal_broker.py
@@ -42,6 +42,14 @@
 def test_price_limit():
     def handle_bar(context, bar_dict):
         stock = "000001.XSHE"
+        price = bar_dict[stock].limit_up * 0.99
+        order_shares(stock, 100, price)
+        assert get_position(stock).quantity == 100
+        assert get_position(stock).avg_price == price
+
+        # 超过涨停价拒单
         order_shares(stock, 100, bar_dict[stock].limit_up)
-        assert context.portfolio.positions[stock].quantity == 100
+        assert get_position(stock).quantity == 100
+        assert get_position(stock).avg_price == price
+
     return locals()
diff --git a/tests/utils.py b/tests/utils.py
index 4e6d09147..e09e4eb9c 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,14 +1,12 @@
-import collections
 from copy import deepcopy
+from typing import Mapping
 
 from six import iteritems
 
 
 def deep_update(from_dict, to_dict):
     for (key, value) in from_dict.items():
-        if (key in to_dict.keys() and
-                isinstance(to_dict[key], collections.Mapping) and
-                isinstance(value, collections.Mapping)):
+        if (key in to_dict.keys() and isinstance(to_dict[key], Mapping) and isinstance(value, Mapping)):
             deep_update(value, to_dict[key])
         else:
             to_dict[key] = value