seolpyo-mplchart 0.0.2__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.

@@ -0,0 +1,371 @@
1
+ from matplotlib.backend_bases import PickEvent
2
+ from matplotlib.collections import LineCollection
3
+ from matplotlib.lines import Line2D
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+
8
+ from .base import Base
9
+
10
+
11
+ class Mixin:
12
+ def generate_data(self, df):
13
+ "This function works after data generate process is done."
14
+ return
15
+
16
+ def on_blit(self):
17
+ "This function works after cavas.blit()."
18
+ return
19
+ def on_draw(self, e):
20
+ "This function works if draw event active."
21
+ return
22
+ def on_move(self, e):
23
+ "This function works if mouse move event active."
24
+ return
25
+ def on_pick(self, e):
26
+ "This function works if pick event active."
27
+ return
28
+
29
+
30
+ _set_key = {'x', 'left', 'right', 'top', 'bottom',}
31
+
32
+ class DataMixin(Base):
33
+ df: pd.DataFrame
34
+ date = 'date'
35
+ Open, high, low, close = ('open', 'high', 'low', 'close')
36
+ volume = 'volume'
37
+
38
+ _visible_ma = set()
39
+ label_ma = '{}일선'
40
+ list_ma = (5, 20, 60, 120, 240)
41
+ # https://matplotlib.org/stable/gallery/color/named_colors.html
42
+ list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
43
+
44
+ color_up = '#fe3032'
45
+ color_down = '#0095ff'
46
+ color_flat = 'k'
47
+ color_up_down = 'w'
48
+ color_down_up = 'w'
49
+ colors_volume = '#1f77b4'
50
+
51
+ def _generate_data(self, df: pd.DataFrame):
52
+ for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
53
+ k: str = getattr(self, i)
54
+ if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
55
+ if i != 'date':
56
+ dtype = df[k].dtype
57
+ if not isinstance(dtype, np.dtypes.Float64DType): raise TypeError(f'Data column type must be "float64".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
58
+
59
+ for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
60
+
61
+ candlewidth_half = 0.3
62
+ volumewidth_half = 0.36
63
+ df['x'] = df.index + 0.5
64
+ df['left'] = df['x'] - candlewidth_half
65
+ df['right'] = df['x'] + candlewidth_half
66
+ df['vleft'] = df['x'] - volumewidth_half
67
+ df['vright'] = df['x'] + volumewidth_half
68
+
69
+ df['top'] = np.where(df['open'] <= df['close'], df['close'], df['open'])
70
+ df['top'] = np.where(df['close'] < df['open'], df['open'], df['close'])
71
+ df['bottom'] = np.where(df['open'] <= df['close'], df['open'], df['close'])
72
+ df['bottom'] = np.where(df['close'] < df['open'], df['close'], df['open'])
73
+
74
+ # 양봉
75
+ df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
76
+ if self.color_up != self.color_down:
77
+ # 음봉
78
+ df.loc[df['close'] < df['open'], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
79
+ if self.color_up != self.color_flat:
80
+ # 보합
81
+ df.loc[df['close'] == df['open'], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
82
+ if self.color_up != self.color_up_down:
83
+ # 양봉(비우기)
84
+ df.loc[(df['facecolor'] == self.color_up) & (df['close'] < df['close'].shift(1)), 'facecolor'] = self.color_up_down
85
+ if self.color_down != self.color_down_up:
86
+ # 음봉(비우기)
87
+ df.loc[(df['facecolor'] == self.color_down) & (df['close'].shift(1) < df['close']), ['facecolor']] = self.color_down_up
88
+
89
+ self.df = df
90
+ return
91
+
92
+
93
+ class CollectionMixin(DataMixin):
94
+ color_sliderline = 'k'
95
+
96
+ _masegment, _macolors = ({}, {})
97
+
98
+ def __init__(self, *args, **kwargs):
99
+ super().__init__(*args, **kwargs)
100
+
101
+ self._connect_event()
102
+ self._add_collection()
103
+ return
104
+
105
+ def _connect_event(self):
106
+ self.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
107
+ return
108
+
109
+ def _add_collection(self):
110
+ self.macollection = LineCollection([], animated=True, antialiased=True, linewidth=1)
111
+ self.ax_price.add_collection(self.macollection)
112
+
113
+ self.slidercollection = LineCollection([], animated=True, antialiased=True)
114
+ self.ax_slider.add_collection(self.slidercollection)
115
+
116
+ # https://white.seolpyo.com/entry/145/
117
+ self.candlecollection = LineCollection([], animated=True, antialiased=True, linewidths=1)
118
+ self.ax_price.add_collection(self.candlecollection)
119
+
120
+ # https://white.seolpyo.com/entry/145/
121
+ self.volumecollection = LineCollection([], animated=True, antialiased=True, facecolors=self.colors_volume, linewidths=1, edgecolors='k')
122
+ self.ax_volume.add_collection(self.volumecollection)
123
+
124
+ return
125
+
126
+ def _get_candlesegment(self, s: pd.Series):
127
+ v = s.values
128
+ segment = (
129
+ (v[0], v[3]), # 심지 상단
130
+ (v[0], v[5]), # 몸통 상단
131
+ (v[1], v[5]), # 몸통 상단 좌측
132
+ (v[1], v[6]), # 몸통 하단 좌측
133
+ (v[0], v[6]), # 몸통 하단
134
+ (v[0], v[4]), # 심지 하단
135
+ (v[0], v[6]), # 몸통 하단
136
+ (v[2], v[6]), # 몸통 하단 우측
137
+ (v[2], v[5]), # 몸통 상단 우측
138
+ (v[0], v[5]), # 몸통 상단
139
+ )
140
+ return segment
141
+
142
+ def _get_volumesegment(self, s: pd.Series):
143
+ v = s.values
144
+ segment = (
145
+ (v[0], 0), # 몸통 하단 좌측
146
+ (v[0], v[2]), # 몸통 상단 좌측
147
+ (v[1], v[2]), # 몸통 상단 우측
148
+ (v[1], 0), # 몸통 하단 우측
149
+ )
150
+ return segment
151
+
152
+ def _set_collection(self):
153
+ self.df.loc[:, ['candlesegment']] = self.df[['x', 'left', 'right', self.high, self.low, 'top', 'bottom']].agg(self._get_candlesegment, axis=1)
154
+ self.df.loc[:, ['volumesegment']] = self.df[['vleft', 'vright', self.volume]].agg(self._get_volumesegment, axis=1)
155
+
156
+ self._set_macollection()
157
+
158
+ # 가격이동평균선
159
+ segments = list(reversed(self._masegment.values()))
160
+ colors, widths = ([], [])
161
+ for i in reversed(self._macolors.values()): (colors.append(i), widths.append(1))
162
+ self.macollection.set_segments(segments)
163
+ self.macollection.set_edgecolor(colors)
164
+
165
+ # 슬라이더 선형차트
166
+ segments.append(self.df[['x', self.close]].apply(tuple, axis=1).tolist())
167
+ (colors.append(self.color_sliderline), widths.append(1.8))
168
+ self.slidercollection.set_segments(segments)
169
+ self.slidercollection.set_edgecolor(colors)
170
+ self.slidercollection.set_linewidth(widths)
171
+
172
+ self.candlecollection.set_segments(self.df['candlesegment'])
173
+ self.candlecollection.set_facecolor(self.df['facecolor'].values)
174
+ self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
175
+
176
+ self.volumecollection.set_segments(self.df['volumesegment'])
177
+ return
178
+
179
+ def _set_macollection(self):
180
+ # 기존 legend 제거
181
+ legends = self.ax_legend.get_legend()
182
+ if legends: legends.remove()
183
+
184
+ self._masegment.clear(), self._macolors.clear()
185
+ handles, labels = ([], [])
186
+ self._visible_ma.clear()
187
+ for n, i in enumerate(self.list_ma):
188
+ try: c = self.list_macolor[n]
189
+ except: c = self.color_sliderline
190
+ self._macolors[i] = c
191
+ self._masegment[i] = self.df[['x', f'ma{i}']].apply(tuple, axis=1).tolist()
192
+
193
+ handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
194
+ labels.append(self.label_ma.format(i))
195
+
196
+ self._visible_ma.add(i)
197
+
198
+ # 가격이동평균선 legend 생성
199
+ if handles:
200
+ legends = self.ax_legend.legend(handles, labels, loc='lower left', ncol=10)
201
+
202
+ for i in legends.legend_handles:
203
+ i.set_picker(5)
204
+ return
205
+
206
+ def _on_pick(self, e):
207
+ self._pick_ma_action(e)
208
+
209
+ return self._draw()
210
+
211
+ def _pick_ma_action(self, e: PickEvent):
212
+ handle = e.artist
213
+ if e.artist.get_alpha() == 0.2:
214
+ visible = True
215
+ handle.set_alpha(1.0)
216
+ else:
217
+ visible = False
218
+ handle.set_alpha(0.2)
219
+
220
+ n = int(handle.get_label())
221
+
222
+ if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
223
+ else: self._visible_ma = {i for i in self._visible_ma if i != n}
224
+
225
+ self.macollection.set_segments([self._masegment[i] for i in reversed(self._masegment) if i in self._visible_ma])
226
+ colors = [self._macolors[i] for i in reversed(self._macolors) if i in self._visible_ma]
227
+ self.macollection.set_colors(colors)
228
+ return
229
+
230
+ def _draw(self):
231
+ if self.fig.canvas is not self.canvas:
232
+ self.canvas = self.fig.canvas
233
+ self.canvas.draw()
234
+ return
235
+
236
+
237
+ class BackgroundMixin(CollectionMixin):
238
+ background = None
239
+ candle_on_ma = True
240
+
241
+ _creating_background = False
242
+
243
+ def __init__(self, *args, **kwargs):
244
+ super().__init__(*args, **kwargs)
245
+ return
246
+
247
+ def _connect_event(self):
248
+ super()._connect_event()
249
+ self.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
250
+ return
251
+
252
+ def _create_background(self):
253
+ if self._creating_background: return
254
+
255
+ if self.fig.canvas is not self.canvas:
256
+ self.canvas = self.fig.canvas
257
+
258
+ self._creating_background = True
259
+ self._copy_bbox()
260
+ self._creating_background = False
261
+ return
262
+
263
+ def _copy_bbox(self):
264
+ self._draw_artist()
265
+ self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
266
+ return
267
+
268
+ def _draw_artist(self):
269
+ renderer = self.canvas.renderer
270
+
271
+ self.ax_slider.xaxis.draw(renderer)
272
+ self.ax_slider.yaxis.draw(renderer)
273
+
274
+ self.slidercollection.draw(renderer)
275
+
276
+ self.ax_price.xaxis.draw(renderer)
277
+ self.ax_price.yaxis.draw(renderer)
278
+
279
+ if self.candle_on_ma:
280
+ self.macollection.draw(renderer)
281
+ self.candlecollection.draw(renderer)
282
+ else:
283
+ self.candlecollection.draw(renderer)
284
+ self.macollection.draw(renderer)
285
+
286
+ self.ax_volume.xaxis.draw(renderer)
287
+ self.ax_volume.yaxis.draw(renderer)
288
+
289
+ self.volumecollection.draw(renderer)
290
+ return
291
+
292
+ def _on_draw(self, e):
293
+ self.background = None
294
+ self._restore_region()
295
+ return
296
+
297
+ def _restore_region(self):
298
+ if not self.background: self._create_background()
299
+
300
+ self.canvas.renderer.restore_region(self.background)
301
+ return
302
+
303
+
304
+ class DrawMixin(BackgroundMixin):
305
+ def set_data(self, df: pd.DataFrame):
306
+ self._generate_data(df)
307
+ self._set_collection()
308
+ self._draw_collection()
309
+ return
310
+
311
+ def _draw_collection(self):
312
+ xmax = self.df['x'].values[-1] + 1
313
+
314
+ xspace = xmax / 40
315
+ self.xmin, self.xmax = (-xspace, xmax+xspace)
316
+ # 슬라이더 xlim
317
+ self.ax_slider.set_xlim(self.xmin, self.xmax)
318
+ # 주가 xlim
319
+ self.ax_price.set_xlim(self.xmin, self.xmax)
320
+ # 거래량 xlim
321
+ self.ax_volume.set_xlim(self.xmin, self.xmax)
322
+
323
+ ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
324
+ ysub = (ymax - ymin) / 15
325
+
326
+ # 슬라이더 ylim
327
+ self._slider_ymin, self._slider_ymax = (ymin-ysub, ymax+ysub)
328
+ self.ax_slider.set_ylim(self._slider_ymin, self._slider_ymax)
329
+
330
+ # 주가 ylim
331
+ self._price_ymin, self._price_ymax = (ymin-ysub, ymax+ysub)
332
+ self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
333
+
334
+ # 거래량 ylim
335
+ self._vol_ymax = self.df[self.volume].max() * 1.2
336
+ self.ax_volume.set_ylim(0, self._vol_ymax)
337
+ return
338
+
339
+
340
+ class Chart(DrawMixin, Mixin):
341
+ def _generate_data(self, df):
342
+ super()._generate_data(df)
343
+ return self.generate_data(self.df)
344
+
345
+ def _on_draw(self, e):
346
+ super()._on_draw(e)
347
+ return self.on_draw(e)
348
+
349
+ def _on_pick(self, e):
350
+ self.on_pick(e)
351
+ return super()._on_pick(e)
352
+
353
+
354
+ if __name__ == '__main__':
355
+ import json
356
+ from time import time
357
+
358
+ import matplotlib.pyplot as plt
359
+ from pathlib import Path
360
+
361
+ with open(Path(__file__).parent / 'data/samsung.txt', 'r', encoding='utf-8') as txt:
362
+ data = json.load(txt)
363
+ print(f'{len(data)=}')
364
+ # data = data[:200]
365
+ df = pd.DataFrame(data)
366
+
367
+ t = time()
368
+ DrawMixin().set_data(df)
369
+ t2 = time() - t
370
+ print(f'{t2=}')
371
+ plt.show()