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.
Files changed (50) hide show
  1. seolpyo_mplchart/__init__.py +164 -99
  2. seolpyo_mplchart/_base.py +117 -0
  3. seolpyo_mplchart/_chart/__init__.py +137 -0
  4. seolpyo_mplchart/_chart/_base.py +217 -0
  5. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  6. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  7. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  8. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  9. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  10. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  11. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  12. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  13. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  14. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  15. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  16. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  17. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  18. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  19. seolpyo_mplchart/_chart/test.py +121 -0
  20. seolpyo_mplchart/_config/__init__.py +3 -0
  21. seolpyo_mplchart/_config/ax.py +28 -0
  22. seolpyo_mplchart/_config/candle.py +30 -0
  23. seolpyo_mplchart/_config/config.py +21 -0
  24. seolpyo_mplchart/_config/cursor.py +49 -0
  25. seolpyo_mplchart/_config/figure.py +41 -0
  26. seolpyo_mplchart/_config/format.py +51 -0
  27. seolpyo_mplchart/_config/ma.py +15 -0
  28. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  29. seolpyo_mplchart/_config/slider/config.py +24 -0
  30. seolpyo_mplchart/_config/slider/figure.py +20 -0
  31. seolpyo_mplchart/_config/slider/nav.py +9 -0
  32. seolpyo_mplchart/_config/unit.py +19 -0
  33. seolpyo_mplchart/_config/utils.py +67 -0
  34. seolpyo_mplchart/_config/volume.py +26 -0
  35. seolpyo_mplchart/_cursor.py +559 -0
  36. seolpyo_mplchart/_draw.py +634 -0
  37. seolpyo_mplchart/_slider.py +634 -0
  38. seolpyo_mplchart/base.py +70 -67
  39. seolpyo_mplchart/cursor.py +308 -271
  40. seolpyo_mplchart/draw.py +449 -237
  41. seolpyo_mplchart/slider.py +451 -396
  42. seolpyo_mplchart/test.py +173 -24
  43. seolpyo_mplchart/utils.py +15 -4
  44. seolpyo_mplchart/xl_to_dict.py +47 -0
  45. seolpyo_mplchart-2.0.0.3.dist-info/METADATA +710 -0
  46. seolpyo_mplchart-2.0.0.3.dist-info/RECORD +50 -0
  47. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
  48. seolpyo_mplchart-0.1.3.1.dist-info/METADATA +0 -49
  49. seolpyo_mplchart-0.1.3.1.dist-info/RECORD +0 -13
  50. {seolpyo_mplchart-0.1.3.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
seolpyo_mplchart/draw.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from matplotlib.backend_bases import PickEvent
2
2
  from matplotlib.collections import LineCollection
3
3
  from matplotlib.lines import Line2D
4
+ from matplotlib.text import Text
4
5
  import numpy as np
5
6
  import pandas as pd
6
7
 
@@ -9,208 +10,399 @@ from .base import Base
9
10
 
10
11
 
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
+
12
25
  def on_draw(self, e):
13
- "This function works if draw event active."
26
+ "If draw event active, This method work."
14
27
  return
28
+
15
29
  def on_pick(self, e):
16
- "This function works if pick event active."
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)
17
66
  return
18
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
19
87
 
20
- _set_key = {'zero', 'x', 'left', 'right', 'top', 'bottom',}
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
21
94
 
22
- class DataMixin(Base):
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):
23
101
  df: pd.DataFrame
102
+
24
103
  date = 'date'
25
104
  Open, high, low, close = ('open', 'high', 'low', 'close')
26
105
  volume = 'volume'
27
-
28
- _visible_ma = set()
29
- label_ma = '{}일선'
30
106
  list_ma = (5, 20, 60, 120, 240)
31
- # https://matplotlib.org/stable/gallery/color/named_colors.html
32
- list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
33
107
 
34
- color_up, color_down = ('#fe3032', '#0095ff')
108
+ candle_width_half, volume_width_half = (0.24, 0.36)
109
+ color_up, color_down = ('#FF2400', '#1E90FF')
35
110
  color_flat = 'k'
36
111
  color_up_down, color_down_up = ('w', 'w')
37
- colors_volume = '#1f77b4'
38
112
 
39
- candlewidth_half, volumewidth_half = (0.3, 0.36)
113
+ color_volume_up, color_volume_down = ('#FF4D4D', '#5CA8F4')
114
+ color_volume_flat = '#A9A9A9'
40
115
 
