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