seolpyo-mplchart 1.1.1__py3-none-any.whl → 1.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of seolpyo-mplchart might be problematic. Click here for more details.

@@ -5,9 +5,9 @@ from pathlib import Path
5
5
  import matplotlib.pyplot as plt
6
6
  import pandas as pd
7
7
 
8
- from .draw import Chart as BaseChart
9
- from .cursor import Chart as BaseCursorChart, format_candleinfo_ko, format_volumeinfo_ko, format_candleinfo_en, format_volumeinfo_en
10
- from .slider import Chart as BaseSliderChart
8
+ from ._draw import Chart as _BaseChart
9
+ from ._cursor import Chart as _BaseCursorChart, format_candleinfo_ko, format_volumeinfo_ko, format_candleinfo_en, format_volumeinfo_en
10
+ from ._slider import Chart as _BaseSliderChart
11
11
 
12
12
 
13
13
  __all__ = [
@@ -24,7 +24,7 @@ path_samsung = Path(__file__).parent / 'sample/samsung.txt'
24
24
  path_apple = Path(__file__).parent / 'sample/apple.txt'
25
25
 
26
26
  def sample(stock: Literal['samsung', 'apple']='samsung', chart: Literal['Chart', 'CursorChart', 'SliderChart']='SliderChart'):
27
- C: BaseSliderChart = {'Chart': BaseChart, 'CursorChart': BaseCursorChart, 'SliderChart': BaseSliderChart}[chart]()
27
+ C: _BaseSliderChart = {'Chart': _BaseChart, 'CursorChart': _BaseCursorChart, 'SliderChart': _BaseSliderChart}[chart]()
28
28
  path_file = path_samsung if stock == 'samsung' else path_apple
29
29
  if stock == 'samsung':
30
30
  C.format_candleinfo = format_candleinfo_ko
@@ -68,7 +68,7 @@ def close(fig='all'):
68
68
  return plt.close(fig)
69
69
 
70
70
 
71
- class OnlyChart(BaseChart):
71
+ class OnlyChart(_BaseChart):
72
72
  r"""
73
73
  You can see the guidance document:
74
74
  Korean: https://white.seolpyo.com/entry/147/
@@ -129,7 +129,7 @@ class OnlyChart(BaseChart):
129
129
  pass
130
130
 
131
131
 
132
- class CursorChart(BaseCursorChart):
132
+ class CursorChart(_BaseCursorChart):
133
133
  r"""
134
134
  You can see the guidance document:
135
135
  Korean: https://white.seolpyo.com/entry/147/
@@ -201,7 +201,7 @@ class CursorChart(BaseCursorChart):
201
201
  pass
202
202
 
203
203
 
204
- class SliderChart(BaseSliderChart):
204
+ class SliderChart(_BaseSliderChart):
205
205
  r"""
206
206
  You can see the guidance document:
207
207
  Korean: https://white.seolpyo.com/entry/147/
@@ -272,7 +272,7 @@ class SliderChart(BaseSliderChart):
272
272
  min_distance: Minimum number of candles that can be selected with the slider. default 30
273
273
  limit_candle: Maximum number of candles to draw. default 800
274
274
  limit_wick: Maximum number of candle wicks to draw. default 4,000
275
- limit_volume: Maximum number of volume bars to draw. default 800. Applies only to drawing candle wicks or price line.
275
+ limit_volume: Maximum number of volume bars to draw. default 200. Applies only to drawing candle wicks or price line.
276
276
  limit_ma: If the number of displayed data is more than this, the price moving average line is not drawn. default 8,000
277
277
 
278
278
  color_navigator_line: Navigator divider color. default '#1e78ff'
@@ -303,7 +303,7 @@ def set_theme(chart: OnlyChart|CursorChart|SliderChart, theme: Literal['light',
303
303
  chart.color_box = 'w'
304
304
  chart.textboxKwargs = {'facecolor': 'k', 'edgecolor': 'w'}
305
305
  chart.textKwargs = {'color': 'w'}
306
- chart.color_navigator_cover, chart.color_navigator_line = ('w', '#00FFFF')
306
+ chart.color_navigator_cover, chart.color_navigator_line = ('w', '#FF2400')
307
307
 
308
308
  if initialized:
309
309
  chart.change_background_color('k')
@@ -0,0 +1,114 @@
1
+ from re import search
2
+
3
+ import matplotlib.pyplot as plt
4
+ import matplotlib.style as mplstyle
5
+ from matplotlib.axes import Axes
6
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
7
+ from matplotlib.figure import Figure as Fig
8
+
9
+
10
+ from .utils import dict_unit, dict_unit_en
11
+
12
+
13
+ try: plt.switch_backend('TkAgg')
14
+ except: pass
15
+
16
+ # 한글 깨짐 문제 방지
17
+ try: plt.rcParams['font.family'] ='Malgun Gothic'
18
+ except: pass
19
+
20
+ mplstyle.use('fast')
21
+
22
+
23
+ def convert_unit(value: float, digit=0, word='원'):
24
+ v = value.__abs__()
25
+ du = dict_unit if search('[가-힣]', word) else dict_unit_en
26
+ for unit, n in du.items():
27
+ if n <= v:
28
+ num = (value / n).__round__(digit)
29
+ if not num % 1: num = int(num)
30
+ return f'{num:,}{unit} {word}'
31
+ value = value.__round__(digit)
32
+ if not value % 1: value = int(value)
33
+ elif value < 10: digit = 2
34
+ text = f'{value:,}{word}'
35
+ return text
36
+
37
+
38
+ class Figure(Fig):
39
+ canvas: FigureCanvasAgg
40
+
41
+
42
+ class Base:
43
+ figure: Figure
44
+
45
+ figsize = (14, 7)
46
+ ratio_ax_legend, ratio_ax_price, ratio_ax_volume = (2, 18, 5)
47
+ adjust = dict(
48
+ top=0.95, bottom=0.05, left=0.01, right=0.93, # 여백
49
+ wspace=0, hspace=0 # 플롯간 간격
50
+ )
51
+
52
+ title = 'seolpyo mplchart'
53
+ color_background = '#fafafa'
54
+ gridKwargs = {}
55
+ color_tick, color_tick_label = ('k', 'k')
56
+
57
+ unit_price, unit_volume = ('원', '주')
58
+
59
+ def __init__(self, *args, **kwargs):
60
+ # 기본 툴바 비활성화
61
+ plt.rcParams['toolbar'] = 'None'
62
+ # plt.rcParams['figure.dpi'] = 600
63
+
64
+ self._get_plot()
65
+ return
66
+
67
+ def _get_plot(self):
68
+ self.figure, axes = plt.subplots(
69
+ 3, # row 수
70
+ figsize=self.figsize, # 기본 크기
71
+ height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
72
+ )
73
+ axes: list[Axes]
74
+ self.ax_legend, self.ax_price, self.ax_volume = axes
75
+ self.ax_legend.set_label('legend ax')
76
+ self.ax_price.set_label('price ax')
77
+ self.ax_volume.set_label('volume ax')
78
+
79
+ self.figure.canvas.manager.set_window_title(f'{self.title}')
80
+ self.figure.set_facecolor(self.color_background)
81
+
82
+ # 플롯간 간격 제거(Configure subplots)
83
+ self.figure.subplots_adjust(**self.adjust)
84
+
85
+ self.ax_legend.set_axis_off()
86
+
87
+ # y ticklabel foramt 설정
88
+ self.ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
89
+ self.ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume, digit=2))
90
+
91
+ gridKwargs = {'visible': True, 'linewidth': 0.7, 'color': '#d0d0d0', 'linestyle': '-', 'dashes': (1, 0)}
92
+ gridKwargs.update(self.gridKwargs)
93
+ # 공통 설정
94
+ for ax in (self.ax_price, self.ax_volume):
95
+ ax.xaxis.set_animated(True)
96
+ ax.yaxis.set_animated(True)
97
+
98
+ # x tick 외부 눈금 표시하지 않기
99
+ ax.xaxis.set_ticks_position('none')
100
+ # x tick label 제거
101
+ ax.set_xticklabels([])
102
+ # y tick 우측으로 이동
103
+ ax.tick_params(left=False, right=True, labelleft=False, labelright=True, colors=self.color_tick_label)
104
+ # Axes 외곽선 색 변경
105
+ for i in ['top', 'bottom', 'left', 'right']: ax.spines[i].set_color(self.color_tick)
106
+
107
+ # 차트 영역 배경 색상
108
+ ax.set_facecolor(self.color_background)
109
+
110
+ # grid(구분선, 격자) 그리기
111
+ # 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
112
+ ax.grid(**gridKwargs)
113
+ return
114
+
@@ -0,0 +1,485 @@
1
+ from fractions import Fraction
2
+
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.backend_bases import MouseEvent
5
+ from matplotlib.collections import LineCollection
6
+ from matplotlib.text import Text
7
+ import pandas as pd
8
+
9
+ from ._draw import BaseMixin as BM, Mixin as M
10
+ from .utils import float_to_str
11
+
12
+
13
+ class Mixin(M):
14
+ def on_move(self, e):
15
+ "If mouse move event active, This method work."
16
+ return
17
+
18
+
19
+ class CollectionMixin(BM):
20
+ lineKwargs = {}
21
+ textboxKwargs = {}
22
+ textKwargs = {}
23
+ color_box = 'k'
24
+
25
+ def _add_collection(self):
26
+ super()._add_collection()
27
+
28
+ lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
29
+ lineKwargs.update(self.lineKwargs)
30
+ lineKwargs.update({'segments': [], 'animated': True})
31
+ textboxKwargs = {'boxstyle': 'round', 'facecolor': 'w'}
32
+ textboxKwargs.update(self.textboxKwargs)
33
+ textKwargs = self.textKwargs
34
+ textKwargs.update({'animated': True, 'bbox': textboxKwargs, 'horizontalalignment': '', 'verticalalignment': ''})
35
+ (textKwargs.pop('horizontalalignment'), textKwargs.pop('verticalalignment'))
36
+
37
+ self.price_crossline = LineCollection(**lineKwargs)
38
+ self.ax_price.add_artist(self.price_crossline)
39
+ self.text_date_price = Text(**textKwargs, horizontalalignment='center', verticalalignment='bottom')
40
+ self.ax_price.add_artist(self.text_date_price)
41
+ self.text_price = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
42
+ self.ax_price.add_artist(self.text_price)
43
+
44
+ self.volume_crossline = LineCollection(**lineKwargs)
45
+ self.ax_volume.add_artist(self.volume_crossline)
46
+ self.text_date_volume = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
47
+ self.ax_volume.add_artist(self.text_date_volume)
48
+ self.text_volume = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
49
+ self.ax_volume.add_artist(self.text_volume)
50
+
51
+ self.price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
52
+ self.ax_price.add_artist(self.price_box)
53
+ self.text_price_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
54
+ self.ax_price.add_artist(self.text_price_info)
55
+
56
+ self.volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
57
+ self.ax_volume.add_artist(self.volume_box)
58
+ self.text_volume_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
59
+ self.ax_volume.add_artist(self.text_volume_info)
60
+ return
61
+
62
+ def change_background_color(self, color):
63
+ super().change_background_color(color)
64
+
65
+ self.text_price.set_backgroundcolor(color)
66
+ self.text_volume.set_backgroundcolor(color)
67
+
68
+ self.text_date_price.set_backgroundcolor(color)
69
+ self.text_date_volume.set_backgroundcolor(color)
70
+
71
+ self.text_price_info.set_backgroundcolor(color)
72
+ self.text_volume_info.set_backgroundcolor(color)
73
+ return
74
+
75
+ def change_text_color(self, color):
76
+ super().change_text_color(color)
77
+
78
+ self.text_price.set_color(color)
79
+ self.text_volume.set_color(color)
80
+
81
+ self.text_date_price.set_color(color)
82
+ self.text_date_volume.set_color(color)
83
+
84
+ self.text_price_info.set_color(color)
85
+ self.text_volume_info.set_color(color)
86
+ return
87
+
88
+ def change_line_color(self, color):
89
+ self.price_crossline.set_edgecolor(color)
90
+ self.volume_crossline.set_edgecolor(color)
91
+
92
+ self.price_box.set_edgecolor(color)
93
+ self.volume_box.set_edgecolor(color)
94
+
95
+ self.text_price.get_bbox_patch().set_edgecolor(color)
96
+ self.text_volume.get_bbox_patch().set_edgecolor(color)
97
+
98
+ self.text_date_price.get_bbox_patch().set_edgecolor(color)
99
+ self.text_date_volume.get_bbox_patch().set_edgecolor(color)
100
+
101
+ self.text_price_info.get_bbox_patch().set_edgecolor(color)
102
+ self.text_volume_info.get_bbox_patch().set_edgecolor(color)
103
+ return
104
+
105
+
106
+ _set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume', '_boxheight', '_boxmin', '_boxmax', '_volumeboxmax',}
107
+
108
+ class DataMixin(CollectionMixin):
109
+ def _validate_column_key(self):
110
+ super()._validate_column_key()
111
+ for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
112
+ v = getattr(self, i)
113
+ if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
114
+ return
115
+
116
+ def _generate_data(self, df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *_, **__):
117
+ super()._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__)
118
+
119
+ if not calc_info:
120
+ keys = set(df.keys())
121
+ list_key = ['rate', 'compare', 'rate_open', 'rate_high', 'rate_low',]
122
+ if self.volume: list_key.append('rate_volume')
123
+ for i in list_key:
124
+ if i not in keys:
125
+ raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
126
+ else:
127
+ self.df['compare'] = (self.df[self.close] - self.df['_pre']).fillna(0)
128
+ self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
129
+ self.df['rate_open'] = ((self.df[self.Open] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
130
+ self.df['rate_high'] = ((self.df[self.high] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
131
+ self.df['rate_low'] = ((self.df[self.low] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
132
+ if self.volume:
133
+ self.df['compare_volume'] = (self.df[self.volume] - self.df[self.volume].shift(1)).fillna(0)
134
+ self.df['rate_volume'] = (self.df['compare_volume'] / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
135
+
136
+ self.df['_boxheight'] = (self.df[self.high] - self.df[self.low]) / 5
137
+ self.df['_boxmin'] = self.df[self.low] - self.df['_boxheight']
138
+ self.df['_boxmax'] = self.df[self.high] + self.df['_boxheight']
139
+ if self.volume: self.df['_volumeboxmax'] = self.df[self.volume] * 1.13
140
+ return
141
+
142
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
143
+ super()._set_lim(xmin, xmax, simpler, set_ma)
144
+
145
+ psub = (self.price_ymax - self.price_ymin)
146
+ self.min_candleboxheight = psub / 8
147
+
148
+ pydistance = psub / 20
149
+ self.text_date_price.set_y(self.price_ymin + pydistance)
150
+
151
+ self.min_volumeboxheight = self.volume_ymax / 4
152
+
153
+ vxsub = self.vxmax - self.vxmin
154
+ self.vmiddle = self.vxmax - int((vxsub) / 2)
155
+
156
+ vxdistance = vxsub / 50
157
+ self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
158
+ self.vsixth = self.vxmin + int((vxsub) / 6)
159
+ self.veighth = self.vxmin + int((vxsub) / 8)
160
+
161
+ yvolume = self.volume_ymax * 0.85
162
+ self.text_date_volume.set_y(yvolume)
163
+
164
+ # 정보 텍스트박스
165
+ self.text_price_info.set_y(self.price_ymax - pydistance)
166
+ self.text_volume_info.set_y(yvolume)
167
+ return
168
+
169
+
170
+ class EventMixin(DataMixin):
171
+ in_price_chart, in_volume_chart = (False, False)
172
+ intx = None
173
+
174
+ def _connect_event(self):
175
+ super()._connect_event()
176
+ self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
177
+ return
178
+
179
+ def _on_move(self, e):
180
+ self._on_move_action(e)
181
+ return
182
+
183
+ def _on_move_action(self, e: MouseEvent):
184
+ self._check_ax(e)
185
+
186
+ self.intx = None
187
+ if self.in_price_chart or self.in_volume_chart: self._get_x(e)
188
+ return
189
+
190
+ def _check_ax(self, e: MouseEvent):
191
+ ax = e.inaxes
192
+ if not ax or e.xdata is None or e.ydata is None:
193
+ self.in_price_chart, self.in_volume_chart = (False, False)
194
+ else:
195
+ self.in_price_chart = ax is self.ax_price
196
+ self.in_volume_chart = False if self.in_price_chart else ax is self.ax_volume
197
+
198
+ return
199
+
200
+ def _get_x(self, e: MouseEvent):
201
+ self.intx = e.xdata.__int__()
202
+ if self.intx < 0: self.intx = None
203
+ else:
204
+ try: self.list_index[self.intx]
205
+ except: self.intx = None
206
+ return
207
+
208
+
209
+ class LineMixin(EventMixin):
210
+ digit_price, digit_volume = (0, 0)
211
+ in_candle, in_volumebar = (False, False)
212
+
213
+ def _on_move(self, e):
214
+ super()._on_move(e)
215
+
216
+ self._restore_region()
217
+
218
+ if self.in_price_chart: self._on_move_price_chart(e)
219
+ elif self.in_volume_chart: self._on_move_volume_chart(e)
220
+
221
+ self._blit()
222
+ return
223
+
224
+ def _on_move_price_chart(self, e: MouseEvent):
225
+ x, y = (e.xdata, e.ydata)
226
+
227
+ self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
228
+ self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
229
+ self._draw_crossline()
230
+
231
+ renderer = self.figure.canvas.renderer
232
+
233
+ # 가격
234
+ self.text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
235
+ self.text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
236
+ self.text_price.set_y(y)
237
+ self.text_price.draw(renderer)
238
+
239
+ index = self.intx
240
+ if index is None: self.in_candle = False
241
+ else:
242
+ # 기준시간 표시
243
+ self.text_date_volume.set_text(f'{self.df[self.date][index]}')
244
+ self.text_date_volume.set_x(x)
245
+ self.text_date_volume.draw(renderer)
246
+
247
+ # 캔들 강조
248
+ low = self.df['_boxmin'][index]
249
+ high = self.df['_boxmax'][index]
250
+ sub = high - low
251
+ if sub < self.min_candleboxheight:
252
+ sub = (self.min_candleboxheight - sub) / 2
253
+ low -= sub
254
+ high += sub
255
+
256
+ if high < y or y < low: self.in_candle = False
257
+ else:
258
+ self.in_candle = True
259
+ x1, x2 = (index-0.3, index+1.4)
260
+ self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
261
+ self.price_box.draw(renderer)
262
+ return
263
+
264
+ def _draw_crossline(self):
265
+ renderer = self.figure.canvas.renderer
266
+ self.price_crossline.draw(renderer)
267
+ self.volume_crossline.draw(renderer)
268
+ return
269
+
270
+ def _on_move_volume_chart(self, e: MouseEvent):
271
+ x, y = (e.xdata, e.ydata)
272
+
273
+ self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
274
+ self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
275
+ self._draw_crossline()
276
+
277
+ if not self.volume: return
278
+
279
+ renderer = self.figure.canvas.renderer
280
+
281
+ # 거래량
282
+ self.text_volume.set_text(f'{float_to_str(y, self.digit_volume)}{self.unit_volume}')
283
+ self.text_volume.set_x(self.v0 if self.veighth < x else self.vsixth)
284
+ self.text_volume.set_y(y)
285
+ self.text_volume.draw(renderer)
286
+
287
+ index = self.intx
288
+ if index is None: self.in_volumebar = False
289
+ else:
290
+ # 기준시간 표시
291
+ self.text_date_price.set_text(f'{self.df[self.date][index]}')
292
+ self.text_date_price.set_x(x)
293
+ self.text_date_price.draw(renderer)
294
+
295
+ # 거래량 강조
296
+ high = self.df[self.volume][index] * 1.15
297
+ low = 0
298
+ if high < self.min_volumeboxheight: high = self.min_volumeboxheight
299
+
300
+ if high < y or y < low: self.in_volumebar = False
301
+ else:
302
+ self.in_volumebar = True
303
+ x1, x2 = (index-0.3, index+1.4)
304
+ self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
305
+ self.volume_box.draw(renderer)
306
+ return
307
+
308
+
309
+ format_candleinfo_ko = '{dt}\n\n종가:  {close}\n등락률: {rate}\n대비:  {compare}\n시가:  {open}({rate_open})\n고가:  {high}({rate_high})\n저가:  {low}({rate_low})\n거래량: {volume}({rate_volume})'
310
+ format_volumeinfo_ko = '{dt}\n\n거래량:    {volume}\n거래량증가율: {rate_volume}\n대비:     {compare}'
311
+ format_candleinfo_en = '{dt}\n\nclose: {close}\nrate: {rate}\ncompare: {compare}\nopen: {open}({rate_open})\nhigh: {high}({rate_high})\nlow: {low}({rate_low})\nvolume: {volume}({rate_volume})'
312
+ format_volumeinfo_en = '{dt}\n\nvolume: {volume}\nvolume rate: {rate_volume}\ncompare: {compare}'
313
+
314
+ class InfoMixin(LineMixin):
315
+ fraction = False
316
+ format_candleinfo = format_candleinfo_ko
317
+ format_volumeinfo = format_volumeinfo_ko
318
+
319
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
320
+ super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
321
+
322
+ self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
323
+ return
324
+
325
+ def _on_move_price_chart(self, e):
326
+ super()._on_move_price_chart(e)
327
+
328
+ # 캔들 강조 확인
329
+ if not self.in_candle: return
330
+
331
+ # 캔들 정보
332
+ self.text_price_info.set_text(self._get_info(self.intx))
333
+
334
+ if self.vmiddle < e.xdata: self.text_price_info.set_x(self.v0)
335
+ else:
336
+ # self.text_price_info.set_x(self.vmax - self.x_distance)
337
+ # self.text_price_info.set_horizontalalignment('right')
338
+ # 텍스트박스 크기 가져오기
339
+ bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
340
+ width = bbox.x1 - bbox.x0
341
+ self.text_price_info.set_x(self.v1 - width)
342
+
343
+ self.text_price_info.draw(self.figure.canvas.renderer)
344
+ return
345
+
346
+ def _on_move_volume_chart(self, e):
347
+ super()._on_move_volume_chart(e)
348
+
349
+ # 거래량 강조 확인
350
+ if not self.in_volumebar: return
351
+
352
+ # 거래량 정보
353
+ self.text_volume_info.set_text(self._get_info(self.intx, is_price=False))
354
+
355
+ if self.vmiddle < e.xdata: self.text_volume_info.set_x(self.v0)
356
+ else:
357
+ # self.text_volume_info.set_x(self.vmax - self.x_distance)
358
+ # self.text_volume_info.set_horizontalalignment('right')
359
+ # 텍스트박스 크기 가져오기
360
+ bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
361
+ width = bbox.x1 - bbox.x0
362
+ self.text_volume_info.set_x(self.v1 - width)
363
+
364
+ self.text_volume_info.draw(self.figure.canvas.renderer)
365
+ return
366
+
367
+ def _get_info(self, index, is_price=True):
368
+ dt = self.df[self.date][index]
369
+ if not self.volume:
370
+ v, vr = ('-', '-%')
371
+ else:
372
+ v = self.df[self.volume][index]
373
+ v = float_to_str(v, self.digit_volume)
374
+ # if not v % 1: v = int(v)
375
+ vr = self.df['rate_volume'][index]
376
+ vr = f'{vr:+06,.2f}%'
377
+
378
+ if is_price:
379
+ o, h, l, c = (self.df[self.Open][index], self.df[self.high][index], self.df[self.low][index], self.df[self.close][index])
380
+ rate, compare = (self.df['rate'][index], self.df['compare'][index])
381
+ r = f'{rate:+06,.2f}'
382
+ Or, hr, lr = (self.df['rate_open'][index], self.df['rate_high'][index], self.df['rate_low'][index])
383
+
384
+ if self.fraction:
385
+ c = c.__round__(self.digit_price)
386
+ cd = divmod(c, 1)
387
+ if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
388
+ else: c = float_to_str(cd[0])
389
+ comd = divmod(compare, 1)
390
+ if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
391
+ else: com = float_to_str(comd[0], plus=True)
392
+ o = o.__round__(self.digit_price)
393
+ od = divmod(o, 1)
394
+ if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
395
+ else: o = float_to_str(od[0])
396
+ h = h.__round__(self.digit_price)
397
+ hd = divmod(h, 1)
398
+ if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
399
+ else: h = float_to_str(hd[0])
400
+ l = l.__round__(self.digit_price)
401
+ ld = divmod(l, 1)
402
+ if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
403
+ else: l = float_to_str(ld[0])
404
+
405
+ text = self.format_candleinfo.format(
406
+ dt=dt,
407
+ close=f'{c:>{self._length_text}}{self.unit_price}',
408
+ rate=f'{r:>{self._length_text}}%',
409
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
410
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
411
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
412
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
413
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
414
+ )
415
+ else:
416
+ o, h, l, c = (float_to_str(o, self.digit_price), float_to_str(h, self.digit_price), float_to_str(l, self.digit_price), float_to_str(c, self.digit_price))
417
+ com = float_to_str(compare, self.digit_price, plus=True)
418
+
419
+ text = self.format_candleinfo.format(
420
+ dt=dt,
421
+ close=f'{c:>{self._length_text}}{self.unit_price}',
422
+ rate=f'{r:>{self._length_text}}%',
423
+ compare=f'{com:>{self._length_text}}{self.unit_price}',
424
+ open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
425
+ high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
426
+ low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
427
+ volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
428
+ )
429
+ elif self.volume:
430
+ compare = self.df['compare_volume'][index]
431
+ com = float_to_str(compare, self.digit_volume, plus=True)
432
+ text = self.format_volumeinfo.format(
433
+ dt=dt,
434
+ volume=f'{v:>{self._length_text}}{self.unit_volume}',
435
+ rate_volume=f'{vr:>{self._length_text}}%',
436
+ compare=f'{com:>{self._length_text}}{self.unit_volume}',
437
+ )
438
+ else: text = ''
439
+ return text
440
+
441
+
442
+ class BaseMixin(InfoMixin):
443
+ pass
444
+
445
+
446
+ class Chart(BaseMixin, Mixin):
447
+ def _add_collection(self):
448
+ super()._add_collection()
449
+ return self.add_artist()
450
+
451
+ def _draw_artist(self):
452
+ super()._draw_artist()
453
+ return self.draw_artist()
454
+
455
+ def _get_segments(self):
456
+ self.generate_data()
457
+ return super()._get_segments()
458
+
459
+ def _on_draw(self, e):
460
+ super()._on_draw(e)
461
+ return self.on_draw(e)
462
+
463
+ def _on_pick(self, e):
464
+ self.on_pick(e)
465
+ return super()._on_pick(e)
466
+
467
+ def _set_candle_segments(self, index_start, index_end):
468
+ super()._set_candle_segments(index_start, index_end)
469
+ self.set_segment(index_start, index_end)
470
+ return
471
+
472
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
473
+ super()._set_wick_segments(index_start, index_end, simpler)
474
+ self.set_segment(index_start, index_end, simpler)
475
+ return
476
+
477
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
478
+ super()._set_line_segments(index_start, index_end, simpler, set_ma)
479
+ self.set_segment(index_start, index_end, simpler, set_ma)
480
+ return
481
+
482
+ def _on_move(self, e):
483
+ super()._on_move(e)
484
+ return self.on_move(e)
485
+