41
- def _generate_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, **_):
42
- for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
43
- k: str = getattr(self, i)
44
- if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
45
- if i != 'date':
46
- dtype = df[k].dtype
47
- if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)):
48
- raise TypeError(f'column dtype must be one of "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
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()
49
120
 
50
- # DataFrame 정렬
51
- if sort_df:
52
- df = df.sort_values([self.date]).reset_index()
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])
53
127
 
54
128
  if not self.list_ma: self.list_ma = tuple()
55
- if calc_ma:
56
- for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
57
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:
58
164
  keys = set(df.keys())
59
- for i in self.list_ma:
60
- if f'ma{i}' not in keys:
61
- raise Exception(f'"ma{i}" column not in DataFrame.\nadd column or set calc_ma=True.')
62
-
63
- df['x'] = df.index + 0.5
64
- df['left'] = df['x'] - self.candlewidth_half
65
- df['right'] = df['x'] + self.candlewidth_half
66
- df['vleft'] = df['x'] - self.volumewidth_half
67
- df['vright'] = df['x'] + self.volumewidth_half
68
-
69
- df['top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
70
- df['top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
71
- df['bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
72
- df['bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
73
-
74
- # 양봉
75
- df.loc[:, ['zero', 'facecolor', 'edgecolor']] = (0, self.color_up, self.color_up)
76
- if self.color_up != self.color_down:
77
- # 음봉
78
- df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
79
- if self.color_up != self.color_flat:
80
- # 보합
81
- df.loc[df[self.close] == df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
82
- if self.color_up != self.color_up_down:
83
- # 양봉(비우기)
84
- df.loc[(df['facecolor'] == self.color_up) & (df[self.close] < df[self.close].shift(1)), 'facecolor'] = self.color_up_down
85
- if self.color_down != self.color_down_up:
86
- # 음봉(비우기)
87
- df.loc[(df['facecolor'] == self.color_down) & (df[self.close].shift(1) < df[self.close]), ['facecolor']] = self.color_down_up
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
88
169
 
89
170
  self.df = df
90
171
  return
91
172
 
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()
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}')
102
177
  return
103
178
 
104
- def _connect_event(self):
105
- self.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
106
- return
107
179
 
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 _set_collection(self):
126
- candleseg = self.df[[
127
- 'x', self.high,
128
- 'x', 'top',
129
- 'left', 'top',
130
- 'left', 'bottom',
131
- 'x', 'bottom',
132
- 'x', self.low,
133
- 'x', 'bottom',
134
- 'right', 'bottom',
135
- 'right', 'top',
136
- 'x', 'top',
137
- ]].values
138
- candleseg = candleseg.reshape(candleseg.shape[0], 10, 2)
180
+ class SegmentMixin(DataMixin):
181
+ _visible_ma = set()
139
182
 
140
- self.candlecollection.set_segments(candleseg)
141
- self.candlecollection.set_facecolor(self.df['facecolor'].values)
142
- self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
183
+ limit_candle = 800
184
+ limit_wick = 4_000
185
+ limit_volume = 800
143
186
 
144
- volseg = self.df[[
145
- 'left', 'zero',
146
- 'left', self.volume,
147
- 'right', self.volume,
148
- 'right', 'zero',
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',
149
207
  ]].values
150
- volseg = volseg.reshape(volseg.shape[0], 4, 2)
151
-
152
- self.volumecollection.set_segments(volseg)
153
-
154
- self._set_macollection()
208
+ self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
155
209
 
156
- # 가격이동평균선
157
- maseg = reversed(self._masegment.values())
158
- colors, widths = ([], [])
159
- for i in reversed(self._macolors.values()): (colors.append(i), widths.append(1))
160
- self.macollection.set_segments(maseg)
161
- self.macollection.set_edgecolor(colors)
162
-
163
- # 슬라이더 선형차트
164
- keys = []
165
- for i in reversed(self.list_ma):
166
- keys.append('x')
167
- keys.append(f'ma{i}')
168
- sliderseg = self.df[keys + ['x', self.close]].values
169
- sliderseg = sliderseg.reshape(sliderseg.shape[0], self.list_ma.__len__()+1, 2).swapaxes(0, 1)
170
- (colors.append(self.color_sliderline), widths.append(1.8))
171
- self.slidercollection.set_segments(sliderseg)
172
- self.slidercollection.set_edgecolor(colors)
173
- self.slidercollection.set_linewidth(widths)
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()
174
240
  return
175
241
 
176
- def _set_macollection(self):
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
+
177
275
  # 기존 legend 제거
178
276
  legends = self.ax_legend.get_legend()
179
277
  if legends: legends.remove()
180
278
 
181
- self._masegment.clear(), self._macolors.clear()
182
- handles, labels = ([], [])
183
279
  self._visible_ma.clear()
280
+
281
+ list_handle, list_label, list_color = ([], [], [])
282
+ arr = (0, 1)
184
283
  for n, i in enumerate(self.list_ma):
185
284
  try: c = self.list_macolor[n]
186
- except: c = self.color_sliderline
187
- self._macolors[i] = c
188
- # seg = self.df['x', f'ma{i}'].values
189
- seg = self.df.loc[self.df[f'ma{i}'] != np.nan, ['x', f'ma{i}']].values
190
- # print(f'{seg[:5]=}')
191
- self._masegment[i] = seg
285
+ except: c = self.color_priceline
286
+ list_color.append(c)
192
287
 
193
- handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
194
- labels.append(self.label_ma.format(i))
288
+ list_handle.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
289
+ list_label.append(self.format_ma.format(i))
195
290
 
196
291
  self._visible_ma.add(i)
292
+ self.edgecolor_ma = list(reversed(list_color))
197
293
 
198
294
  # 가격이동평균선 legend 생성
199
- if handles:
200
- legends = self.ax_legend.legend(handles, labels, loc='lower left', ncol=10)
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 = self.segment_volume_wick[index_start:index_end]
352
+ if simpler:
353
+ values = seg[:, 1, 1]
354
+ top_index = np.argsort(-values)[:self.limit_volume]
355
+ seg = seg[top_index]
356
+ self.collection_volume.set_segments(seg)
357
+ self.collection_volume.set_linewidth(1.3)
358
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
359
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
360
+
361
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
362
+ self.collection_ma.set_edgecolor(self.edgecolor_ma)
363
+ return
364
+
365
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
366
+ self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
367
+ self.collection_candle.set_facecolor([])
368
+ self.collection_candle.set_edgecolor(self.color_priceline)
369
+
370
+ if self.volume:
371
+ seg = self.segment_volume_wick[index_start:index_end]
372
+ if simpler:
373
+ values = seg[:, 1, 1]
374
+ top_index = np.argsort(-values)[:self.limit_volume]
375
+ seg = seg[top_index]
376
+ self.collection_volume.set_segments(seg)
377
+ self.collection_volume.set_linewidth(1.3)
378
+ self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
379
+ self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
380
+
381
+ if not set_ma: self.collection_ma.set_segments([])
382
+ else:
383
+ self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
384
+ self.collection_ma.set_edgecolor(self.edgecolor_ma)
385
+ return
386
+
201
387
 
202
- for i in legends.legend_handles:
203
- i.set_picker(5)
388
+ class EventMixin(SegmentMixin):
389
+ def __init__(self, *args, **kwargs):
390
+ super().__init__(*args, **kwargs)
391
+
392
+ self._connect_event()
393
+ return
394
+
395
+ def _connect_event(self):
396
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
204
397
  return
205
398
 
206
399
  def _on_pick(self, e):
207
400
  self._pick_ma_action(e)
208
-
209
- return self._draw()
401
+ return
210
402
 
211
403
  def _pick_ma_action(self, e: PickEvent):
212
404
  handle = e.artist
213
- if e.artist.get_alpha() == 0.2:
405
+ if handle.get_alpha() == 0.2:
214
406
  visible = True
215
407
  handle.set_alpha(1.0)
216
408
  else:
@@ -218,130 +410,154 @@ class CollectionMixin(DataMixin):
218
410
  handle.set_alpha(0.2)
219
411
 
220
412
  n = int(handle.get_label())
221
-
222
413
  if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
223
414
  else: self._visible_ma = {i for i in self._visible_ma if i != n}
224
415
 
225
- self.macollection.set_segments([self._masegment[i] for i in reversed(self._masegment) if i in self._visible_ma])
226
- colors = [self._macolors[i] for i in reversed(self._macolors) if i in self._visible_ma]
227
- self.macollection.set_colors(colors)
416
+ alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
417
+ self.collection_ma.set_alpha(alphas)
418
+
419
+ self._draw()
228
420
  return
229
421
 
230
422
  def _draw(self):
231
- if self.fig.canvas is not self.canvas:
232
- self.canvas = self.fig.canvas
233
- self.canvas.draw()
423
+ self.figure.canvas.draw()
234
424
  return
235
425
 
236
426
 
237
- class BackgroundMixin(CollectionMixin):
238
- background = None
427
+ class DrawMixin(EventMixin):
239
428
  candle_on_ma = True
240
429
 
241
- _creating_background = False
430
+ def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
431
+ self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
432
+ self._get_segments()
242
433
 
243
- def __init__(self, *args, **kwargs):
244
- super().__init__(*args, **kwargs)
434
+ vmin, vmax = self.get_default_lim()
435
+ self._set_lim(vmin, vmax)
245
436
  return
246
437
 
247
438
  def _connect_event(self):
248
439
  super()._connect_event()
249
- self.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
250
- return
251
-
252
- def _create_background(self):
253
- if self._creating_background: return
254
-
255
- if self.fig.canvas is not self.canvas:
256
- self.canvas = self.fig.canvas
257
-
258
- self._creating_background = True
259
- self._copy_bbox()
260
- self._creating_background = False
440
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
261
441
  return
262
442
 
263
- def _copy_bbox(self):
443
+ def _on_draw(self, e):
264
444
  self._draw_artist()
265
- self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
445
+ self._blit()
266
446
  return
267
447
 
268
448
  def _draw_artist(self):
269
- renderer = self.canvas.renderer
270
-
271
- self.ax_slider.xaxis.draw(renderer)
272
- self.ax_slider.yaxis.draw(renderer)
273
-
274
- self.slidercollection.draw(renderer)
449
+ renderer = self.figure.canvas.renderer
275
450
 
276
451
  self.ax_price.xaxis.draw(renderer)
277
452
  self.ax_price.yaxis.draw(renderer)
278
453
 
279
454
  if self.candle_on_ma:
280
- self.macollection.draw(renderer)
281
- self.candlecollection.draw(renderer)
455
+ self.collection_ma.draw(renderer)
456
+ self.collection_candle.draw(renderer)
282
457
  else:
283
- self.candlecollection.draw(renderer)
284
- self.macollection.draw(renderer)
458
+ self.collection_candle.draw(renderer)
459
+ self.collection_ma.draw(renderer)
460
+
461
+ if self.watermark:
462
+ self.text_watermark.set_text(self.watermark)
463
+ self.text_watermark.draw(renderer)
285
464
 
286
465
  self.ax_volume.xaxis.draw(renderer)
287
466
  self.ax_volume.yaxis.draw(renderer)
288
467
 
289
- self.volumecollection.draw(renderer)
468
+ self.collection_volume.draw(renderer)
290
469
  return
291
470
 
292
- def _on_draw(self, e):
293
- self.background = None
294
- self._restore_region()
471
+ def _blit(self):
472
+ self.figure.canvas.blit()
295
473
  return
296
474
 
297
- def _restore_region(self):
298
- if not self.background: self._create_background()
475
+ def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
476
+ self.vxmin, self.vxmax = (xmin, xmax + 1)
477
+ if xmin < 0: xmin = 0
478
+ if xmax < 0: xmax = 0
479
+ if xmin == xmax: xmax += 1
480
+
481
+ ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
482
+ yspace = (ymax - ymin) / 15
483
+ # 주가 차트 ymin, ymax
484
+ self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
485
+
486
+ # 거래량 차트 ymax
487
+ self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
299
488
 
300
- self.canvas.renderer.restore_region(self.background)
489
+ self._set_segments(xmin, xmax, simpler, set_ma)
490
+ self._change_lim(self.vxmin, self.vxmax)
301
491
  return
302
492
 
303
- class DrawMixin(BackgroundMixin):
304
- def set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
305
- self._set_data(df, sort_df, calc_ma, change_lim, **kwargs)
306
- return self.df
493
+ def _change_lim(self, xmin, xmax):
494
+ # 주가 차트 xlim
495
+ self.ax_price.set_xlim(xmin, xmax)
496
+ # 거래량 차트 xlim
497
+ self.ax_volume.set_xlim(xmin, xmax)
307
498
 
308
- def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
309
- self._generate_data(df, sort_df, calc_ma, **kwargs)
310
- self._set_collection()
311
- self._draw_collection(change_lim)
499
+ # 주가 차트 ylim
500
+ self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
501
+ # 거래량 차트 ylim
502
+ self.ax_volume.set_ylim(0, self.volume_ymax)
312
503
  return
313
504
 
314
- def _draw_collection(self, change_lim=True):
315
- xmax = self.df['x'].values[-1] + 1
505
+ def get_default_lim(self):
506
+ return (0, self.list_index[-1])
507
+
508
+
509
+ class BackgroundMixin(DrawMixin):
510
+ background = None
511
+
512
+ _creating_background = False
316
513
 
317
- xspace = xmax / 40
318
- self.xmin, self.xmax = (-xspace, xmax+xspace)
319
- # 슬라이더 xlim
320
- self.ax_slider.set_xlim(self.xmin, self.xmax)
321
- if change_lim:
322
- # 주가 xlim
323
- self.ax_price.set_xlim(self.xmin, self.xmax)
324
- # 거래량 xlim
325
- self.ax_volume.set_xlim(self.xmin, self.xmax)
514
+ def _connect_event(self):
515
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
516
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
517
+ return
326
518
 
327
- ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
328
- ysub = (ymax - ymin) / 15
519
+ def _create_background(self):
520
+ if self._creating_background: return
521
+
522
+ self._creating_background = True
523
+ self._copy_bbox()
524
+ self._creating_background = False
525
+ return
526
+
527
+ def _copy_bbox(self):
528
+ self._draw_artist()
529
+ self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
530
+ return
329
531
 
330
- # 슬라이더 ylim
331
- self._slider_ymin, self._slider_ymax = (ymin-ysub, ymax+ysub)
332
- self.ax_slider.set_ylim(self._slider_ymin, self._slider_ymax)
532
+ def _on_draw(self, e):
533
+ self.background = None
534
+ self._restore_region()
535
+ return
333
536
 
334
- # 주가 ylim
335
- self._price_ymin, self._price_ymax = (ymin-ysub, ymax+ysub)
336
- if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
537
+ def _restore_region(self):
538
+ if not self.background: self._create_background()
337
539
 
338
- # 거래량 ylim
339
- self._vol_ymax = self.df[self.volume].max() * 1.2
340
- if change_lim: self.ax_volume.set_ylim(0, self._vol_ymax)
540
+ self.figure.canvas.renderer.restore_region(self.background)
341
541
  return
342
542
 
343
543
 
344
- class Chart(DrawMixin, Mixin):
544
+ class BaseMixin(BackgroundMixin):
545
+ pass
546
+
547
+
548
+ class Chart(BaseMixin, Mixin):
549
+ def _add_collection(self):
550
+ super()._add_collection()
551
+ return self.add_artist()
552
+
553
+ def _draw_artist(self):
554
+ super()._draw_artist()
555
+ return self.draw_artist()
556
+
557
+ def _get_segments(self):
558
+ self.generate_data()
559
+ return super()._get_segments()
560
+
345
561
  def _on_draw(self, e):
346
562
  super()._on_draw(e)
347
563
  return self.on_draw(e)
@@ -350,22 +566,18 @@ class Chart(DrawMixin, Mixin):
350
566
  self.on_pick(e)
351
567
  return super()._on_pick(e)
352
568
 
569
+ def _set_candle_segments(self, index_start, index_end):
570
+ super()._set_candle_segments(index_start, index_end)
571
+ self.set_segment(index_start, index_end)
572
+ return
353
573
 
354
- if __name__ == '__main__':
355
- import json
356
- from time import time
357
-
358
- import matplotlib.pyplot as plt
359
- from pathlib import Path
574
+ def _set_wick_segments(self, index_start, index_end, simpler=False):
575
+ super()._set_wick_segments(index_start, index_end, simpler)
576
+ self.set_segment(index_start, index_end, simpler)
577
+ return
360
578
 
361
- with open(Path(__file__).parent / 'data/samsung.txt', 'r', encoding='utf-8') as txt:
362
- data = json.load(txt)
363
- print(f'{len(data)=}')
364
- # data = data[:200]
365
- df = pd.DataFrame(data)
579
+ def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
580
+ super()._set_line_segments(index_start, index_end, simpler, set_ma)
581
+ self.set_segment(index_start, index_end, simpler, set_ma)
582
+ return
366
583
 
367
- t = time()
368
- DrawMixin().set_data(df)
369
- t2 = time() - t
370
- print(f'{t2=}')
371
- plt.show()