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