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.
- seolpyo_mplchart/__init__.py +49 -0
- seolpyo_mplchart/base.py +113 -0
- seolpyo_mplchart/cursor.py +433 -0
- seolpyo_mplchart/data/apple.txt +64187 -0
- seolpyo_mplchart/data/samsung.txt +120002 -0
- seolpyo_mplchart/draw.py +367 -0
- seolpyo_mplchart/slider.py +601 -0
- seolpyo_mplchart/test.py +38 -0
- seolpyo_mplchart/utils.py +45 -0
- seolpyo_mplchart-0.0.1.dist-info/METADATA +36 -0
- seolpyo_mplchart-0.0.1.dist-info/RECORD +13 -0
- seolpyo_mplchart-0.0.1.dist-info/WHEEL +5 -0
- seolpyo_mplchart-0.0.1.dist-info/top_level.txt +1 -0
seolpyo_mplchart/draw.py
ADDED
|
@@ -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()
|