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
@@ -0,0 +1,187 @@
1
+ from fractions import Fraction
2
+
3
+ from matplotlib.backend_bases import MouseEvent
4
+
5
+ from ._cursor import BaseMixin as Base
6
+ from ..._config.utils import float_to_str
7
+
8
+
9
+ class InfoMixin(Base):
10
+ fraction = False
11
+
12
+ def _set_data(self, df, *args, **kwargs):
13
+ super()._set_data(df, *args, **kwargs)
14
+
15
+ self._set_length_text()
16
+ return
17
+
18
+ def _set_length_text(self):
19
+ func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit, word=self.CONFIG.UNIT.price))
20
+ self._length_text = self.df['high'].apply(func).max()
21
+
22
+ if self.key_volume:
23
+ func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit_volume, word=self.CONFIG.UNIT.volume))
24
+ lenth_volume = self.df['volume'].apply(func).max()
25
+ # print(f'{self._length_text=}')
26
+ # print(f'{lenth_volume=}')
27
+ if self._length_text < lenth_volume:
28
+ self._length_text = lenth_volume
29
+ return
30
+
31
+ def _draw_box_artist(self, e):
32
+ super()._draw_box_artist(e)
33
+
34
+ if self.intx is not None:
35
+ if self.in_candle:
36
+ self._draw_candle_info_artist(e)
37
+ elif self.key_volume and self.in_volumebar:
38
+ self._draw_volume_info_artist(e)
39
+ return
40
+
41
+ def _draw_candle_info_artist(self, e: MouseEvent):
42
+ # 캔들 정보
43
+ self.artist_info_candle.set_text(self._get_info(self.intx))
44
+
45
+ # 정보 텍스트를 중앙에 몰리게 설정할 수도 있지만,
46
+ # 그런 경우 차트를 가리므로 좌우 끝단에 위치하도록 설정
47
+ if self.vmiddle < e.xdata:
48
+ self.artist_info_candle.set_x(self.v0)
49
+ else:
50
+ # self.artist_info_candle.set_x(self.vmax - self.x_distance)
51
+ # self.artist_info_candle.set_horizontalalignment('right')
52
+ # 텍스트박스 크기 가져오기
53
+ bbox = self.artist_info_candle.get_window_extent()\
54
+ .transformed(self.ax_price.transData.inverted())
55
+ width = bbox.x1 - bbox.x0
56
+ self.artist_info_candle.set_x(self.v1 - width)
57
+
58
+ self.artist_info_candle.draw(self.figure.canvas.renderer)
59
+ return
60
+
61
+ def _draw_volume_info_artist(self, e: MouseEvent):
62
+ # 거래량 정보
63
+ self.artist_info_volume.set_text(self._get_info(self.intx, is_price=False))
64
+
65
+ if self.vmiddle < e.xdata:
66
+ self.artist_info_volume.set_x(self.v0)
67
+ else:
68
+ # self.artist_info_volume.set_x(self.vmax - self.x_distance)
69
+ # self.artist_info_volume.set_horizontalalignment('right')
70
+ # 텍스트박스 크기 가져오기
71
+ bbox = self.artist_info_volume.get_window_extent()\
72
+ .transformed(self.ax_price.transData.inverted())
73
+ width = bbox.x1 - bbox.x0
74
+ self.artist_info_volume.set_x(self.v1 - width)
75
+
76
+ self.artist_info_volume.draw(self.figure.canvas.renderer)
77
+ return
78
+
79
+ def get_info_kwargs(self, is_price: bool, **kwargs)-> dict:
80
+ """
81
+ get text info kwargs
82
+
83
+ Args:
84
+ is_price (bool): is price chart info or not
85
+
86
+ Returns:
87
+ dict[str, any]: text info kwargs
88
+ """
89
+ return kwargs
90
+
91
+ def _get_info(self, index, is_price=True):
92
+ # print(f'{self._length_text=}')
93
+ dt = self.df.loc[index, 'date']
94
+ if not self.key_volume:
95
+ v, vr = ('-', '-')
96
+ else:
97
+ v = self.df.loc[index, 'volume']
98
+ # print(f'{self.CONFIG.UNIT.digit_volume=}')
99
+ v = float_to_str(v, digit=self.CONFIG.UNIT.digit_volume)
100
+ # if not v % 1:
101
+ # v = int(v)
102
+ vr = self.df.loc[index, 'rate_volume']
103
+ vr = f'{vr:+06,.2f}'
104
+
105
+ if is_price:
106
+ o, h, l, c = (self.df.loc[index, 'open'], self.df.loc[index, 'high'], self.df.loc[index, 'low'], self.df.loc[index, 'close'])
107
+ rate, compare = (self.df.loc[index, 'rate'], self.df.loc[index, 'compare'])
108
+ r = f'{rate:+06,.2f}'
109
+ Or, hr, lr = (self.df.loc[index, 'rate_open'], self.df.loc[index, 'rate_high'], self.df.loc[index, 'rate_low'])
110
+
111
+ if self.fraction:
112
+ data = {}
113
+ c = round(c, self.CONFIG.UNIT.digit)
114
+ for value, key in [
115
+ [c, 'close'],
116
+ [compare, 'compare'],
117
+ [o, 'open'],
118
+ [h, 'high'],
119
+ [l, 'low'],
120
+ ]:
121
+ div = divmod(value, 1)
122
+ if div[1]:
123
+ if div[0]:
124
+ data[key] = f'{float_to_str(div[0])} {Fraction((div[1]))}'
125
+ else:
126
+ data[key] = f'  {Fraction((div[1]))}'
127
+ else:
128
+ data[key] = float_to_str(div[0])
129
+ # print(f'{data=}')
130
+
131
+ kwargs = self.get_info_kwargs(
132
+ is_price=is_price,
133
+ dt=dt,
134
+ close=f'{data["close"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
135
+ rate=f'{r:>{self._length_text}}%',
136
+ compare=f'{data["compare"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
137
+ open=f'{data["open"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
138
+ high=f'{data["high"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
139
+ low=f'{data["low"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
140
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
141
+ )
142
+ text = self.CONFIG.FORMAT.candle.format(**kwargs)
143
+ else:
144
+ o, h, l, c = (
145
+ float_to_str(o, digit=self.CONFIG.UNIT.digit),
146
+ float_to_str(h, digit=self.CONFIG.UNIT.digit),
147
+ float_to_str(l, digit=self.CONFIG.UNIT.digit),
148
+ float_to_str(c, digit=self.CONFIG.UNIT.digit),
149
+ )
150
+ com = float_to_str(compare, digit=self.CONFIG.UNIT.digit, plus=True)
151
+
152
+ kwargs = self.get_info_kwargs(
153
+ is_price=is_price,
154
+ dt=dt,
155
+ close=f'{c:>{self._length_text}}{self.CONFIG.UNIT.price}',
156
+ rate=f'{r:>{self._length_text}}%',
157
+ compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.price}',
158
+ open=f'{o:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
159
+ high=f'{h:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
160
+ low=f'{l:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
161
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
162
+ )
163
+ text = self.CONFIG.FORMAT.candle.format(**kwargs)
164
+ elif self.key_volume:
165
+ compare = self.df.loc[index, 'compare_volume']
166
+ com = float_to_str(compare, digit=self.CONFIG.UNIT.digit_volume, plus=True)
167
+ kwargs = self.get_info_kwargs(
168
+ is_price=is_price,
169
+ dt=dt,
170
+ volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}',
171
+ rate_volume=f'{vr:>{self._length_text}}%',
172
+ compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.volume}',
173
+ )
174
+ text = self.CONFIG.FORMAT.volume.format(**kwargs)
175
+ else:
176
+ text = ''
177
+
178
+ return text
179
+
180
+
181
+ class BaseMixin(InfoMixin):
182
+ pass
183
+
184
+
185
+ class Chart(BaseMixin):
186
+ pass
187
+
@@ -0,0 +1,2 @@
1
+ from ._draw import BaseMixin, Chart
2
+
@@ -0,0 +1,50 @@
1
+ from matplotlib.collections import LineCollection
2
+
3
+ from .._base import BaseMixin as Base
4
+
5
+
6
+ class CollectionMixin(Base):
7
+ def _add_artists(self):
8
+ super()._add_artists()
9
+
10
+ self.collection_candle = LineCollection([], animated=True, linewidths=0.8)
11
+ self.ax_price.add_collection(self.collection_candle)
12
+
13
+ self.collection_ma = LineCollection([], animated=True, linewidths=1)
14
+ self.ax_price.add_collection(self.collection_ma)
15
+
16
+ self.collection_volume = LineCollection([], animated=True, linewidths=1)
17
+ self.ax_volume.add_collection(self.collection_volume)
18
+ return
19
+
20
+
21
+ class SegmentMixin(CollectionMixin):
22
+ def get_candle_segment(self, *, is_up, x, left, right, top, bottom, high, low):
23
+ """
24
+ get candle segment
25
+
26
+ Args:
27
+ is_up (bool): (True if open < close else False)
28
+ x (float): center of candle
29
+ left (float): left of candle
30
+ right (float): right of candle
31
+ top (float): top of candle(close if `is_up` else open)
32
+ bottom (float): bottom of candle(open if `is_up` else close)
33
+ high (float): top of candle wick
34
+ low (float): bottom of candle wick
35
+
36
+ Returns:
37
+ tuple[tuple[float, float]]: candle segment
38
+ """
39
+ return (
40
+ (x, top),
41
+ (left, top), (left, bottom),
42
+ (x, bottom), (x, low), (x, bottom),
43
+ (right, bottom), (right, top),
44
+ (x, top), (x, high)
45
+ )
46
+
47
+
48
+ class BaseMixin(SegmentMixin):
49
+ pass
50
+
@@ -0,0 +1,314 @@
1
+ from matplotlib.lines import Line2D
2
+ import numpy as np
3
+ import pandas as pd
4
+
5
+ from ._artist import BaseMixin as Base
6
+
7
+
8
+ class DataMixin(Base):
9
+ df: pd.DataFrame
10
+
11
+ key_date = 'date'
12
+ key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
13
+ key_volume = 'volume'
14
+
15
+ def set_data(self, df: pd.DataFrame, *args, **kwargs):
16
+ # print(f'{kwargs=}')
17
+ self._set_data(df, *args, **kwargs)
18
+ self.figure.canvas.draw_idle()
19
+ return
20
+
21
+ def _set_data(self, df: pd.DataFrame, *args, **kwargs):
22
+ keys = {
23
+ self.key_date: 'date',
24
+ self.key_open: 'open',
25
+ self.key_high: 'high',
26
+ self.key_low: 'low',
27
+ self.key_close: 'close',
28
+ self.key_volume: 'volume',
29
+ }
30
+ df.rename(columns=keys, inplace=True)
31
+
32
+ # 오름차순 정렬
33
+ df = df.sort_values(['date'])
34
+ df = df.reset_index(drop=True)
35
+
36
+ if self.key_volume:
37
+ df = df[['date', 'open', 'high', 'low', 'close', 'volume']]
38
+ else:
39
+ df = df[['date', 'open', 'high', 'low', 'close',]]
40
+ df['volume'] = 0
41
+
42
+ # 전일 종가
43
+ df['pre_close'] = df['close'].shift(1).fillna(0)
44
+ # 거래정지인 경우 전일종가 적용
45
+ df.loc[df['close'] == 0, 'close'] = df['pre_close']
46
+ # 종가만 유효한 경우 종가로 통일
47
+ df.loc[(df['close'] != 0) & (df['open'] == 0), ['open', 'high', 'low']] = df['close']
48
+
49
+ self.chart_price_ymax = df['high'].max() * 1.3
50
+ if self.key_volume:
51
+ self.chart_volume_ymax = df['volume'].max() * 1.3
52
+ else:
53
+ self.chart_volume_ymax = 10
54
+ # 거래량 차트 영역 최소화
55
+ self.CONFIG.FIGURE.RATIO.volume = 0
56
+ # tick 그리지 않기
57
+ self.ax_volume.set_yticklabels([])
58
+ self.ax_volume.set_yticks([])
59
+ self._set_figure()
60
+
61
+ self.index_list = df.index.tolist()
62
+ self.xmin, self.xmax = (0, self.index_list[-1])
63
+
64
+ if not self.CONFIG.MA.ma_list:
65
+ self.CONFIG.MA.ma_list = tuple()
66
+ else:
67
+ self.CONFIG.MA.ma_list = sorted(self.CONFIG.MA.ma_list)
68
+ # 가격이동평균선 계산
69
+ for i in self.CONFIG.MA.ma_list:
70
+ df[f'ma{i}'] = df['close'].rolling(i).mean()
71
+
72
+ df['x'] = df.index + 0.5
73
+ df['left_candle'] = df['x'] - self.CONFIG.CANDLE.half_width
74
+ df['right_candle'] = df['x'] + self.CONFIG.CANDLE.half_width
75
+ df['left_volume'] = df['x'] - self.CONFIG.VOLUME.half_width
76
+ df['right_volume'] = df['x'] + self.CONFIG.VOLUME.half_width
77
+ df['zero'] = 0
78
+
79
+ df['is_up'] = np.where(df['open'] < df['close'], True, False)
80
+ df['top_candle'] = np.where(df['is_up'], df['close'], df['open'])
81
+ df['bottom_candle'] = np.where(df['is_up'], df['open'], df['close'])
82
+
83
+ if self.key_volume:
84
+ df['ymax_volume'] = df['volume'] * 1.2
85
+
86
+ self.df = df
87
+
88
+ return
89
+
90
+
91
+ class CandleSegmentMixin(DataMixin):
92
+ def set_segments(self):
93
+ self.set_candle_segments()
94
+ self.set_candle_color_segments()
95
+ return
96
+
97
+ def set_color_segments(self):
98
+ self.set_candle_color_segments()
99
+ return
100
+
101
+ def set_candle_segments(self):
102
+ # 캔들 세그먼트
103
+ segment_candle = []
104
+ segment_wick = []
105
+ segment_priceline = []
106
+ for x, left, right, top, bottom, is_up, high, low in zip(
107
+ self.df['x'].to_numpy().tolist(),
108
+ self.df['left_candle'].to_numpy().tolist(), self.df['right_candle'].to_numpy().tolist(),
109
+ self.df['top_candle'].to_numpy().tolist(), self.df['bottom_candle'].to_numpy().tolist(),
110
+ self.df['is_up'].to_numpy().tolist(),
111
+ self.df['high'].to_numpy().tolist(), self.df['low'].to_numpy().tolist(),
112
+ ):
113
+ segment_candle.append(
114
+ self.get_candle_segment(
115
+ is_up=is_up,
116
+ x=x, left=left, right=right,
117
+ top=top, bottom=bottom,
118
+ high=high, low=low,
119
+ )
120
+ )
121
+
122
+ self.segment_candle = np.array(segment_candle)
123
+ # 심지 세그먼트
124
+ segment_wick = self.df[[
125
+ 'x', 'high',
126
+ 'x', 'low',
127
+ ]].values
128
+ self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
129
+ # 종가 세그먼트
130
+ segment_priceline = segment_wick = self.df[['x', 'close']].values
131
+ self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
132
+ return
133
+
134
+ def set_candle_color_segments(self):
135
+ self.add_candle_color_column()
136
+
137
+ self.facecolor_candle = self.df['facecolor'].values
138
+ self.edgecolor_candle = self.df['edgecolor'].values
139
+ return
140
+
141
+ def add_candle_color_column(self):
142
+ columns = ['facecolor', 'edgecolor']
143
+ face_bull_rise = self.CONFIG.CANDLE.FACECOLOR.bull_rise
144
+ face_bull_fall = self.CONFIG.CANDLE.FACECOLOR.bull_fall
145
+ face_bear_rise = self.CONFIG.CANDLE.FACECOLOR.bear_rise
146
+ face_bear_fall = self.CONFIG.CANDLE.FACECOLOR.bear_fall
147
+ edge_bull_rise = self.CONFIG.CANDLE.EDGECOLOR.bull_rise
148
+ edge_bull_fall = self.CONFIG.CANDLE.EDGECOLOR.bull_fall
149
+ edge_bear_rise = self.CONFIG.CANDLE.EDGECOLOR.bear_rise
150
+ edge_bear_fall = self.CONFIG.CANDLE.EDGECOLOR.bear_fall
151
+ doji = self.CONFIG.CANDLE.EDGECOLOR.doji
152
+
153
+ # 상승양봉
154
+ self.df.loc[:, columns] = (face_bull_rise, edge_bull_rise)
155
+ if face_bull_rise != face_bear_fall or edge_bull_rise != edge_bear_fall:
156
+ # 하락음봉
157
+ self.df.loc[self.df['close'] < self.df['open'], columns] = (face_bear_fall, edge_bear_fall)
158
+ if face_bull_rise != doji or face_bear_fall != doji or edge_bull_rise != doji or edge_bear_fall != doji:
159
+ # 보합
160
+ self.df.loc[self.df['close'] == self.df['open'], columns] = (doji, doji)
161
+
162
+ if face_bull_rise != face_bull_fall or edge_bull_rise != edge_bull_fall:
163
+ # 하락양봉(비우기)
164
+ self.df.loc[(self.df['facecolor'] == face_bull_rise) & (self.df['close'] <= self.df['pre_close']), columns] = (face_bull_fall, edge_bull_fall)
165
+ if face_bear_fall != face_bear_rise or edge_bear_fall != edge_bear_rise:
166
+ # 상승음봉(비우기)
167
+ self.df.loc[(self.df['facecolor'] == face_bear_fall) & (self.df['pre_close'] <= self.df['close']), columns] = (face_bear_rise, edge_bear_rise)
168
+ return
169
+
170
+
171
+ class MaSegmentMixin(CandleSegmentMixin):
172
+ _visible_ma = set()
173
+
174
+ def set_segments(self):
175
+ super().set_segments()
176
+
177
+ self.set_ma_segments()
178
+ self._set_ma_color_segments()
179
+ return
180
+
181
+ def set_color_segments(self):
182
+ self._set_ma_color_segments()
183
+
184
+ super().set_color_segments()
185
+ return
186
+
187
+ def set_ma_segments(self):
188
+ # 주가 차트 가격이동평균선
189
+ key_ma = []
190
+ for i in reversed(self.CONFIG.MA.ma_list):
191
+ key_ma.append('x')
192
+ key_ma.append(f'ma{i}')
193
+ if key_ma:
194
+ segment_ma = self.df[key_ma].values
195
+ self.segment_ma = segment_ma.reshape(
196
+ segment_ma.shape[0], len(self.CONFIG.MA.ma_list), 2
197
+ ).swapaxes(0, 1)
198
+ return
199
+
200
+ def _set_ma_color_segments(self):
201
+ # 기존 legend 제거
202
+ legends = self.ax_legend.get_legend()
203
+ if legends:
204
+ legends.remove()
205
+
206
+ self._visible_ma.clear()
207
+
208
+ # 이평선 색상 가져오기
209
+ handle_list, label_list, list_color = ([], [], [])
210
+ arr = [0, 1]
211
+ for n, i in enumerate(self.CONFIG.MA.ma_list):
212
+ try:
213
+ c = self.CONFIG.MA.color_list[n]
214
+ except:
215
+ c = self.CONFIG.MA.color_default
216
+ list_color.append(c)
217
+
218
+ handle_list.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
219
+ label_list.append(self.CONFIG.MA.format.format(i))
220
+
221
+ self._visible_ma.add(i)
222
+ self.edgecolor_ma = list(reversed(list_color))
223
+
224
+ # 가격이동평균선 legend 생성
225
+ if handle_list:
226
+ legends = self.ax_legend.legend(
227
+ handle_list, label_list, loc='lower left', ncol=10,
228
+ facecolor=self.CONFIG.AX.facecolor, edgecolor=self.CONFIG.AX.TICK.edgecolor,
229
+ labelcolor=self.CONFIG.AX.TICK.fontcolor,
230
+ )
231
+ for i in legends.legend_handles:
232
+ # legend 클릭시 pick event가 발생할 수 있도록 설정
233
+ i.set_picker(5)
234
+ return
235
+
236
+
237
+ class VolumeSegmentMixin(MaSegmentMixin):
238
+ def set_segments(self):
239
+ super().set_segments()
240
+
241
+ if self.key_volume:
242
+ self.set_volume_segments()
243
+ self.set_volume_color_segments()
244
+ return
245
+
246
+ def set_color_segments(self):
247
+ super().set_color_segments()
248
+
249
+ self.set_volume_color_segments()
250
+ return
251
+
252
+ def set_volume_segments(self):
253
+ # 거래량 바 세그먼트
254
+ segment_volume_wick = self.df[[
255
+ 'left_volume', 'zero',
256
+ 'left_volume', 'volume',
257
+ 'right_volume', 'volume',
258
+ 'right_volume', 'zero',
259
+ ]].values
260
+
261
+ self.segment_volume = segment_volume_wick.reshape(segment_volume_wick.shape[0], 4, 2)
262
+
263
+ # segment_volume = []
264
+ # for x, left, right, top in zip(
265
+ # self.df['x'].to_numpy().tolist(),
266
+ # self.df['left_volume'].to_numpy().tolist(), self.df['right_volume'].to_numpy().tolist(),
267
+ # self.df[self.key_volume].to_numpy().tolist(),
268
+ # ):
269
+ # segment_volume.append(
270
+ # self.get_volume_segment(x=x, left=left, right=right, top=top)
271
+ # )
272
+ # self.segment_volume = np.array(segment_volume)
273
+
274
+ # 거래량 심지 세그먼트
275
+ segment_volume_wick = self.df[[
276
+ 'x', 'zero',
277
+ 'x', 'volume',
278
+ ]].values
279
+ self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
280
+
281
+ return
282
+
283
+ def _add_volume_color_column(self):
284
+ columns = ['facecolor_volume', 'edgecolor_volume']
285
+ face_rise = self.CONFIG.VOLUME.FACECOLOR.rise
286
+ face_fall = self.CONFIG.VOLUME.FACECOLOR.fall
287
+ edge_rise = self.CONFIG.VOLUME.EDGECOLOR.rise
288
+ edge_fall = self.CONFIG.VOLUME.EDGECOLOR.fall
289
+ face_doji = self.CONFIG.VOLUME.FACECOLOR.doji
290
+ edge_doji = self.CONFIG.VOLUME.EDGECOLOR.doji
291
+
292
+ # 주가 상승
293
+ self.df.loc[:, columns] = (face_rise, edge_rise)
294
+ if face_rise != face_fall or edge_rise != edge_fall:
295
+ # 주가 하락
296
+ condition = self.df['close'] < self.df['pre_close']
297
+ self.df.loc[condition, columns] = (face_fall, edge_fall)
298
+ if face_rise != face_doji or edge_rise != edge_doji:
299
+ # 보합
300
+ condition = self.df['close'] == self.df['pre_close']
301
+ self.df.loc[condition, columns] = (edge_doji, edge_doji)
302
+ return
303
+
304
+ def set_volume_color_segments(self):
305
+ self._add_volume_color_column()
306
+
307
+ self.facecolor_volume = self.df['facecolor_volume'].values
308
+ self.edgecolor_volume = self.df['edgecolor_volume'].values
309
+ return
310
+
311
+
312
+ class BaseMixin(VolumeSegmentMixin):
313
+ pass
314
+
@@ -0,0 +1,103 @@
1
+ from ._lim import BaseMixin as Base
2
+
3
+
4
+ class DrawMixin(Base):
5
+ candle_on_ma = True
6
+
7
+ def _connect_events(self):
8
+ super()._connect_events()
9
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self.on_draw(x))
10
+ return
11
+
12
+ def on_draw(self, e):
13
+ self._on_draw(e)
14
+ return
15
+
16
+ def _on_draw(self, e):
17
+ self.draw_artists()
18
+ self.figure.canvas.blit()
19
+ return
20
+
21
+ def draw_artists(self):
22
+ self._draw_artists()
23
+ return
24
+
25
+ def _draw_artists(self):
26
+ self._draw_ax_price()
27
+ self._draw_ax_volume()
28
+ return
29
+
30
+ def _draw_ax_price(self):
31
+ renderer = self.figure.canvas.renderer
32
+ # print(f'{renderer=}')
33
+
34
+ self.ax_price.xaxis.draw(renderer)
35
+ self.ax_price.yaxis.draw(renderer)
36
+
37
+ if self.candle_on_ma:
38
+ self.collection_ma.draw(renderer)
39
+ self.collection_candle.draw(renderer)
40
+ else:
41
+ self.collection_candle.draw(renderer)
42
+ self.collection_ma.draw(renderer)
43
+
44
+ if self.watermark:
45
+ if self.watermark != self.artist_watermark.get_text():
46
+ self.artist_watermark.set_text(self.watermark)
47
+ self.artist_watermark.draw(renderer)
48
+ return
49
+
50
+ def _draw_ax_volume(self):
51
+ renderer = self.figure.canvas.renderer
52
+
53
+ self.ax_volume.xaxis.draw(renderer)
54
+ self.ax_volume.yaxis.draw(renderer)
55
+
56
+ self.collection_volume.draw(renderer)
57
+ return
58
+
59
+
60
+ class BackgroundMixin(DrawMixin):
61
+ background = None
62
+
63
+ _creating_background = False
64
+
65
+ def _create_background(self):
66
+ if self._creating_background:
67
+ return
68
+
69
+ self._creating_background = True
70
+ self._copy_bbox()
71
+ self._creating_background = False
72
+ return
73
+
74
+ def _copy_bbox(self):
75
+ self.draw_artists()
76
+
77
+ renderer = self.figure.canvas.renderer
78
+ self.background = renderer.copy_from_bbox(self.figure.bbox)
79
+ return
80
+
81
+ def _on_draw(self, e):
82
+ self.background = None
83
+ self._restore_region()
84
+ return
85
+
86
+ def _restore_region(self):
87
+ if not self.background:
88
+ self._create_background()
89
+
90
+ self.figure.canvas.renderer.restore_region(self.background)
91
+ return
92
+
93
+
94
+ class BaseMixin(BackgroundMixin):
95
+ def _refresh(self):
96
+ self.set_segments()
97
+ self.set_collections(self.vxmin, xmax=self.vxmax)
98
+ return super()._refresh()
99
+
100
+
101
+ class Chart(BaseMixin):
102
+ pass
103
+