• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ricequant / rqalpha / 17906102104

22 Sep 2025 06:02AM UTC coverage: 65.213% (+0.03%) from 65.183%
17906102104

push

github

web-flow
Rqsdk 815 (#938)

* Remove debug print statement from the plot function in the system analyser module.

* Add debug logging for Matplotlib backend in plot function of system analyser module

* add FutureArbitrage

* update version

* update

* update

---------

Co-authored-by: 崔子琦 <oscarcui@ricequant.com>
Co-authored-by: Cuizi7 <Cuizi7@users.noreply.github.com>
Co-authored-by: pitaya <wangjiangfeng77qq@163.com>
Co-authored-by: Don <lin.dongzhao@ricequant.com>

8 of 13 new or added lines in 5 files covered. (61.54%)

128 existing lines in 5 files now uncovered.

6790 of 10412 relevant lines covered (65.21%)

4.53 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.49
/rqalpha/mod/rqalpha_mod_sys_analyser/plot/plot.py
1
# -*- coding: utf-8 -*-
2
# 版权所有 2021 深圳米筐科技有限公司(下称“米筐科技”)
3
#
4
# 除非遵守当前许可,否则不得使用本软件。
5
#
6
#     * 非商业用途(非商业用途指个人出于非商业目的使用本软件,或者高校、研究所等非营利机构出于教育、科研等目的使用本软件):
7
#         遵守 Apache License 2.0(下称“Apache 2.0 许可”),
8
#         您可以在以下位置获得 Apache 2.0 许可的副本:http://www.apache.org/licenses/LICENSE-2.0。
9
#         除非法律有要求或以书面形式达成协议,否则本软件分发时需保持当前许可“原样”不变,且不得附加任何条件。
10
#
11
#     * 商业用途(商业用途指个人出于任何商业目的使用本软件,或者法人或其他组织出于任何目的使用本软件):
12
#         未经米筐科技授权,任何个人不得出于任何商业目的使用本软件(包括但不限于向第三方提供、销售、出租、出借、转让本软件、
13
#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务),任何法人或其他组织不得出于任何目的使用本软件,
14
#         否则米筐科技有权追究相应的知识产权侵权责任。
15
#         在此前提下,对本软件的使用同样需要遵守 Apache 2.0 许可,Apache 2.0 许可与本许可冲突之处,以本许可为准。
16
#         详细的授权流程,请联系 public@ricequant.com 获取。
17

18
import os
7✔
19
from typing import List, Mapping, Tuple, Sequence, Optional
7✔
20
from collections import ChainMap
7✔
21

22
import pandas as pd
7✔
23
from matplotlib.axes import Axes
7✔
24
from matplotlib.figure import Figure
7✔
25
from matplotlib import gridspec, ticker, image as mpimg, pyplot
7✔
26

27
import rqalpha
7✔
28
from rqalpha.const import POSITION_EFFECT
7✔
29
from rqalpha.utils.logger import system_log
7✔
30
from .utils import IndicatorInfo, LineInfo, max_dd as _max_dd, SpotInfo, max_ddd as _max_ddd
7✔
31
from .utils import weekly_returns, trading_dates_index
7✔
32
from .consts import PlotTemplate, DefaultPlot
7✔
33
from .consts import IMG_WIDTH, INDICATOR_AREA_HEIGHT, PLOT_AREA_HEIGHT, USER_PLOT_AREA_HEIGHT, PLOT_TITLE_HEIGHT
7✔
34
from .consts import LABEL_FONT_SIZE, BLACK, SUPPORT_CHINESE, TITLE_FONT_SIZE
7✔
35
from .consts import MAX_DD, MAX_DDD, OPEN_POINT, CLOSE_POINT
7✔
36
from .consts import LINE_BENCHMARK, LINE_STRATEGY, LINE_WEEKLY_BENCHMARK, LINE_WEEKLY, LINE_EXCESS
7✔
37

38

39
class SubPlot:
7✔
40
    height: int
7✔
41
    right_pad: Optional[int] = None
7✔
42

43
    def plot(self, ax: Axes):
7✔
44
        raise NotImplementedError
×
45

46

47
class IndicatorArea(SubPlot):
7✔
48
    height: int = INDICATOR_AREA_HEIGHT
7✔
49
    right_pad = -1
7✔
50

51
    def __init__(
7✔
52
            self, indicators: List[List[IndicatorInfo]], indicator_values: Mapping[str, float],
53
            plot_template: PlotTemplate, strategy_name=None
54
    ):
55
        self._indicators = indicators
×
56
        self._values = indicator_values
×
57
        self._template = plot_template
×
58
        self._strategy_name = strategy_name
×
59

60
    def plot(self, ax: Axes):
7✔
61
        ax.axis("off")
×
62
        for lineno, indicators in enumerate(self._indicators[::-1]):  # lineno: 自下而上的行号
×
63
            _extra_width = 0  # 用于保存加长的部分, 原因是部分label太长出现覆盖
×
64
            for index_in_line, i in enumerate(indicators):
×
65
                _extra_width += (i.label_width_multiplier - 1) * self._template.INDICATOR_WIDTH
×
66
                x = index_in_line * self._template.INDICATOR_WIDTH + _extra_width
×
67
                y_value = lineno * (self._template.INDICATOR_VALUE_HEIGHT + self._template.INDICATOR_LABEL_HEIGHT)
×
68
                y_label = y_value + self._template.INDICATOR_LABEL_HEIGHT
×
69
                try:
×
70
                    value = i.formatter.format(self._values[i.key])
×
71
                except KeyError:
×
72
                    value = "nan"
×
73
                ax.text(x, y_label, i.label, color=i.color, fontsize=LABEL_FONT_SIZE),
×
74
                ax.text(x, y_value, value, color=BLACK, fontsize=i.value_font_size)
×
75
        if self._strategy_name:
×
76
            p = TitlePlot(self._strategy_name, len(self._indicators), self._template)
×
77
            p.plot(ax)
×
78
        
79

80
class ReturnPlot(SubPlot):
7✔
81
    height: int = PLOT_AREA_HEIGHT
7✔
82

83
    def __init__(
7✔
84
            self,
85
            returns,
86
            lines: List[Tuple[pd.Series, LineInfo]],
87
            spots_on_returns: List[Tuple[Sequence[int], SpotInfo]]
88
    ):
89
        self._returns = returns
×
90
        self._lines = lines
×
91
        self._spots_on_returns = spots_on_returns
×
92

93
    @classmethod
7✔
94
    def _plot_line(cls, ax, returns, info: LineInfo):
7✔
95
        if returns is not None:
×
96
            ax.plot(returns, label=info.label, alpha=info.alpha, linewidth=info.linewidth, color=info.color)
×
97

98
    def _plot_spots_on_returns(self, ax, positions: Sequence[int], info: SpotInfo):
7✔
99
        ax.plot(
×
100
            self._returns.index[positions], self._returns[positions],
101
            info.marker, color=info.color, markersize=info.markersize, alpha=info.alpha, label=info.label
102
        )
103

104
    def plot(self, ax: Axes):
7✔
105
        ax.get_xaxis().set_minor_locator(ticker.AutoMinorLocator())
×
106
        ax.get_yaxis().set_minor_locator(ticker.AutoMinorLocator())
×
107
        ax.grid(visible=True, which='minor', linewidth=.2)
×
108
        ax.grid(visible=True, which='major', linewidth=1)
×
109
        ax.patch.set_alpha(0.6)
×
110

111
        # plot lines
112
        for returns, info in self._lines:
×
113
            self._plot_line(ax, returns, info)
×
114
        # plot MaxDD/MaxDDD
115
        for positions, info in self._spots_on_returns:
×
116
            self._plot_spots_on_returns(ax, positions, info)
×
117

118
        # place legend
119
        pyplot.legend(loc="best").get_frame().set_alpha(0.5)
×
120
        # manipulate axis
121
        ax.set_yticks(ax.get_yticks())  # make matplotlib happy
×
122
        ax.set_yticklabels(['{:3.2f}%'.format(x * 100) for x in ax.get_yticks()])
×
123

124

125
class UserPlot(SubPlot):
7✔
126
    height: int = USER_PLOT_AREA_HEIGHT
7✔
127

128
    def __init__(self, plots_df):
7✔
129
        self._df = plots_df
×
130

131
    def plot(self, ax: Axes):
7✔
132
        ax.patch.set_alpha(0.6)
×
133
        for column in self._df.columns:
×
134
            ax.plot(self._df[column], label=column)
×
135
        pyplot.legend(loc="best").get_frame().set_alpha(0.5)
×
136

137

138
class TitlePlot(SubPlot):
7✔
139
    height: int = PLOT_TITLE_HEIGHT
7✔
140

141
    def __init__(self, strategy_name, indicator_area_rows, plot_template: PlotTemplate):
7✔
142
        self._strategy_name = strategy_name
×
143
        self._indicator_area_rows = indicator_area_rows
×
144
        self._template = plot_template
×
145

146
    def plot(self, ax:Axes):
7✔
147
        x = 0.57  # title 为整图居中,而非子图居中
×
148
        y = (self._template.INDICATOR_LABEL_HEIGHT + self._template.INDICATOR_VALUE_HEIGHT) * self._indicator_area_rows + 0.1
×
149
        ax.text(x, y, self._strategy_name, ha='center', va='bottom', color=BLACK, fontsize=TITLE_FONT_SIZE)
×
150

151
class WaterMark:
7✔
152
    def __init__(self, img_width, img_height, strategy_name):
7✔
153
        logo_file = os.path.join(
×
154
            os.path.dirname(os.path.realpath(rqalpha.__file__)),
155
            "resource", 'ricequant-logo.png')
156
        self.img_width = img_width
×
157
        self.img_height = img_height
×
158
        self.logo_img = mpimg.imread(logo_file)
×
159
        self.dpi = self.logo_img.shape[1] / img_width * 1.1
×
160

161
    def plot(self, fig: Figure):
7✔
162
        fig.figimage(
×
163
            self.logo_img, 
164
            xo = (self.img_width * self.dpi - self.logo_img.shape[1]) / 2,
165
            yo = (PLOT_AREA_HEIGHT * self.dpi - self.logo_img.shape[0]) / 2, 
166
            alpha=0.4
167
            )
168

169

170
def _plot(title: str, sub_plots: List[SubPlot], strategy_name):
7✔
171
    img_height = sum(s.height for s in sub_plots)
×
172
    water_mark = WaterMark(IMG_WIDTH, img_height, strategy_name)
×
173
    fig = pyplot.figure(title, figsize=(IMG_WIDTH, img_height), dpi=water_mark.dpi, clear=True)
×
174
    water_mark.plot(fig)
×
175

176
    gs = gridspec.GridSpec(img_height, 8, figure=fig)
×
177
    last_height = 0
×
178
    for p in sub_plots:
×
179
        p.plot(pyplot.subplot(gs[last_height:last_height + p.height, :p.right_pad]))
×
180
        last_height += p.height
×
181

182
    pyplot.tight_layout()
×
183
    return fig
×
184

185

186
def plot_result(
7✔
187
        result_dict, show=True, save=None, weekly_indicators: bool = False, open_close_points: bool = False,
188
        plot_template_cls=DefaultPlot, strategy_name=None
189
):
190
    summary = result_dict["summary"]
×
191
    portfolio = result_dict["portfolio"]
×
192

193
    return_lines: List[Tuple[pd.Series, LineInfo]] = [(portfolio.unit_net_value - 1, LINE_STRATEGY)]
×
194
    if "benchmark_portfolio" in result_dict:
×
195
        benchmark_portfolio = result_dict["benchmark_portfolio"]
×
196
        plot_template = plot_template_cls(portfolio.unit_net_value, benchmark_portfolio.unit_net_value)
×
197
        ex_returns = plot_template.geometric_excess_returns
×
198
        ex_max_dd_ddd = "MaxDD {}\nMaxDDD {}".format(
×
199
            _max_dd(ex_returns + 1, portfolio.index).repr, _max_ddd(ex_returns + 1, portfolio.index).repr
200
        )
201
        indicators = plot_template.INDICATORS + plot_template.EXCESS_INDICATORS
×
202

203
        # 在图例中输出基准信息
204
        _b_str = summary["benchmark_symbol"] if SUPPORT_CHINESE else summary["benchmark"]
×
205
        _INFO = LineInfo(
×
206
            LINE_BENCHMARK.label + "({})".format(_b_str), LINE_BENCHMARK.color,
207
            LINE_BENCHMARK.alpha, LINE_BENCHMARK.linewidth
208
        )
209

210
        return_lines.extend([
×
211
            (benchmark_portfolio.unit_net_value - 1, _INFO),
212
            (ex_returns, LINE_EXCESS),
213
        ])
214
        if weekly_indicators:
×
215
            return_lines.append((weekly_returns(benchmark_portfolio), LINE_WEEKLY_BENCHMARK))
×
216
    else:
217
        ex_max_dd_ddd = "nan"
×
218
        plot_template = plot_template_cls(portfolio.unit_net_value, None)
×
219
        indicators = plot_template.INDICATORS
×
220
    if weekly_indicators:
×
221
        return_lines.append((weekly_returns(portfolio), LINE_WEEKLY))
×
222
        indicators.extend(plot_template.WEEKLY_INDICATORS)
×
223
    max_dd = _max_dd(portfolio.unit_net_value.values, portfolio.index)
×
224
    max_ddd = summary["max_drawdown_duration"]
×
225
    spots_on_returns: List[Tuple[Sequence[int], SpotInfo]] = [
×
226
        ([max_dd.start, max_dd.end], MAX_DD),
227
        ([max_ddd.start, max_ddd.end], MAX_DDD)
228
    ]
229
    if open_close_points and not result_dict["trades"].empty:
×
230
        trades: pd.DataFrame = result_dict["trades"]
×
231
        spots_on_returns.append((trading_dates_index(trades, POSITION_EFFECT.CLOSE, portfolio.index), CLOSE_POINT))
×
232
        spots_on_returns.append((trading_dates_index(trades, POSITION_EFFECT.OPEN, portfolio.index), OPEN_POINT))
×
233

234
    sub_plots = [IndicatorArea(indicators, ChainMap(summary, {
×
235
        "max_dd_ddd": "MaxDD {}\nMaxDDD {}".format(max_dd.repr, max_ddd.repr),
236
        "excess_max_dd_ddd": ex_max_dd_ddd,
237
    }), plot_template, strategy_name), ReturnPlot(
238
        portfolio.unit_net_value - 1, return_lines, spots_on_returns
239
    )]
240
    if "plots" in result_dict:
×
241
        sub_plots.append(UserPlot(result_dict["plots"]))
×
242
    
243
    if strategy_name:
×
244
        for p in sub_plots:
×
245
            if (isinstance(p, IndicatorArea)): p.height += PLOT_TITLE_HEIGHT
×
246

247
    _plot(summary["strategy_file"], sub_plots, strategy_name)
×
248

NEW
249
    system_log.debug(f"Matplotlib backend: {pyplot.get_backend()}")
×
250
    
251
    if save:
×
252
        file_path = save
×
253
        if os.path.isdir(save):
×
254
            file_path = os.path.join(save, "{}.png".format(summary["strategy_name"]))
×
255
        pyplot.savefig(file_path, bbox_inches='tight')
×
256

257
    if show:
×
258
        pyplot.show()
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc