seolpyo-mplchart 1.1.1__py3-none-any.whl → 1.2.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 +9 -9
- seolpyo_mplchart/_base.py +114 -0
- seolpyo_mplchart/_cursor.py +485 -0
- seolpyo_mplchart/_draw.py +591 -0
- seolpyo_mplchart/_slider.py +618 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.1.dist-info}/METADATA +1 -1
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.1.dist-info}/RECORD +9 -5
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.1.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
from matplotlib.backend_bases import PickEvent
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.lines import Line2D
|
|
4
|
+
from matplotlib.text import Text
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from ._base import Base
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Mixin:
|
|
13
|
+
def add_artist(self):
|
|
14
|
+
"This method work when ```__init__()``` run."
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
def draw_artist(self):
|
|
18
|
+
"This method work before ```figure.canvas.blit()```."
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def generate_data(self):
|
|
22
|
+
"This method work before create segments."
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def on_draw(self, e):
|
|
26
|
+
"If draw event active, This method work."
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
def on_pick(self, e):
|
|
30
|
+
"If draw pick active, This method work."
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
def set_segment(self, xmin, xmax, simpler=False, set_ma=True):
|
|
34
|
+
"This method work if xlim change."
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class CollectionMixin(Base):
|
|
39
|
+
facecolor_volume, edgecolor_volume = ('#1f77b4', 'k')
|
|
40
|
+
watermark = 'seolpyo mplchart'
|
|
41
|
+
|
|
42
|
+
def __init__(self, *args, **kwargs):
|
|
43
|
+
super().__init__(*args, **kwargs)
|
|
44
|
+
|
|
45
|
+
self._add_collection()
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
def _add_collection(self):
|
|
49
|
+
self.collection_ma = LineCollection([], animated=True, linewidths=1)
|
|
50
|
+
self.ax_price.add_collection(self.collection_ma)
|
|
51
|
+
|
|
52
|
+
self.collection_candle = LineCollection([], animated=True, linewidths=0.8)
|
|
53
|
+
self.ax_price.add_collection(self.collection_candle)
|
|
54
|
+
|
|
55
|
+
self.collection_volume = LineCollection([], animated=True, linewidths=1)
|
|
56
|
+
self.ax_volume.add_collection(self.collection_volume)
|
|
57
|
+
|
|
58
|
+
x = (self.adjust['right']-self.adjust['left']) / 2
|
|
59
|
+
self.text_watermark = Text(
|
|
60
|
+
x=x, y=0.51, text=self.watermark,
|
|
61
|
+
animated=True,
|
|
62
|
+
fontsize=20, color=self.color_tick_label, alpha=0.2,
|
|
63
|
+
horizontalalignment='center', verticalalignment='center',
|
|
64
|
+
)
|
|
65
|
+
self.figure.add_artist(self.text_watermark)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
def change_background_color(self, color):
|
|
69
|
+
self.figure.set_facecolor(color)
|
|
70
|
+
self.ax_price.set_facecolor(color)
|
|
71
|
+
self.ax_volume.set_facecolor(color)
|
|
72
|
+
legends = self.ax_legend.get_legend()
|
|
73
|
+
if legends: legends.get_frame().set_facecolor(color)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def change_tick_color(self, color):
|
|
77
|
+
for ax in (self.ax_price, self.ax_volume):
|
|
78
|
+
for i in ['top', 'bottom', 'left', 'right']: ax.spines[i].set_color(self.color_tick)
|
|
79
|
+
ax.tick_params(colors=color)
|
|
80
|
+
ax.tick_params(colors=color)
|
|
81
|
+
|
|
82
|
+
legends = self.ax_legend.get_legend()
|
|
83
|
+
if legends: legends.get_frame().set_edgecolor(color)
|
|
84
|
+
|
|
85
|
+
self.change_text_color(color)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def change_text_color(self, color):
|
|
89
|
+
self.text_watermark.set_color(color)
|
|
90
|
+
legends = self.ax_legend.get_legend()
|
|
91
|
+
if legends:
|
|
92
|
+
for i in legends.texts: i.set_color(color)
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
def change_line_color(self, color): return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_set_key = {'_x', '_left', '_right', '_volleft', '_volright', '_top', '_bottom', '_pre', '_zero', '_volymax',}
|
|
99
|
+
|
|
100
|
+
class DataMixin(CollectionMixin):
|
|
101
|
+
df: pd.DataFrame
|
|
102
|
+
|
|
103
|
+
date = 'date'
|
|
104
|
+
Open, high, low, close = ('open', 'high', 'low', 'close')
|
|
105
|
+
volume = 'volume'
|
|
106
|
+
list_ma = (5, 20, 60, 120, 240)
|
|
107
|
+
|
|
108
|
+
candle_width_half, volume_width_half = (0.24, 0.36)
|
|
109
|
+
color_up, color_down = ('#FF2400', '#1E90FF')
|
|
110
|
+
color_flat = 'k'
|
|
111
|
+
color_up_down, color_down_up = ('w', 'w')
|
|
112
|
+
|
|
113
|
+
color_volume_up, color_volume_down = ('#FF4D4D', '#5CA8F4')
|
|
114
|
+
color_volume_flat = '#A9A9A9'
|
|
115
|
+
|
|
116
|
+
set_candlecolor, set_volumecolor = (True, True)
|
|
117
|
+
|
|
118
|
+
def _generate_data(self, df: pd.DataFrame, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__):
|
|
119
|
+
self._validate_column_key()
|
|
120
|
+
|
|
121
|
+
# 오름차순 정렬
|
|
122
|
+
if sort_df: df = df.sort_values([self.date])
|
|
123
|
+
df = df.reset_index()
|
|
124
|
+
|
|
125
|
+
self.list_index = df.index.tolist()
|
|
126
|
+
self.xmin, self.xmax = (0, self.list_index[-1])
|
|
127
|
+
|
|
128
|
+
if not self.list_ma: self.list_ma = tuple()
|
|
129
|
+
else:
|
|
130
|
+
self.list_ma = sorted(self.list_ma)
|
|
131
|
+
# 가격이동평균선 계산
|
|
132
|
+
if calc_ma:
|
|
133
|
+
for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
|
|
134
|
+
else:
|
|
135
|
+
set_key = set(self.df.keys())
|
|
136
|
+
for i in self.list_ma:
|
|
137
|
+
key = f'ma{i}'
|
|
138
|
+
if key not in set_key:
|
|
139
|
+
raise KeyError(f'"{key}" column not found.\nset calc_ma=True or add "{key}" column.')
|
|
140
|
+
|
|
141
|
+
df['_x'] = df.index + 0.5
|
|
142
|
+
df['_left'] = df['_x'] - self.candle_width_half
|
|
143
|
+
df['_right'] = df['_x'] + self.candle_width_half
|
|
144
|
+
df['_volleft'] = df['_x'] - self.volume_width_half
|
|
145
|
+
df['_volright'] = df['_x'] + self.volume_width_half
|
|
146
|
+
df.loc[:, '_zero'] = 0
|
|
147
|
+
|
|
148
|
+
df['_top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
|
|
149
|
+
df['_top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
|
|
150
|
+
df['_bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
|
|
151
|
+
df['_bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
|
|
152
|
+
|
|
153
|
+
df['_pre'] = df[self.close].shift(1)
|
|
154
|
+
if self.volume: df['_volymax'] = df[self.volume] * 1.2
|
|
155
|
+
|
|
156
|
+
if not set_candlecolor:
|
|
157
|
+
keys = set(df.keys())
|
|
158
|
+
for i in ('facecolor', 'edgecolor', 'volumefacecolor',):
|
|
159
|
+
if i not in keys:
|
|
160
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_candlecolor=True.')
|
|
161
|
+
self.set_candlecolor = set_candlecolor
|
|
162
|
+
|
|
163
|
+
if not set_volumecolor:
|
|
164
|
+
keys = set(df.keys())
|
|
165
|
+
for i in ('volumefacecolor', 'volumeedgecolor',):
|
|
166
|
+
if i not in keys:
|
|
167
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_volumecolor=True.')
|
|
168
|
+
self.set_volumecolor = set_volumecolor
|
|
169
|
+
|
|
170
|
+
self.df = df
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
def _validate_column_key(self):
|
|
174
|
+
for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
|
|
175
|
+
v = getattr(self, i)
|
|
176
|
+
if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class SegmentMixin(DataMixin):
|
|
181
|
+
_visible_ma = set()
|
|
182
|
+
|
|
183
|
+
limit_candle = 800
|
|
184
|
+
limit_wick = 4_000
|
|
185
|
+
limit_volume = 200
|
|
186
|
+
|
|
187
|
+
color_priceline = 'k'
|
|
188
|
+
format_ma = '{}일선'
|
|
189
|
+
# https://matplotlib.org/stable/gallery/color/named_colors.html
|
|
190
|
+
list_macolor = ('#B22222', '#228B22', '#1E90FF', '#FF8C00', '#4B0082')
|
|
191
|
+
|
|
192
|
+
def _get_segments(self):
|
|
193
|
+
# 캔들 세그먼트
|
|
194
|
+
segment_candle = self.df[[
|
|
195
|
+
'_x', self.high,
|
|
196
|
+
'_x', '_top',
|
|
197
|
+
'_left', '_top',
|
|
198
|
+
'_left', '_bottom',
|
|
199
|
+
'_x', '_bottom',
|
|
200
|
+
'_x', self.low,
|
|
201
|
+
'_x', '_bottom',
|
|
202
|
+
'_right', '_bottom',
|
|
203
|
+
'_right', '_top',
|
|
204
|
+
'_x', '_top',
|
|
205
|
+
'_x', self.high,
|
|
206
|
+
'_x', '_top',
|
|
207
|
+
]].values
|
|
208
|
+
self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
|
|
209
|
+
|
|
210
|
+
# 심지 세그먼트
|
|
211
|
+
segment_wick = self.df[[
|
|
212
|
+
'_x', self.high,
|
|
213
|
+
'_x', self.low,
|
|
214
|
+
]].values
|
|
215
|
+
self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
|
|
216
|
+
|
|
217
|
+
# 종가 세그먼트
|
|
218
|
+
segment_priceline = segment_wick = self.df[['_x', self.close]].values
|
|
219
|
+
self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
|
|
220
|
+
|
|
221
|
+
if self.volume:
|
|
222
|
+
# 거래량 바 세그먼트
|
|
223
|
+
segment_volume = self.df[[
|
|
224
|
+
'_volleft', '_zero',
|
|
225
|
+
'_volleft', self.volume,
|
|
226
|
+
'_volright', self.volume,
|
|
227
|
+
'_volright', '_zero',
|
|
228
|
+
]].values
|
|
229
|
+
self.segment_volume = segment_volume.reshape(segment_volume.shape[0], 4, 2)
|
|
230
|
+
|
|
231
|
+
# 거래량 심지 세그먼트
|
|
232
|
+
segment_volume_wick = self.df[[
|
|
233
|
+
'_x', '_zero',
|
|
234
|
+
'_x', self.volume,
|
|
235
|
+
]].values
|
|
236
|
+
self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
|
|
237
|
+
|
|
238
|
+
self._get_ma_segment()
|
|
239
|
+
self._get_color_segment()
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
def _get_color_segment(self):
|
|
243
|
+
if self.set_candlecolor:
|
|
244
|
+
# 양봉
|
|
245
|
+
self.df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
|
|
246
|
+
if self.color_up != self.color_down:
|
|
247
|
+
# 음봉
|
|
248
|
+
self.df.loc[self.df[self.close] < self.df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
|
|
249
|
+
if self.color_up != self.color_flat and self.color_down != self.color_flat:
|
|
250
|
+
# 보합
|
|
251
|
+
self.df.loc[self.df[self.close] == self.df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
|
|
252
|
+
if self.color_up != self.color_up_down:
|
|
253
|
+
# 양봉(비우기)
|
|
254
|
+
self.df.loc[(self.df['facecolor'] == self.color_up) & (self.df[self.close] <= self.df['_pre']), 'facecolor'] = self.color_up_down
|
|
255
|
+
if self.color_down != self.color_down_up:
|
|
256
|
+
# 음봉(비우기)
|
|
257
|
+
self.df.loc[(self.df['facecolor'] == self.color_down) & (self.df['_pre'] <= self.df[self.close]), ['facecolor']] = self.color_down_up
|
|
258
|
+
|
|
259
|
+
self.facecolor_candle = self.df['facecolor'].values
|
|
260
|
+
self.edgecolor_candle = self.df['edgecolor'].values
|
|
261
|
+
|
|
262
|
+
if self.set_volumecolor:
|
|
263
|
+
# 거래량
|
|
264
|
+
self.df.loc[:, ['volumefacecolor', 'volumeedgecolor']] = (self.color_volume_up, self.color_volume_up)
|
|
265
|
+
if self.color_up != self.color_down:
|
|
266
|
+
# 전일대비 하락
|
|
267
|
+
self.df.loc[self.df[self.close] < self.df['_pre'], ['volumefacecolor', 'volumeedgecolor']] = (self.color_volume_down, self.color_volume_down)
|
|
268
|
+
if self.color_up != self.color_flat:
|
|
269
|
+
# 전일과 동일
|
|
270
|
+
self.df.loc[self.df[self.close] == self.df['_pre'], ['volumefacecolor', 'volumeedgecolor']] = (self.color_volume_flat, self.color_volume_flat)
|
|
271
|
+
|
|
272
|
+
self.facecolor_volume = self.df['volumefacecolor'].values
|
|
273
|
+
self.edgecolor_volume = self.df['volumeedgecolor'].values
|
|
274
|
+
|
|
275
|
+
# 기존 legend 제거
|
|
276
|
+
legends = self.ax_legend.get_legend()
|
|
277
|
+
if legends: legends.remove()
|
|
278
|
+
|
|
279
|
+
self._visible_ma.clear()
|
|
280
|
+
|
|
281
|
+
list_handle, list_label, list_color = ([], [], [])
|
|
282
|
+
arr = (0, 1)
|
|
283
|
+
for n, i in enumerate(self.list_ma):
|
|
284
|
+
try: c = self.list_macolor[n]
|
|
285
|
+
except: c = self.color_priceline
|
|
286
|
+
list_color.append(c)
|
|
287
|
+
|
|
288
|
+
list_handle.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
|
|
289
|
+
list_label.append(self.format_ma.format(i))
|
|
290
|
+
|
|
291
|
+
self._visible_ma.add(i)
|
|
292
|
+
self.edgecolor_ma = list(reversed(list_color))
|
|
293
|
+
|
|
294
|
+
# 가격이동평균선 legend 생성
|
|
295
|
+
if list_handle:
|
|
296
|
+
legends = self.ax_legend.legend(
|
|
297
|
+
list_handle, list_label, loc='lower left', ncol=10,
|
|
298
|
+
facecolor=self.color_background, edgecolor=self.color_tick,
|
|
299
|
+
labelcolor=self.color_tick_label,
|
|
300
|
+
)
|
|
301
|
+
for i in legends.legend_handles: i.set_picker(5)
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
def _get_ma_segment(self):
|
|
305
|
+
if not self.list_ma: return
|
|
306
|
+
|
|
307
|
+
# 주가 차트 가격이동평균선
|
|
308
|
+
key_ma = []
|
|
309
|
+
for i in reversed(self.list_ma):
|
|
310
|
+
key_ma.append('_x')
|
|
311
|
+
key_ma.append(f'ma{i}')
|
|
312
|
+
segment_ma = self.df[key_ma].values
|
|
313
|
+
self.segment_ma = segment_ma.reshape(segment_ma.shape[0], len(self.list_ma), 2).swapaxes(0, 1)
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
def _set_segments(self, index_start, index_end, simpler, set_ma):
|
|
317
|
+
indsub = index_end - index_start
|
|
318
|
+
if index_start < 0: index_start = 0
|
|
319
|
+
if index_end < 1: index_end = 1
|
|
320
|
+
|
|
321
|
+
index_end += 1
|
|
322
|
+
if indsub < self.limit_candle:
|
|
323
|
+
self._set_candle_segments(index_start, index_end)
|
|
324
|
+
elif indsub < self.limit_wick:
|
|
325
|
+
self._set_wick_segments(index_start, index_end, simpler)
|
|
326
|
+
else:
|
|
327
|
+
self._set_line_segments(index_start, index_end, simpler, set_ma)
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
331
|
+
self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
|
|
332
|
+
self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
|
|
333
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
334
|
+
|
|
335
|
+
if self.volume:
|
|
336
|
+
self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
|
|
337
|
+
self.collection_volume.set_linewidth(0.7)
|
|
338
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
339
|
+
self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
|
|
340
|
+
|
|
341
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
342
|
+
self.collection_ma.set_edgecolor(self.edgecolor_ma)
|
|
343
|
+
return
|
|
344
|
+
|
|
345
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
346
|
+
self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
|
|
347
|
+
self.collection_candle.set_facecolor([])
|
|
348
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
349
|
+
|
|
350
|
+
if self.volume:
|
|
351
|
+
seg_volume = self.segment_volume_wick[index_start:index_end]
|
|
352
|
+
seg_facecolor_volume = self.facecolor_volume[index_start:index_end]
|
|
353
|
+
seg_edgecolor_volume = self.edgecolor_volume[index_start:index_end]
|
|
354
|
+
if simpler:
|
|
355
|
+
values = seg_volume[:, 1, 1]
|
|
356
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
357
|
+
seg_volume = seg_volume[top_index]
|
|
358
|
+
seg_facecolor_volume = seg_facecolor_volume[top_index]
|
|
359
|
+
seg_edgecolor_volume = seg_edgecolor_volume[top_index]
|
|
360
|
+
self.collection_volume.set_segments(seg_volume)
|
|
361
|
+
self.collection_volume.set_linewidth(1.3)
|
|
362
|
+
self.collection_volume.set_facecolor(seg_facecolor_volume)
|
|
363
|
+
self.collection_volume.set_edgecolor(seg_edgecolor_volume)
|
|
364
|
+
|
|
365
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
366
|
+
self.collection_ma.set_edgecolor(self.edgecolor_ma)
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
370
|
+
self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
|
|
371
|
+
self.collection_candle.set_facecolor([])
|
|
372
|
+
self.collection_candle.set_edgecolor(self.color_priceline)
|
|
373
|
+
|
|
374
|
+
if self.volume:
|
|
375
|
+
seg_volume = self.segment_volume_wick[index_start:index_end]
|
|
376
|
+
seg_facecolor_volume = self.facecolor_volume[index_start:index_end]
|
|
377
|
+
seg_edgecolor_volume = self.edgecolor_volume[index_start:index_end]
|
|
378
|
+
if simpler:
|
|
379
|
+
values = seg_volume[:, 1, 1]
|
|
380
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
381
|
+
seg_volume = seg_volume[top_index]
|
|
382
|
+
seg_facecolor_volume = seg_facecolor_volume[top_index]
|
|
383
|
+
seg_edgecolor_volume = seg_edgecolor_volume[top_index]
|
|
384
|
+
self.collection_volume.set_segments(seg_volume)
|
|
385
|
+
self.collection_volume.set_linewidth(1.3)
|
|
386
|
+
self.collection_volume.set_facecolor(seg_facecolor_volume)
|
|
387
|
+
self.collection_volume.set_edgecolor(seg_edgecolor_volume)
|
|
388
|
+
|
|
389
|
+
if not set_ma: self.collection_ma.set_segments([])
|
|
390
|
+
else:
|
|
391
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
392
|
+
self.collection_ma.set_edgecolor(self.edgecolor_ma)
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
class EventMixin(SegmentMixin):
|
|
397
|
+
def __init__(self, *args, **kwargs):
|
|
398
|
+
super().__init__(*args, **kwargs)
|
|
399
|
+
|
|
400
|
+
self._connect_event()
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
def _connect_event(self):
|
|
404
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
def _on_pick(self, e):
|
|
408
|
+
self._pick_ma_action(e)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
def _pick_ma_action(self, e: PickEvent):
|
|
412
|
+
handle = e.artist
|
|
413
|
+
if handle.get_alpha() == 0.2:
|
|
414
|
+
visible = True
|
|
415
|
+
handle.set_alpha(1.0)
|
|
416
|
+
else:
|
|
417
|
+
visible = False
|
|
418
|
+
handle.set_alpha(0.2)
|
|
419
|
+
|
|
420
|
+
n = int(handle.get_label())
|
|
421
|
+
if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
|
|
422
|
+
else: self._visible_ma = {i for i in self._visible_ma if i != n}
|
|
423
|
+
|
|
424
|
+
alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
|
|
425
|
+
self.collection_ma.set_alpha(alphas)
|
|
426
|
+
|
|
427
|
+
self._draw()
|
|
428
|
+
return
|
|
429
|
+
|
|
430
|
+
def _draw(self):
|
|
431
|
+
self.figure.canvas.draw()
|
|
432
|
+
return
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
class DrawMixin(EventMixin):
|
|
436
|
+
candle_on_ma = True
|
|
437
|
+
|
|
438
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
|
|
439
|
+
self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
|
|
440
|
+
self._get_segments()
|
|
441
|
+
|
|
442
|
+
vmin, vmax = self.get_default_lim()
|
|
443
|
+
self._set_lim(vmin, vmax)
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
def _connect_event(self):
|
|
447
|
+
super()._connect_event()
|
|
448
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
def _on_draw(self, e):
|
|
452
|
+
self._draw_artist()
|
|
453
|
+
self._blit()
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
def _draw_artist(self):
|
|
457
|
+
renderer = self.figure.canvas.renderer
|
|
458
|
+
|
|
459
|
+
self.ax_price.xaxis.draw(renderer)
|
|
460
|
+
self.ax_price.yaxis.draw(renderer)
|
|
461
|
+
|
|
462
|
+
if self.candle_on_ma:
|
|
463
|
+
self.collection_ma.draw(renderer)
|
|
464
|
+
self.collection_candle.draw(renderer)
|
|
465
|
+
else:
|
|
466
|
+
self.collection_candle.draw(renderer)
|
|
467
|
+
self.collection_ma.draw(renderer)
|
|
468
|
+
|
|
469
|
+
if self.watermark:
|
|
470
|
+
self.text_watermark.set_text(self.watermark)
|
|
471
|
+
self.text_watermark.draw(renderer)
|
|
472
|
+
|
|
473
|
+
self.ax_volume.xaxis.draw(renderer)
|
|
474
|
+
self.ax_volume.yaxis.draw(renderer)
|
|
475
|
+
|
|
476
|
+
self.collection_volume.draw(renderer)
|
|
477
|
+
return
|
|
478
|
+
|
|
479
|
+
def _blit(self):
|
|
480
|
+
self.figure.canvas.blit()
|
|
481
|
+
return
|
|
482
|
+
|
|
483
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
484
|
+
self.vxmin, self.vxmax = (xmin, xmax + 1)
|
|
485
|
+
if xmin < 0: xmin = 0
|
|
486
|
+
if xmax < 0: xmax = 0
|
|
487
|
+
if xmin == xmax: xmax += 1
|
|
488
|
+
|
|
489
|
+
ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
|
|
490
|
+
yspace = (ymax - ymin) / 15
|
|
491
|
+
# 주가 차트 ymin, ymax
|
|
492
|
+
self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
|
|
493
|
+
|
|
494
|
+
# 거래량 차트 ymax
|
|
495
|
+
self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
|
|
496
|
+
|
|
497
|
+
self._set_segments(xmin, xmax, simpler, set_ma)
|
|
498
|
+
self._change_lim(self.vxmin, self.vxmax)
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
def _change_lim(self, xmin, xmax):
|
|
502
|
+
# 주가 차트 xlim
|
|
503
|
+
self.ax_price.set_xlim(xmin, xmax)
|
|
504
|
+
# 거래량 차트 xlim
|
|
505
|
+
self.ax_volume.set_xlim(xmin, xmax)
|
|
506
|
+
|
|
507
|
+
# 주가 차트 ylim
|
|
508
|
+
self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
|
|
509
|
+
# 거래량 차트 ylim
|
|
510
|
+
self.ax_volume.set_ylim(0, self.volume_ymax)
|
|
511
|
+
return
|
|
512
|
+
|
|
513
|
+
def get_default_lim(self):
|
|
514
|
+
return (0, self.list_index[-1])
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class BackgroundMixin(DrawMixin):
|
|
518
|
+
background = None
|
|
519
|
+
|
|
520
|
+
_creating_background = False
|
|
521
|
+
|
|
522
|
+
def _connect_event(self):
|
|
523
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
524
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
525
|
+
return
|
|
526
|
+
|
|
527
|
+
def _create_background(self):
|
|
528
|
+
if self._creating_background: return
|
|
529
|
+
|
|
530
|
+
self._creating_background = True
|
|
531
|
+
self._copy_bbox()
|
|
532
|
+
self._creating_background = False
|
|
533
|
+
return
|
|
534
|
+
|
|
535
|
+
def _copy_bbox(self):
|
|
536
|
+
self._draw_artist()
|
|
537
|
+
self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
538
|
+
return
|
|
539
|
+
|
|
540
|
+
def _on_draw(self, e):
|
|
541
|
+
self.background = None
|
|
542
|
+
self._restore_region()
|
|
543
|
+
return
|
|
544
|
+
|
|
545
|
+
def _restore_region(self):
|
|
546
|
+
if not self.background: self._create_background()
|
|
547
|
+
|
|
548
|
+
self.figure.canvas.renderer.restore_region(self.background)
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
class BaseMixin(BackgroundMixin):
|
|
553
|
+
pass
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
class Chart(BaseMixin, Mixin):
|
|
557
|
+
def _add_collection(self):
|
|
558
|
+
super()._add_collection()
|
|
559
|
+
return self.add_artist()
|
|
560
|
+
|
|
561
|
+
def _draw_artist(self):
|
|
562
|
+
super()._draw_artist()
|
|
563
|
+
return self.draw_artist()
|
|
564
|
+
|
|
565
|
+
def _get_segments(self):
|
|
566
|
+
self.generate_data()
|
|
567
|
+
return super()._get_segments()
|
|
568
|
+
|
|
569
|
+
def _on_draw(self, e):
|
|
570
|
+
super()._on_draw(e)
|
|
571
|
+
return self.on_draw(e)
|
|
572
|
+
|
|
573
|
+
def _on_pick(self, e):
|
|
574
|
+
self.on_pick(e)
|
|
575
|
+
return super()._on_pick(e)
|
|
576
|
+
|
|
577
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
578
|
+
super()._set_candle_segments(index_start, index_end)
|
|
579
|
+
self.set_segment(index_start, index_end)
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
583
|
+
super()._set_wick_segments(index_start, index_end, simpler)
|
|
584
|
+
self.set_segment(index_start, index_end, simpler)
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
588
|
+
super()._set_line_segments(index_start, index_end, simpler, set_ma)
|
|
589
|
+
self.set_segment(index_start, index_end, simpler, set_ma)
|
|
590
|
+
return
|
|
591
|
+
|