seolpyo-mplchart 0.1.3.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.
- seolpyo_mplchart/__init__.py +120 -0
- seolpyo_mplchart/base.py +111 -0
- seolpyo_mplchart/cursor.py +448 -0
- seolpyo_mplchart/data/apple.txt +64187 -0
- seolpyo_mplchart/data/samsung.txt +120002 -0
- seolpyo_mplchart/draw.py +371 -0
- seolpyo_mplchart/slider.py +550 -0
- seolpyo_mplchart/test.py +38 -0
- seolpyo_mplchart/utils.py +45 -0
- seolpyo_mplchart-0.1.3.1.dist-info/METADATA +49 -0
- seolpyo_mplchart-0.1.3.1.dist-info/RECORD +13 -0
- seolpyo_mplchart-0.1.3.1.dist-info/WHEEL +5 -0
- seolpyo_mplchart-0.1.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
from matplotlib.collections import LineCollection
|
|
2
|
+
from matplotlib.backend_bases import MouseEvent, MouseButton, cursors
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .cursor import CursorMixin, Chart as CM
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Mixin:
|
|
10
|
+
def on_click(self, e):
|
|
11
|
+
"This function works if mouse button click event active."
|
|
12
|
+
return
|
|
13
|
+
def on_release(self, e):
|
|
14
|
+
"This function works if mouse button release event active."
|
|
15
|
+
return
|
|
16
|
+
def draw_artist(self):
|
|
17
|
+
"This function works before canvas.blit()."
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NavgatorMixin(CursorMixin):
|
|
22
|
+
min_distance = 30
|
|
23
|
+
color_navigatorline = '#1e78ff'
|
|
24
|
+
color_navigator = 'k'
|
|
25
|
+
|
|
26
|
+
_x_click, _x_release = (0, 0)
|
|
27
|
+
is_click, is_move = (False, False)
|
|
28
|
+
_navcoordinate = (0, 0)
|
|
29
|
+
|
|
30
|
+
def _add_collection(self):
|
|
31
|
+
super()._add_collection()
|
|
32
|
+
|
|
33
|
+
# 슬라이더 네비게이터
|
|
34
|
+
self.navigator = LineCollection([], animated=True, edgecolors=[self.color_navigator, self.color_navigatorline], alpha=(0.2, 1.0))
|
|
35
|
+
self.ax_slider.add_artist(self.navigator)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
39
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
|
|
40
|
+
|
|
41
|
+
# 네비게이터 라인 선택 영역
|
|
42
|
+
xsub = self.xmax - self.xmin
|
|
43
|
+
self._navLineWidth = xsub * 0.008
|
|
44
|
+
if self._navLineWidth < 1: self._navLineWidth = 1
|
|
45
|
+
self._navLineWidth_half = self._navLineWidth / 2
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
def _connect_event(self):
|
|
49
|
+
super()._connect_event()
|
|
50
|
+
self.canvas.mpl_connect('axes_leave_event', lambda x: self._leave_axes(x))
|
|
51
|
+
self.canvas.mpl_connect('button_press_event', lambda x: self._on_click(x))
|
|
52
|
+
self.canvas.mpl_connect('button_release_event', lambda x: self._on_release(x))
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
def _leave_axes(self, e: MouseEvent):
|
|
56
|
+
if not self.is_click and e.inaxes is self.ax_slider:
|
|
57
|
+
self.canvas.set_cursor(cursors.POINTER)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
def _on_click(self, e: MouseEvent):
|
|
61
|
+
if self.is_click or e.button != MouseButton.LEFT or e.inaxes is not self.ax_slider: return
|
|
62
|
+
|
|
63
|
+
self.is_click = True
|
|
64
|
+
|
|
65
|
+
x = e.xdata.__int__()
|
|
66
|
+
left, right = self._navcoordinate
|
|
67
|
+
lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
|
|
68
|
+
rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
|
|
69
|
+
|
|
70
|
+
gtl, ltr = (lmax < x, x < rmin)
|
|
71
|
+
if gtl and ltr:
|
|
72
|
+
self._x_click = x
|
|
73
|
+
self.is_move = True
|
|
74
|
+
self.canvas.set_cursor(cursors.MOVE)
|
|
75
|
+
else:
|
|
76
|
+
self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
77
|
+
if not gtl and lmin <= x:
|
|
78
|
+
self._x_click = right
|
|
79
|
+
elif not ltr and x <= rmax:
|
|
80
|
+
self._x_click = left
|
|
81
|
+
else:
|
|
82
|
+
self._x_click = x
|
|
83
|
+
|
|
84
|
+
# 그리기 후 최초 클릭이면 좌표 수정
|
|
85
|
+
if left == right:
|
|
86
|
+
self._navcoordinate = (x, x)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def _on_release(self, e: MouseEvent):
|
|
90
|
+
if e.inaxes is not self.ax_slider: return
|
|
91
|
+
self.is_click, self.is_move = (False, False)
|
|
92
|
+
|
|
93
|
+
if self._navcoordinate[0] == self._navcoordinate[1]:
|
|
94
|
+
self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class BackgroundMixin(NavgatorMixin):
|
|
99
|
+
def _on_draw(self, e):
|
|
100
|
+
self.background = None
|
|
101
|
+
self._restore_region()
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
def _restore_region(self, with_nav=True, empty=False, empty_with_nav=False):
|
|
105
|
+
if not self.background: self._create_background()
|
|
106
|
+
|
|
107
|
+
if empty: self.canvas.restore_region(self.background_empty)
|
|
108
|
+
elif empty_with_nav: self.canvas.restore_region(self.background_empty_with_nav)
|
|
109
|
+
elif with_nav: self.canvas.restore_region(self.background_with_nav)
|
|
110
|
+
else: self.canvas.renderer.restore_region(self.background)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def _copy_bbox(self):
|
|
114
|
+
renderer = self.canvas.renderer
|
|
115
|
+
|
|
116
|
+
self.background_empty = renderer.copy_from_bbox(self.fig.bbox)
|
|
117
|
+
|
|
118
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
119
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
120
|
+
self.slidercollection.draw(renderer)
|
|
121
|
+
self.background_empty_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
122
|
+
|
|
123
|
+
self._draw_artist()
|
|
124
|
+
self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
125
|
+
|
|
126
|
+
self.navigator.draw(self.canvas.renderer)
|
|
127
|
+
self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
def _draw_artist(self):
|
|
131
|
+
renderer = self.canvas.renderer
|
|
132
|
+
|
|
133
|
+
self.ax_price.xaxis.draw(renderer)
|
|
134
|
+
self.ax_price.yaxis.draw(renderer)
|
|
135
|
+
|
|
136
|
+
if self.candle_on_ma:
|
|
137
|
+
self.macollection.draw(renderer)
|
|
138
|
+
self.candlecollection.draw(renderer)
|
|
139
|
+
else:
|
|
140
|
+
self.candlecollection.draw(renderer)
|
|
141
|
+
self.macollection.draw(renderer)
|
|
142
|
+
|
|
143
|
+
self.ax_volume.xaxis.draw(renderer)
|
|
144
|
+
self.ax_volume.yaxis.draw(renderer)
|
|
145
|
+
|
|
146
|
+
self.volumecollection.draw(renderer)
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class DrawMixin(BackgroundMixin):
|
|
151
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
152
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
|
|
153
|
+
|
|
154
|
+
# 네비게이터 높이 설정
|
|
155
|
+
ysub = self._slider_ymax - self._slider_ymin
|
|
156
|
+
self._ymiddle = self._slider_ymax - ysub / 2
|
|
157
|
+
self.navigator.set_linewidth((ysub, 5))
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
def _on_release(self, e: MouseEvent):
|
|
161
|
+
super()._on_release(e)
|
|
162
|
+
self._set_navigator(*self._navcoordinate)
|
|
163
|
+
|
|
164
|
+
self._restore_region(empty=True)
|
|
165
|
+
self._creating_background = False
|
|
166
|
+
self._create_background()
|
|
167
|
+
self._restore_region()
|
|
168
|
+
self._blit()
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
def _on_move(self, e: MouseEvent):
|
|
172
|
+
self._restore_region((not self.is_click))
|
|
173
|
+
|
|
174
|
+
self._on_move_action(e)
|
|
175
|
+
|
|
176
|
+
if self.in_slider:
|
|
177
|
+
self._change_coordinate()
|
|
178
|
+
if self.is_click:
|
|
179
|
+
if self.is_move: self._set_navigator(*self._navcoordinate)
|
|
180
|
+
elif self.intx is not None: self._set_navigator(self._x_click, self.intx)
|
|
181
|
+
self.navigator.draw(self.canvas.renderer)
|
|
182
|
+
self._slider_move_action(e)
|
|
183
|
+
elif self.is_click:
|
|
184
|
+
self.navigator.draw(self.canvas.renderer)
|
|
185
|
+
else:
|
|
186
|
+
if self.in_slider or self.in_price or self.in_volume:
|
|
187
|
+
self._slider_move_action(e)
|
|
188
|
+
if self.in_price or self.in_volume:
|
|
189
|
+
self._chart_move_action(e)
|
|
190
|
+
|
|
191
|
+
self._blit()
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
def _change_coordinate(self):
|
|
195
|
+
if self.intx is None: return
|
|
196
|
+
x = self.intx
|
|
197
|
+
left, right = self._navcoordinate
|
|
198
|
+
|
|
199
|
+
if not self.is_click:
|
|
200
|
+
lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
|
|
201
|
+
rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
|
|
202
|
+
ltel, gter = (x <= lmax, rmin <= x)
|
|
203
|
+
if ltel and lmin <= x:
|
|
204
|
+
self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
205
|
+
elif gter and x <= rmax:
|
|
206
|
+
self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
207
|
+
elif not ltel and not gter: self.canvas.set_cursor(cursors.MOVE)
|
|
208
|
+
else: self.canvas.set_cursor(cursors.POINTER)
|
|
209
|
+
else:
|
|
210
|
+
# 네비게이터 좌표 수정
|
|
211
|
+
intx = x.__int__()
|
|
212
|
+
if self.is_move:
|
|
213
|
+
xsub = self._x_click - intx
|
|
214
|
+
left, right = (left-xsub, right-xsub)
|
|
215
|
+
self._x_click = intx
|
|
216
|
+
else:
|
|
217
|
+
if intx == left: left = intx
|
|
218
|
+
elif intx == right: right = intx
|
|
219
|
+
else:
|
|
220
|
+
if self._x_click < intx: left, right = (self._x_click, intx)
|
|
221
|
+
else: left, right = (intx, self._x_click)
|
|
222
|
+
|
|
223
|
+
nsub = right - left
|
|
224
|
+
if right < 0 or self.df.index[-1] < left or nsub < self.min_distance: left, right = self._navcoordinate
|
|
225
|
+
self._navcoordinate = (left, right)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
def _set_navigator(self, x1, x2):
|
|
229
|
+
xmin, xmax = (x1, x2) if x1 < x2 else (x2, x1)
|
|
230
|
+
|
|
231
|
+
left = ((self.xmin, self._ymiddle), (xmin, self._ymiddle))
|
|
232
|
+
right = ((xmax, self._ymiddle), (self.xmax, self._ymiddle))
|
|
233
|
+
leftline = ((xmin, self._slider_ymin), (xmin, self._slider_ymax))
|
|
234
|
+
rightline = ((xmax, self._slider_ymin), (xmax, self._slider_ymax))
|
|
235
|
+
self.navigator.set_segments((left, leftline, right, rightline))
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class LimMixin(DrawMixin):
|
|
240
|
+
def _on_release(self, e: MouseEvent):
|
|
241
|
+
if e.inaxes is not self.ax_slider: return
|
|
242
|
+
self.is_click, self.is_move = (False, False)
|
|
243
|
+
|
|
244
|
+
if self._navcoordinate[0] == self._navcoordinate[1]:
|
|
245
|
+
self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
|
|
246
|
+
self._set_navigator(*self._navcoordinate)
|
|
247
|
+
self._lim()
|
|
248
|
+
|
|
249
|
+
self._restore_region(empty=True)
|
|
250
|
+
self._copy_bbox()
|
|
251
|
+
self._restore_region()
|
|
252
|
+
self._blit()
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
def _on_move(self, e):
|
|
256
|
+
self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
|
|
257
|
+
|
|
258
|
+
self._on_move_action(e)
|
|
259
|
+
|
|
260
|
+
if self.in_slider:
|
|
261
|
+
self._change_coordinate()
|
|
262
|
+
if self.is_click:
|
|
263
|
+
nsub = self._navcoordinate[1] - self._navcoordinate[0]
|
|
264
|
+
if self.min_distance <= nsub: self._lim()
|
|
265
|
+
if self.is_move: self._set_navigator(*self._navcoordinate)
|
|
266
|
+
elif self.intx is not None: self._set_navigator(self._x_click, self.intx)
|
|
267
|
+
self.navigator.draw(self.canvas.renderer)
|
|
268
|
+
self._draw_blit_artist()
|
|
269
|
+
self._slider_move_action(e)
|
|
270
|
+
elif self.is_click:
|
|
271
|
+
self.navigator.draw(self.canvas.renderer)
|
|
272
|
+
self._draw_blit_artist()
|
|
273
|
+
else:
|
|
274
|
+
if self.in_slider or self.in_price or self.in_volume:
|
|
275
|
+
self._slider_move_action(e)
|
|
276
|
+
if self.in_price or self.in_volume:
|
|
277
|
+
self._chart_move_action(e)
|
|
278
|
+
|
|
279
|
+
self._blit()
|
|
280
|
+
return
|
|
281
|
+
|
|
282
|
+
def _draw_blit_artist(self):
|
|
283
|
+
return self._draw_artist()
|
|
284
|
+
|
|
285
|
+
def _lim(self):
|
|
286
|
+
xmin, xmax = self._navcoordinate
|
|
287
|
+
|
|
288
|
+
xmax += 1
|
|
289
|
+
self.ax_price.set_xlim(xmin, xmax)
|
|
290
|
+
self.ax_volume.set_xlim(xmin, xmax)
|
|
291
|
+
|
|
292
|
+
indmin, indmax = (xmin, xmax)
|
|
293
|
+
if xmin < 0: indmin = 0
|
|
294
|
+
if indmax < 1: indmax = 1
|
|
295
|
+
if indmin == indmax: indmax += 1
|
|
296
|
+
ymin, ymax = (self.df[self.low][indmin:indmax].min(), self.df[self.high][indmin:indmax].max())
|
|
297
|
+
ysub = (ymax - ymin) / 15
|
|
298
|
+
pmin, pmax = (ymin-ysub, ymax+ysub)
|
|
299
|
+
self.ax_price.set_ylim(pmin, pmax)
|
|
300
|
+
|
|
301
|
+
ymax = self.df[self.volume][indmin:indmax].max()
|
|
302
|
+
# self._vol_ymax = ymax*1.2
|
|
303
|
+
volmax = ymax * 1.2
|
|
304
|
+
self.ax_volume.set_ylim(0, volmax)
|
|
305
|
+
|
|
306
|
+
self.set_text_coordante(xmin, xmax, pmin, pmax, volmax)
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class SimpleMixin(LimMixin):
|
|
311
|
+
simpler = False
|
|
312
|
+
limit_volume = 1_500
|
|
313
|
+
default_left, default_right = (180, 10)
|
|
314
|
+
_draw_blit = False
|
|
315
|
+
|
|
316
|
+
def __init__(self, *args, **kwargs):
|
|
317
|
+
super().__init__(*args, **kwargs)
|
|
318
|
+
|
|
319
|
+
# 영역 이동시 주가 collection
|
|
320
|
+
self.blitcandle = LineCollection([], animated=True)
|
|
321
|
+
self.ax_price.add_collection(self.blitcandle)
|
|
322
|
+
self.priceline = LineCollection([], animated=True, edgecolors='k')
|
|
323
|
+
self.ax_price.add_artist(self.priceline)
|
|
324
|
+
|
|
325
|
+
# 영역 이동시 거래량 collection
|
|
326
|
+
self.blitvolume = LineCollection([], animated=True, edgecolors=self.colors_volume)
|
|
327
|
+
self.ax_volume.add_collection(self.blitvolume)
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
331
|
+
super()._set_data(df, sort_df, calc_ma, False, calc_info)
|
|
332
|
+
|
|
333
|
+
seg = self.df[['x', self.high, 'x', self.low]].values
|
|
334
|
+
seg = seg.reshape(seg.shape[0], 2, 2)
|
|
335
|
+
self.blitcandle.set_segments(seg)
|
|
336
|
+
self.blitcandle.set_edgecolor(self.df['edgecolor'])
|
|
337
|
+
|
|
338
|
+
pseg = self.df[['x', self.close]].values
|
|
339
|
+
self.priceline.set_verts(pseg.reshape(1, *pseg.shape))
|
|
340
|
+
|
|
341
|
+
l = self.df.__len__()
|
|
342
|
+
if l < self.limit_volume:
|
|
343
|
+
volseg = self.df.loc[:, ['x', 'zero', 'x', self.volume]].values
|
|
344
|
+
else:
|
|
345
|
+
v = self.df[['x', 'zero', 'x', self.volume]].sort_values([self.volume], axis=0, ascending=False)
|
|
346
|
+
volseg = v[:self.limit_volume].values
|
|
347
|
+
|
|
348
|
+
self.blitvolume.set_segments(volseg.reshape(volseg.shape[0], 2, 2))
|
|
349
|
+
|
|
350
|
+
if change_lim:
|
|
351
|
+
index = self.df.index[-1]
|
|
352
|
+
if index < self.default_left + self.default_right: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
|
|
353
|
+
else: self._navcoordinate = (index-self.default_left, index+self.default_right)
|
|
354
|
+
|
|
355
|
+
self._set_navigator(*self._navcoordinate)
|
|
356
|
+
self._lim()
|
|
357
|
+
return
|
|
358
|
+
|
|
359
|
+
def _draw_blit_artist(self):
|
|
360
|
+
renderer = self.canvas.renderer
|
|
361
|
+
|
|
362
|
+
self.ax_price.xaxis.draw(renderer)
|
|
363
|
+
self.ax_price.yaxis.draw(renderer)
|
|
364
|
+
|
|
365
|
+
if self.simpler:
|
|
366
|
+
if self._draw_blit: self.priceline.draw(renderer)
|
|
367
|
+
else: self.blitcandle.draw(renderer)
|
|
368
|
+
elif self.candle_on_ma:
|
|
369
|
+
self.macollection.draw(renderer)
|
|
370
|
+
if self._draw_blit: self.blitcandle.draw(renderer)
|
|
371
|
+
else: self.candlecollection.draw(renderer)
|
|
372
|
+
else:
|
|
373
|
+
if self._draw_blit: self.blitcandle.draw(renderer)
|
|
374
|
+
else: self.candlecollection.draw(renderer)
|
|
375
|
+
self.macollection.draw(renderer)
|
|
376
|
+
|
|
377
|
+
self.ax_volume.xaxis.draw(renderer)
|
|
378
|
+
self.ax_volume.yaxis.draw(renderer)
|
|
379
|
+
|
|
380
|
+
self.blitvolume.draw(renderer)
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class ClickMixin(SimpleMixin):
|
|
385
|
+
is_click_chart = False
|
|
386
|
+
|
|
387
|
+
def _on_click(self, e: MouseEvent):
|
|
388
|
+
if not self.is_click and e.button == MouseButton.LEFT:
|
|
389
|
+
if e.inaxes is self.ax_slider: pass
|
|
390
|
+
elif e.inaxes is self.ax_price or e.inaxes is self.ax_volume: return self._on_chart_click(e)
|
|
391
|
+
else: return
|
|
392
|
+
else: return
|
|
393
|
+
|
|
394
|
+
self.is_click = True
|
|
395
|
+
|
|
396
|
+
x = e.xdata.__int__()
|
|
397
|
+
left, right = self._navcoordinate
|
|
398
|
+
lmin, lmax = (left-self._navLineWidth, left+self._navLineWidth_half)
|
|
399
|
+
rmin, rmax = (right-self._navLineWidth_half, right+self._navLineWidth)
|
|
400
|
+
|
|
401
|
+
gtl, ltr = (lmax < x, x < rmin)
|
|
402
|
+
if gtl and ltr:
|
|
403
|
+
self._x_click = x
|
|
404
|
+
self.is_move = True
|
|
405
|
+
self.canvas.set_cursor(cursors.MOVE)
|
|
406
|
+
else:
|
|
407
|
+
self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
408
|
+
if not gtl and lmin <= x:
|
|
409
|
+
self._x_click = right
|
|
410
|
+
elif not ltr and x <= rmax:
|
|
411
|
+
self._x_click = left
|
|
412
|
+
else:
|
|
413
|
+
self._x_click = x
|
|
414
|
+
|
|
415
|
+
# 그리기 후 최초 클릭이면 좌표 수정
|
|
416
|
+
if left == right:
|
|
417
|
+
self._navcoordinate = (x, x)
|
|
418
|
+
return
|
|
419
|
+
|
|
420
|
+
def _on_release(self, e: MouseEvent):
|
|
421
|
+
if not self.is_click: return
|
|
422
|
+
elif e.inaxes is self.ax_slider: return super()._on_release(e)
|
|
423
|
+
elif not self.in_price and not self.in_volume and not self.is_click_chart: return
|
|
424
|
+
# 차트 click release action
|
|
425
|
+
self.canvas.set_cursor(cursors.POINTER)
|
|
426
|
+
self.is_click, self.is_move = (False, False)
|
|
427
|
+
self.is_click_chart = False
|
|
428
|
+
|
|
429
|
+
self._restore_region(empty=True)
|
|
430
|
+
self._copy_bbox()
|
|
431
|
+
self._restore_region()
|
|
432
|
+
self._blit()
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
def _on_chart_click(self, e: MouseEvent):
|
|
436
|
+
self.is_click = True
|
|
437
|
+
self.is_click_chart = True
|
|
438
|
+
self._x_click = e.x.__int__()
|
|
439
|
+
self.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
def _change_coordinate(self):
|
|
443
|
+
if self.is_click_chart: self._change_coordinate_chart()
|
|
444
|
+
else: super()._change_coordinate()
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
def _change_coordinate_chart(self, e: MouseEvent):
|
|
448
|
+
x = e.x.__int__()
|
|
449
|
+
left, right = self._navcoordinate
|
|
450
|
+
nsub = right - left
|
|
451
|
+
xsub = x - self._x_click
|
|
452
|
+
xdiv = (xsub / (1200 / nsub)).__int__()
|
|
453
|
+
if xdiv:
|
|
454
|
+
left, right = (left-xdiv, right-xdiv)
|
|
455
|
+
if -1 < right and left < self.df.index[-1]:
|
|
456
|
+
self._navcoordinate = (left, right)
|
|
457
|
+
self._x_click = x
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
def _on_move(self, e):
|
|
461
|
+
self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
|
|
462
|
+
|
|
463
|
+
self._on_move_action(e)
|
|
464
|
+
|
|
465
|
+
if self.in_slider and not self.is_click_chart:
|
|
466
|
+
self._change_coordinate()
|
|
467
|
+
if self.is_click:
|
|
468
|
+
nsub = self._navcoordinate[1] - self._navcoordinate[0]
|
|
469
|
+
if self.is_move: self._set_navigator(*self._navcoordinate)
|
|
470
|
+
else:
|
|
471
|
+
self._draw_blit = 900 < nsub
|
|
472
|
+
if self.intx is not None: self._set_navigator(self._x_click, self.intx)
|
|
473
|
+
|
|
474
|
+
if self.min_distance <= nsub: self._lim()
|
|
475
|
+
|
|
476
|
+
self.navigator.draw(self.canvas.renderer)
|
|
477
|
+
self._draw_blit_artist()
|
|
478
|
+
self._slider_move_action(e)
|
|
479
|
+
elif self.is_click:
|
|
480
|
+
if self.is_click_chart and (self.in_price or self.in_volume):
|
|
481
|
+
if (self.vmin, self.vmax) != self._navcoordinate:
|
|
482
|
+
self._change_coordinate_chart(e)
|
|
483
|
+
self._lim()
|
|
484
|
+
self._set_navigator(*self._navcoordinate)
|
|
485
|
+
self.navigator.draw(self.canvas.renderer)
|
|
486
|
+
self._draw_blit_artist()
|
|
487
|
+
else:
|
|
488
|
+
if self.in_slider or self.in_price or self.in_volume:
|
|
489
|
+
self._slider_move_action(e)
|
|
490
|
+
if self.in_price or self.in_volume:
|
|
491
|
+
self._chart_move_action(e)
|
|
492
|
+
|
|
493
|
+
self._blit()
|
|
494
|
+
return
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class SliderMixin(ClickMixin):
|
|
498
|
+
pass
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class Chart(SliderMixin, CM, Mixin):
|
|
502
|
+
def _on_draw(self, e):
|
|
503
|
+
super()._on_draw(e)
|
|
504
|
+
return self.on_draw(e)
|
|
505
|
+
|
|
506
|
+
def _on_pick(self, e):
|
|
507
|
+
self.on_pick(e)
|
|
508
|
+
return super()._on_pick(e)
|
|
509
|
+
|
|
510
|
+
def _on_move(self, e):
|
|
511
|
+
super()._on_move(e)
|
|
512
|
+
return self.on_move(e)
|
|
513
|
+
|
|
514
|
+
def _draw_artist(self):
|
|
515
|
+
super()._draw_artist()
|
|
516
|
+
return self.draw_artist()
|
|
517
|
+
def _draw_blit_artist(self):
|
|
518
|
+
super()._draw_blit_artist()
|
|
519
|
+
return self.draw_artist()
|
|
520
|
+
|
|
521
|
+
def _on_click(self, e):
|
|
522
|
+
super()._on_click(e)
|
|
523
|
+
return self.on_click(e)
|
|
524
|
+
def _on_release(self, e):
|
|
525
|
+
super()._on_release(e)
|
|
526
|
+
return self.on_release(e)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
if __name__ == '__main__':
|
|
530
|
+
import json
|
|
531
|
+
from time import time
|
|
532
|
+
|
|
533
|
+
import matplotlib.pyplot as plt
|
|
534
|
+
from pathlib import Path
|
|
535
|
+
|
|
536
|
+
file = Path(__file__).parent / 'data/samsung.txt'
|
|
537
|
+
# file = Path(__file__).parent / 'data/apple.txt'
|
|
538
|
+
with open(file, 'r', encoding='utf-8') as txt:
|
|
539
|
+
data = json.load(txt)
|
|
540
|
+
data = data
|
|
541
|
+
df = pd.DataFrame(data)
|
|
542
|
+
|
|
543
|
+
t = time()
|
|
544
|
+
# c = SimpleMixin()
|
|
545
|
+
c = SliderMixin()
|
|
546
|
+
c.set_data(df)
|
|
547
|
+
t2 = time() - t
|
|
548
|
+
print(f'{t2=}')
|
|
549
|
+
plt.show()
|
|
550
|
+
|
seolpyo_mplchart/test.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from seolpyo_mplchart import Chart
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_name = {'samsung', 'apple'}
|
|
13
|
+
def sample(name: Literal['samsung', 'apple']='samsung'):
|
|
14
|
+
if name not in _name:
|
|
15
|
+
print('name should be either samsung or apple.')
|
|
16
|
+
return
|
|
17
|
+
file = Path(__file__).parent / f'data/{name}.txt'
|
|
18
|
+
with open(file, 'r', encoding='utf-8') as txt:
|
|
19
|
+
data = json.load(txt)
|
|
20
|
+
data = data
|
|
21
|
+
df = pd.DataFrame(data)
|
|
22
|
+
|
|
23
|
+
c = Chart()
|
|
24
|
+
if name == 'apple':
|
|
25
|
+
c.unit_price = '$'
|
|
26
|
+
c.unit_volume = 'vol'
|
|
27
|
+
c.digit_price = 3
|
|
28
|
+
c.label_ma = 'ma{}'
|
|
29
|
+
c.candleformat = '{}\n\nclose: {}\nrate: {}\ncompare: {}\nopen: {}({})\nhigh: {}({})\nlow: {}({})\nvolume: {}({})'
|
|
30
|
+
c.volumeformat = '{}\n\nvolume: {}\nvolume rate: {}'
|
|
31
|
+
c.set_data(df)
|
|
32
|
+
plt.show()
|
|
33
|
+
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == '__main__':
|
|
38
|
+
sample('apple')
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from re import search
|
|
2
|
+
|
|
3
|
+
def convert_num(num):
|
|
4
|
+
if isinstance(num, float) and num % 1: return num
|
|
5
|
+
return int(num)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def float_to_str(num: float, digit=0, plus=False):
|
|
9
|
+
num.__round__(digit)
|
|
10
|
+
text = f'{num:+,.{digit}f}' if plus else f'{num:,.{digit}f}'
|
|
11
|
+
return text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
dict_unit = {
|
|
15
|
+
'경': 10_000_000_000_000_000,
|
|
16
|
+
'조': 1_000_000_000_000,
|
|
17
|
+
'억': 100_000_000,
|
|
18
|
+
'만': 10_000,
|
|
19
|
+
'천': 1_000,
|
|
20
|
+
}
|
|
21
|
+
dict_unit_en = {
|
|
22
|
+
'Qd': 1_000_000_000_000_000,
|
|
23
|
+
'T': 1_000_000_000_000,
|
|
24
|
+
'B': 1_000_000_000,
|
|
25
|
+
'M': 1_000_000,
|
|
26
|
+
'K': 1_000,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
def convert_unit(value, digit=0, word='원'):
|
|
30
|
+
v = abs(value)
|
|
31
|
+
du = dict_unit if search('[가-힣]', word) else dict_unit_en
|
|
32
|
+
for unit, n in du.items():
|
|
33
|
+
if n <= v:
|
|
34
|
+
num = value / n
|
|
35
|
+
return f'{float_to_str(num, digit)}{unit} {word}'
|
|
36
|
+
if not value % 1: value = int(value)
|
|
37
|
+
text = f'{float_to_str(value, digit)}{word}'
|
|
38
|
+
return text
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == '__main__':
|
|
42
|
+
a = 456.123
|
|
43
|
+
print(float_to_str(a))
|
|
44
|
+
print(float_to_str(a, 2))
|
|
45
|
+
print(float_to_str(a, 6))
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: seolpyo-mplchart
|
|
3
|
+
Version: 0.1.3.1
|
|
4
|
+
Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
|
|
5
|
+
Author-email: white-seolpyo <white-seolpyo@naver.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
Project-URL: Homepage, https://white.seolpyo.com/
|
|
8
|
+
Project-URL: Documentation(English), https://white.seolpyo.com/entry/148/
|
|
9
|
+
Project-URL: Documentation(한글), https://white.seolpyo.com/entry/147/
|
|
10
|
+
Project-URL: repository, https://github.com/white-seolpyo/seolpyo-mplchart
|
|
11
|
+
Project-URL: Issues, https://github.com/white-seolpyo/seolpyo-mplchart/issues
|
|
12
|
+
Keywords: chart,차트,stock,주식,invest,투자,finance,파이낸스,candle,캔들,candlestick,캔들스틱,matplotlib,mplfinance,pyqtgraph,finplot,virtual currency,가상화폐,coin,코인,bitcoin,비트코인,ethereum,이더리움
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Framework :: Matplotlib
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
Requires-Dist: matplotlib>=3.7.0
|
|
26
|
+
Requires-Dist: pandas>=2.0.0
|
|
27
|
+
|
|
28
|
+
# Donation
|
|
29
|
+
Bitcoin: 1MKCHW8smDZv5DFMiVkA5G3DeXcMn871ZX
|
|
30
|
+
|
|
31
|
+
Ethereum: 0x1c5fb8a5e0b1153cd4116c91736bd16fabf83520
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Document
|
|
35
|
+
[English](https://white.seolpyo.com/entry/148/)
|
|
36
|
+
|
|
37
|
+
[한글](https://white.seolpyo.com/entry/147/)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Sample
|
|
41
|
+

|
|
42
|
+
|
|
43
|
+
<img alt="tkinter sample gif" src="https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/images/with%20tkinter.gif?raw=true">
|
|
44
|
+
|
|
45
|
+

|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
<img alt="40,000 sample" src="https://github.com/white-seolpyo/seolpyo-mplchart/blob/main/images/40000.gif?raw=true">
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
seolpyo_mplchart/__init__.py,sha256=FiuciCaX9lU12gwuDBYUNw0yLD06p4o7o19kR5jGips,5248
|
|
2
|
+
seolpyo_mplchart/base.py,sha256=vQ4OOBm3nGwjJ4wjDLaD_3LGxYzlP6AWpI6SZrZiwnQ,3600
|
|
3
|
+
seolpyo_mplchart/cursor.py,sha256=rXGWf0p3oElnsVfEPObGVnD8dBMfvTgx2o6RermkMbE,18405
|
|
4
|
+
seolpyo_mplchart/draw.py,sha256=NJH1dnmfepafMlc7K2ccwZbv0FDS3ItSiirCq4gMlOI,13145
|
|
5
|
+
seolpyo_mplchart/slider.py,sha256=R29vyNAdJkLEXgpP4hxe9O0WoLgoOnPOUHuKFNpdcnw,19601
|
|
6
|
+
seolpyo_mplchart/test.py,sha256=cW2hoaVbRtoSXlpmA4i1BKHBjI3-FAqYq__kryxkrC8,1007
|
|
7
|
+
seolpyo_mplchart/utils.py,sha256=-8cq4-WwiqKQxtwu3NPxOVTDDvoWH28tu4OTWr4hPTg,1208
|
|
8
|
+
seolpyo_mplchart/data/apple.txt,sha256=0izAfweu1lLsC0IwVthdVlo9reG8KGbKGTSX5knI5Zc,1380864
|
|
9
|
+
seolpyo_mplchart/data/samsung.txt,sha256=UejaSkbzr4E5K3lkelCT0yJiWUPfmViBEaTyoXyphIs,2476424
|
|
10
|
+
seolpyo_mplchart-0.1.3.1.dist-info/METADATA,sha256=3fGWiUtSqRHxnuEEOLdwvPJAr8dJELx8RdGzU1wF9Sw,2347
|
|
11
|
+
seolpyo_mplchart-0.1.3.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
12
|
+
seolpyo_mplchart-0.1.3.1.dist-info/top_level.txt,sha256=KgqFn7rKWize7OjMaTCHxKm9ie6vqnyb5c8fN7y_tSo,17
|
|
13
|
+
seolpyo_mplchart-0.1.3.1.dist-info/RECORD,,
|