seolpyo-mplchart 1.4.1__py3-none-any.whl → 2.1.0__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 (83) hide show
  1. seolpyo_mplchart/__init__.py +53 -333
  2. seolpyo_mplchart/_chart/__init__.py +145 -0
  3. seolpyo_mplchart/_chart/_base.py +217 -0
  4. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  5. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  6. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  7. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  8. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  9. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  10. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  11. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  12. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  13. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  14. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  15. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  16. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  17. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  18. seolpyo_mplchart/_chart/base/__init__.py +111 -0
  19. seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
  20. seolpyo_mplchart/_chart/base/b_artist.py +143 -0
  21. seolpyo_mplchart/_chart/base/c_draw.py +100 -0
  22. seolpyo_mplchart/_chart/base/d_segment.py +262 -0
  23. seolpyo_mplchart/_chart/base/e_axis.py +267 -0
  24. seolpyo_mplchart/_chart/base/f_background.py +62 -0
  25. seolpyo_mplchart/_chart/base/g_event.py +66 -0
  26. seolpyo_mplchart/_chart/base/h_data.py +138 -0
  27. seolpyo_mplchart/_chart/base/test.py +58 -0
  28. seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
  29. seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
  30. seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
  31. seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
  32. seolpyo_mplchart/_chart/cursor/e_axis.py +65 -0
  33. seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
  34. seolpyo_mplchart/_chart/cursor/h_data.py +61 -0
  35. seolpyo_mplchart/_chart/cursor/test.py +69 -0
  36. seolpyo_mplchart/_chart/slider/__init__.py +169 -0
  37. seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
  38. seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
  39. seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
  40. seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
  41. seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
  42. seolpyo_mplchart/_chart/slider/f_background.py +37 -0
  43. seolpyo_mplchart/_chart/slider/g_event.py +353 -0
  44. seolpyo_mplchart/_chart/slider/h_data.py +102 -0
  45. seolpyo_mplchart/_chart/slider/test.py +71 -0
  46. seolpyo_mplchart/_chart/test.py +121 -0
  47. seolpyo_mplchart/_config/__init__.py +3 -0
  48. seolpyo_mplchart/_config/ax.py +28 -0
  49. seolpyo_mplchart/_config/candle.py +31 -0
  50. seolpyo_mplchart/_config/config.py +21 -0
  51. seolpyo_mplchart/_config/cursor.py +49 -0
  52. seolpyo_mplchart/_config/figure.py +40 -0
  53. seolpyo_mplchart/_config/format.py +51 -0
  54. seolpyo_mplchart/_config/ma.py +17 -0
  55. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  56. seolpyo_mplchart/_config/slider/config.py +24 -0
  57. seolpyo_mplchart/_config/slider/figure.py +19 -0
  58. seolpyo_mplchart/_config/slider/nav.py +10 -0
  59. seolpyo_mplchart/_config/unit.py +19 -0
  60. seolpyo_mplchart/_config/utils.py +67 -0
  61. seolpyo_mplchart/_config/volume.py +27 -0
  62. seolpyo_mplchart/_cursor.py +27 -25
  63. seolpyo_mplchart/_draw.py +7 -18
  64. seolpyo_mplchart/_slider.py +26 -20
  65. seolpyo_mplchart/_utils/__init__.py +10 -0
  66. seolpyo_mplchart/_utils/nums.py +67 -0
  67. seolpyo_mplchart/_utils/theme/__init__.py +15 -0
  68. seolpyo_mplchart/_utils/theme/dark.py +57 -0
  69. seolpyo_mplchart/_utils/theme/light.py +56 -0
  70. seolpyo_mplchart/_utils/utils.py +28 -0
  71. seolpyo_mplchart/_utils/xl/__init__.py +15 -0
  72. seolpyo_mplchart/_utils/xl/csv.py +46 -0
  73. seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
  74. seolpyo_mplchart/sample/apple.txt +6058 -0
  75. seolpyo_mplchart/sample/samsung.txt +5938 -0
  76. seolpyo_mplchart/test.py +172 -56
  77. seolpyo_mplchart/xl_to_dict.py +47 -0
  78. seolpyo_mplchart-2.1.0.dist-info/METADATA +718 -0
  79. seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
  80. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +1 -1
  81. seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
  82. seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
  83. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,262 @@
1
+ import pandas as pd
2
+ import numpy as np
3
+
4
+ from ..._config import ConfigData
5
+
6
+
7
+ class DataMixin:
8
+ df: pd.DataFrame
9
+ CONFIG: ConfigData
10
+
11
+ def add_volume_color_column(self):
12
+ columns = ['facecolor_volume', 'edgecolor_volume']
13
+ face_rise = self.CONFIG.VOLUME.FACECOLOR.rise
14
+ face_fall = self.CONFIG.VOLUME.FACECOLOR.fall
15
+ edge_rise = self.CONFIG.VOLUME.EDGECOLOR.rise
16
+ edge_fall = self.CONFIG.VOLUME.EDGECOLOR.fall
17
+ face_doji = self.CONFIG.VOLUME.FACECOLOR.doji
18
+ edge_doji = self.CONFIG.VOLUME.EDGECOLOR.doji
19
+
20
+ # 주가 상승
21
+ self.df.loc[:, columns] = (face_rise, edge_rise)
22
+ if face_rise != face_fall or edge_rise != edge_fall:
23
+ # 주가 하락
24
+ condition = self.df['close'] < self.df['pre_close']
25
+ self.df.loc[condition, columns] = (face_fall, edge_fall)
26
+ if face_rise != face_doji or edge_rise != edge_doji:
27
+ # 보합
28
+ condition = self.df['close'] == self.df['pre_close']
29
+ self.df.loc[condition, columns] = (edge_doji, edge_doji)
30
+ return
31
+
32
+ def add_candle_color_column(self):
33
+ columns = ['facecolor', 'edgecolor']
34
+ face_bull_rise = self.CONFIG.CANDLE.FACECOLOR.bull_rise
35
+ face_bull_fall = self.CONFIG.CANDLE.FACECOLOR.bull_fall
36
+ face_bear_rise = self.CONFIG.CANDLE.FACECOLOR.bear_rise
37
+ face_bear_fall = self.CONFIG.CANDLE.FACECOLOR.bear_fall
38
+ edge_bull_rise = self.CONFIG.CANDLE.EDGECOLOR.bull_rise
39
+ edge_bull_fall = self.CONFIG.CANDLE.EDGECOLOR.bull_fall
40
+ edge_bear_rise = self.CONFIG.CANDLE.EDGECOLOR.bear_rise
41
+ edge_bear_fall = self.CONFIG.CANDLE.EDGECOLOR.bear_fall
42
+ doji = self.CONFIG.CANDLE.EDGECOLOR.doji
43
+
44
+ # 상승양봉
45
+ self.df.loc[:, columns] = (face_bull_rise, edge_bull_rise)
46
+ if face_bull_rise != face_bear_fall or edge_bull_rise != edge_bear_fall:
47
+ # 하락음봉
48
+ self.df.loc[self.df['close'] < self.df['open'], columns] = (face_bear_fall, edge_bear_fall)
49
+ if face_bull_rise != doji or face_bear_fall != doji or edge_bull_rise != doji or edge_bear_fall != doji:
50
+ # 보합
51
+ self.df.loc[self.df['close'] == self.df['open'], columns] = (doji, doji)
52
+
53
+ if face_bull_rise != face_bull_fall or edge_bull_rise != edge_bull_fall:
54
+ # 하락양봉(비우기)
55
+ self.df.loc[(self.df['facecolor'] == face_bull_rise) & (self.df['close'] <= self.df['pre_close']), columns] = (face_bull_fall, edge_bull_fall)
56
+ if face_bear_fall != face_bear_rise or edge_bear_fall != edge_bear_rise:
57
+ # 상승음봉(비우기)
58
+ self.df.loc[(self.df['facecolor'] == face_bear_fall) & (self.df['pre_close'] <= self.df['close']), columns] = (face_bear_rise, edge_bear_rise)
59
+ return
60
+
61
+
62
+ class VolumeSegmentMixin(DataMixin):
63
+ key_volume: str
64
+
65
+ segment_volume: np.ndarray
66
+ segment_volume_wick: np.ndarray
67
+ facecolor_volume: np.ndarray
68
+ edgecolor_volume: np.ndarray
69
+
70
+ def set_volume_color_segments(self):
71
+ self.add_volume_color_column()
72
+
73
+ self.facecolor_volume = self.df['facecolor_volume'].values
74
+ self.edgecolor_volume = self.df['edgecolor_volume'].values
75
+ return
76
+
77
+ def set_volume_segments(self):
78
+ # 거래량 바 세그먼트
79
+ segment_volume_wick = self.df[[
80
+ 'left_volume', 'zero',
81
+ 'left_volume', 'volume',
82
+ 'right_volume', 'volume',
83
+ 'right_volume', 'zero',
84
+ ]].values
85
+
86
+ self.segment_volume = segment_volume_wick.reshape(segment_volume_wick.shape[0], 4, 2)
87
+
88
+ # 거래량 심지 세그먼트
89
+ segment_volume_wick = self.df[[
90
+ 'x', 'zero',
91
+ 'x', 'volume',
92
+ ]].values
93
+ self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
94
+
95
+ return
96
+
97
+
98
+ class MethodMixin:
99
+ def get_candle_segment(self, *, is_up, x, left, right, top, bottom, high, low):
100
+ """
101
+ get candle segment
102
+
103
+ Args:
104
+ is_up (bool): (True if open < close else False)
105
+ x (float): center of candle
106
+ left (float): left of candle
107
+ right (float): right of candle
108
+ top (float): top of candle(close if `is_up` else open)
109
+ bottom (float): bottom of candle(open if `is_up` else close)
110
+ high (float): top of candle wick
111
+ low (float): bottom of candle wick
112
+
113
+ Returns:
114
+ tuple[tuple[float, float]]: candle segment
115
+ """
116
+ return (
117
+ (x, top),
118
+ (left, top), (left, bottom),
119
+ (x, bottom), (x, low), (x, bottom),
120
+ (right, bottom), (right, top),
121
+ (x, top), (x, high)
122
+ )
123
+
124
+ def get_bar_segment(self, *, is_up, x, left, right, top, bottom, high, low):
125
+ if is_up:
126
+ return (
127
+ (x, top),
128
+ (x, high),
129
+ (x, top),
130
+ (right, top),
131
+ (x, top),
132
+ (x, low),
133
+ (x, bottom),
134
+ (left, bottom),
135
+ (x, bottom),
136
+ )
137
+ return (
138
+ (x, top),
139
+ (x, high),
140
+ (x, top),
141
+ (left, top),
142
+ (x, top),
143
+ (x, low),
144
+ (x, bottom),
145
+ (right, bottom),
146
+ (x, bottom),
147
+ )
148
+
149
+
150
+ class CandleSegmentMixin(MethodMixin, DataMixin):
151
+ segment_candle: np.ndarray
152
+ segment_candle_wick: np.ndarray
153
+ segment_priceline: np.ndarray
154
+ facecolor_candle: np.ndarray
155
+ edgecolor_candle: np.ndarray
156
+
157
+ def set_candle_color_segments(self):
158
+ self.add_candle_color_column()
159
+
160
+ self.facecolor_candle = self.df['facecolor'].values
161
+ self.edgecolor_candle = self.df['edgecolor'].values
162
+ return
163
+
164
+ def set_candle_segments(self):
165
+ # 캔들 세그먼트
166
+ segment_candle = []
167
+ for x, left, right, top, bottom, is_up, high, low in zip(
168
+ self.df['x'].to_numpy().tolist(),
169
+ self.df['left_candle'].to_numpy().tolist(), self.df['right_candle'].to_numpy().tolist(),
170
+ self.df['top_candle'].to_numpy().tolist(), self.df['bottom_candle'].to_numpy().tolist(),
171
+ self.df['is_up'].to_numpy().tolist(),
172
+ self.df['high'].to_numpy().tolist(), self.df['low'].to_numpy().tolist(),
173
+ ):
174
+ segment_candle.append(
175
+ self.get_candle_segment(
176
+ is_up=is_up,
177
+ x=x, left=left, right=right,
178
+ top=top, bottom=bottom,
179
+ high=high, low=low,
180
+ )
181
+ )
182
+
183
+ self.segment_candle = np.array(segment_candle)
184
+ return
185
+
186
+ def _set_candle_segments(self):
187
+ # 심지 세그먼트
188
+ segment_wick = self.df[[
189
+ 'x', 'high',
190
+ 'x', 'low',
191
+ ]].values
192
+ self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
193
+ # 종가 세그먼트
194
+ segment_priceline = segment_wick = self.df[['x', 'close']].values
195
+ self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
196
+ return
197
+
198
+
199
+ class MaSegmentMixin(DataMixin):
200
+ _visible_ma: set
201
+
202
+ segment_ma: np.ndarray
203
+ edgecolor_ma: np.ndarray
204
+
205
+ def set_ma_segments(self):
206
+ # 주가 차트 가격이동평균선
207
+ key_ma = []
208
+ for i in reversed(self.CONFIG.MA.ma_list):
209
+ key_ma.append('x')
210
+ key_ma.append(f'ma{i}')
211
+ if key_ma:
212
+ segment_ma = self.df[key_ma].values
213
+ self.segment_ma = segment_ma.reshape(
214
+ segment_ma.shape[0], len(self.CONFIG.MA.ma_list), 2
215
+ ).swapaxes(0, 1)
216
+ return
217
+
218
+ def _set_ma_color_segments(self):
219
+ # 이평선 색상 가져오기
220
+ edgecolors = []
221
+ for n, _ in enumerate(self.CONFIG.MA.ma_list):
222
+ try:
223
+ c = self.CONFIG.MA.color_list[n]
224
+ except:
225
+ c = self.CONFIG.MA.color_default
226
+ edgecolors.append(c)
227
+
228
+ self.edgecolor_ma = list(reversed(edgecolors))
229
+
230
+ return
231
+
232
+
233
+ class SegmentMixin(CandleSegmentMixin, VolumeSegmentMixin, MaSegmentMixin):
234
+ segment_volume: np.ndarray
235
+ segment_volume_wick: np.ndarray
236
+ facecolor_volume: np.ndarray
237
+ edgecolor_volume: np.ndarray
238
+
239
+ segment_candle: np.ndarray
240
+ segment_candle_wick: np.ndarray
241
+ segment_priceline: np.ndarray
242
+ facecolor_candle: np.ndarray
243
+ edgecolor_candle: np.ndarray
244
+
245
+ segment_ma: np.ndarray
246
+ edgecolor_ma: np.ndarray
247
+
248
+ def set_segments(self):
249
+ self.set_candle_segments()
250
+ self._set_candle_segments()
251
+ self.set_volume_segments()
252
+ self.set_ma_segments()
253
+
254
+ self.set_color_segments()
255
+ return
256
+
257
+ def set_color_segments(self):
258
+ self.set_candle_color_segments()
259
+ self.set_volume_color_segments()
260
+ self._set_ma_color_segments()
261
+ return
262
+
@@ -0,0 +1,267 @@
1
+ from matplotlib.axes import Axes
2
+ from matplotlib.collections import LineCollection
3
+ from matplotlib.text import Text
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ from ..._config import ConfigData
8
+
9
+
10
+ class Base:
11
+ CONFIG: ConfigData
12
+
13
+ key_volume: str
14
+ df: pd.DataFrame
15
+
16
+ index_list: list[int]
17
+
18
+ collection_candle: LineCollection
19
+ collection_volume: LineCollection
20
+ collection_ma: LineCollection
21
+
22
+ segment_candle: np.ndarray
23
+ segment_candle_wick: np.ndarray
24
+ segment_priceline: np.ndarray
25
+ facecolor_candle: np.ndarray
26
+ edgecolor_candle: np.ndarray
27
+
28
+ segment_volume: np.ndarray
29
+ segment_volume_wick: np.ndarray
30
+ facecolor_volume: np.ndarray
31
+ edgecolor_volume: np.ndarray
32
+
33
+ segment_ma: np.ndarray
34
+ edgecolor_ma: np.ndarray
35
+
36
+
37
+ class LimMixin(Base):
38
+ def _get_indices(self, ind_start, *, ind_end):
39
+ "조회 영역에 해당하는 index 가져오기"
40
+ if ind_start < 0:
41
+ ind_start = 0
42
+ if ind_end < 1:
43
+ ind_end = 1
44
+
45
+ if ind_end < ind_start:
46
+ msg = 'ind_end < ind_start'
47
+ msg += f' {ind_start=:,}'
48
+ msg += f' {ind_end=:,}'
49
+ raise Exception(msg)
50
+ return (ind_start, ind_end)
51
+
52
+ def _get_price_ylim(self, ind_start, *, ind_end):
53
+ ymin, ymax = (self.df['low'][ind_start:ind_end].min(), self.df['high'][ind_start:ind_end].max())
54
+
55
+ if ymin == ymax:
56
+ if ymax:
57
+ ymin, ymax = (round(ymax * 0.9, self.CONFIG.UNIT.digit+2), round(ymax * 1.1, self.CONFIG.UNIT.digit+2))
58
+ else:
59
+ ymin, ymax = (-5, 10)
60
+ else:
61
+ height = ymax - ymin
62
+ if height < 15:
63
+ height = 15
64
+
65
+ ymin = ymin - round(height / 20, self.CONFIG.UNIT.digit+2)
66
+ ymax = ymax + round(height / 10, self.CONFIG.UNIT.digit+2)
67
+
68
+ return (ymin, ymax)
69
+
70
+ def _get_volume_ylim(self, ind_start, *, ind_end):
71
+ if not self.key_volume:
72
+ ymax = 1
73
+ else:
74
+ series = self.df['volume'][ind_start:ind_end]
75
+ # print(f'{series=}')
76
+ ymax = series.max()
77
+ height = ymax
78
+ ymax = ymax + round(height / 5, self.CONFIG.UNIT.digit_volume+2)
79
+ if ymax < 1:
80
+ ymax = 1
81
+ # print(f'{ymax=}')
82
+ return (0, ymax)
83
+
84
+
85
+ class MaMixin(Base):
86
+ def _set_ma_collection_segments(self, ind_start, ind_end):
87
+ self.collection_ma.set_segments(self.segment_ma[:, ind_start:ind_end])
88
+ self.collection_ma.set_edgecolor(self.edgecolor_ma)
89
+ return
90
+
91
+
92
+ class VolumeMixin(Base):
93
+ def _set_volume_collection_segments(self, ind_start, ind_end):
94
+ if not self.key_volume:
95
+ self.collection_volume.set_segments([])
96
+ return
97
+
98
+ self.collection_volume.set_segments(self.segment_volume[ind_start:ind_end])
99
+ self.collection_volume.set_linewidth(self.CONFIG.VOLUME.linewidth)
100
+ self.collection_volume.set_facecolor(self.facecolor_volume[ind_start:ind_end])
101
+ self.collection_volume.set_edgecolor(self.edgecolor_volume[ind_start:ind_end])
102
+ return
103
+
104
+ def _set_volume_collection_wick_segments(self, ind_start, ind_end):
105
+ # print(f'{(ind_start, ind_end)=}')
106
+ if not self.key_volume:
107
+ self.collection_volume.set_segments([])
108
+ return
109
+
110
+ seg_volume = self.segment_volume_wick[ind_start:ind_end]
111
+ seg_edgecolor_volume = self.edgecolor_volume[ind_start:ind_end]
112
+
113
+ self.collection_volume.set_segments(seg_volume)
114
+ self.collection_volume.set_linewidth(1.3)
115
+ self.collection_volume.set_facecolor([])
116
+ self.collection_volume.set_edgecolor(seg_edgecolor_volume)
117
+ return
118
+
119
+ class CandleMixin(Base):
120
+ def _set_candle_collection_segments(self, ind_start, ind_end):
121
+ # print(f'{self.edgecolor_candle[ind_start:ind_end]=}')
122
+ self.collection_candle.set_segments(self.segment_candle[ind_start:ind_end])
123
+ self.collection_candle.set_facecolor(self.facecolor_candle[ind_start:ind_end])
124
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[ind_start:ind_end])
125
+ self.collection_candle.set_linewidth(self.CONFIG.CANDLE.linewidth)
126
+ self.collection_candle.set_antialiased(False)
127
+ return
128
+
129
+ def _set_candle_collection_wick_segments(self, ind_start, ind_end):
130
+ # print(f'{self.edgecolor_candle[ind_start:ind_end]=}')
131
+ self.collection_candle.set_segments(self.segment_candle_wick[ind_start:ind_end])
132
+ self.collection_candle.set_facecolor([])
133
+ self.collection_candle.set_edgecolor(self.edgecolor_candle[ind_start:ind_end])
134
+ self.collection_candle.set_linewidth(1.5)
135
+ self.collection_candle.set_antialiased(False)
136
+ return
137
+
138
+ def set_candle_collection_priceline_segments(self, ind_start, ind_end):
139
+ self.collection_candle.set_segments(self.segment_priceline[:, ind_start:ind_end])
140
+ self.collection_candle.set_facecolor([])
141
+ self.collection_candle.set_edgecolor(self.CONFIG.CANDLE.line_color)
142
+ self.collection_candle.set_linewidth(2)
143
+ self.collection_candle.set_antialiased(True)
144
+ return
145
+
146
+
147
+ class CollectionMixin(CandleMixin, VolumeMixin, MaMixin):
148
+ limit_candle = 400
149
+ limit_wick = 2_000
150
+
151
+ def set_collections(self, ind_start, *, ind_end):
152
+ if ind_start < 0:
153
+ ind_start = 0
154
+ indsub = ind_end - ind_start
155
+ # print(f'{indsub=:,}')
156
+
157
+ if not self.limit_candle or indsub < self.limit_candle:
158
+ # print('candle')
159
+ self._set_candle_collection_segments(ind_start, ind_end=ind_end)
160
+ self._set_volume_collection_segments(ind_start, ind_end=ind_end)
161
+ else:
162
+ self._set_volume_collection_wick_segments(ind_start, ind_end=ind_end)
163
+
164
+ if not self.limit_wick or indsub < self.limit_wick:
165
+ # print('wick')
166
+ self._set_candle_collection_wick_segments(ind_start, ind_end=ind_end)
167
+ else:
168
+ # print('line')
169
+ self.set_candle_collection_priceline_segments(ind_start, ind_end=ind_end)
170
+
171
+ self._set_ma_collection_segments(ind_start, ind_end=ind_end)
172
+ return
173
+
174
+
175
+ class AxisMixin(LimMixin, CollectionMixin):
176
+ limit_candle = 400
177
+ limit_wick = 2_000
178
+
179
+ ax_price: Axes
180
+ ax_volume: Axes
181
+
182
+ vxmin: int
183
+ vxmax: int
184
+ price_ymin: int
185
+ price_ymax: int
186
+ volume_ymax: int
187
+
188
+ def axis(self, xmin, *, xmax):
189
+ "조회 영역 변경"
190
+ # print('base axis')
191
+ self.set_collections(xmin, ind_end=xmax+1)
192
+
193
+ self.vxmin, self.vxmax = (xmin, xmax+1)
194
+ ind_start, ind_end = self._get_indices(xmin, ind_end=xmax)
195
+
196
+ self.price_ymin, self.price_ymax = self._get_price_ylim(ind_start, ind_end=ind_end)
197
+
198
+ # 주가 차트 xlim
199
+ self.ax_price.set_xlim(self.vxmin, self.vxmax)
200
+ # 주가 차트 ylim
201
+ self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
202
+
203
+ # 거래량 차트 xlim
204
+ self.ax_volume.set_xlim(self.vxmin, self.vxmax)
205
+ self.volume_ymax = 1
206
+ if self.key_volume:
207
+ _, self.volume_ymax = self._get_volume_ylim(ind_start, ind_end=ind_end)
208
+ # 거래량 차트 ylim
209
+ self.ax_volume.set_ylim(0, self.volume_ymax)
210
+
211
+ self.set_xtick_labels(xmin, xmax=xmax)
212
+ return
213
+
214
+ def set_xtick_labels(self, xmin, *, xmax):
215
+ # x축에 일부 date 표시하기
216
+ xsub = xmax - xmin
217
+ xmiddle = xmin + (xsub // 2)
218
+ indices = [idx for idx in (xmin, xmiddle, xmax) if 0 <= idx and idx <= self.index_list[-1]]
219
+ # print(f'{xmiddle=}')
220
+ # print(f'{indices=}')
221
+
222
+ m = (xmiddle - xmin) // 2
223
+ ind_end = self.index_list[-1]
224
+ aligns = ['left', 'center', 'center']
225
+ if len(indices) < 2:
226
+ if xmin < 0 and self.index_list[-1] < xmax:
227
+ indices = [0, xmiddle, ind_end]
228
+ else:
229
+ if xmin <= 0:
230
+ if m <= xmax:
231
+ aligns = aligns[-2:]
232
+ indices = [0, xmax]
233
+ else:
234
+ aligns = aligns[-1:]
235
+ indices = [0]
236
+ else:
237
+ if xmin+m <= ind_end:
238
+ aligns = aligns[:2]
239
+ indices = [xmin, ind_end]
240
+ else:
241
+ aligns = aligns[:1]
242
+ indices = [ind_end]
243
+ elif len(indices) < 3:
244
+ if xmin < 0:
245
+ if 0 <= (xmiddle - m):
246
+ indices = [0] + indices
247
+ else:
248
+ aligns = aligns[-2:]
249
+ indices[0] = 0
250
+ else:
251
+ if (xmiddle + m) <= ind_end:
252
+ indices.append(ind_end)
253
+ else:
254
+ aligns = aligns[:2]
255
+ indices[-1] = ind_end
256
+
257
+ date_list = [self.df.iloc[idx]['date'] for idx in indices]
258
+ # 라벨을 노출할 틱 위치, major tick과 겹쳐서 무시되는 것 방지
259
+ self.ax_volume.set_xticks([idx+0.501 for idx in indices], minor=True)
260
+ # 라벨
261
+ self.ax_volume.set_xticklabels(date_list, minor=True)
262
+ labels: list[Text] = self.ax_volume.get_xticklabels(minor=True)
263
+ for label, align in zip(labels, aligns):
264
+ # 라벨 텍스트 정렬
265
+ label.set_horizontalalignment(align)
266
+ return
267
+
@@ -0,0 +1,62 @@
1
+ from .a_canvas import Figure
2
+
3
+
4
+ class Base:
5
+ _background = None
6
+ _background_background = None
7
+
8
+ _creating_background = False
9
+
10
+ figure: Figure
11
+
12
+ draw_chart: callable
13
+ draw_artists: callable
14
+ draw_background: callable
15
+
16
+ def _draw_canvas(self):
17
+ self._background = None
18
+ self._restore_region()
19
+ return
20
+
21
+ def _restore_region(self):
22
+ # print(f'{self._background=}')
23
+ if not self._background:
24
+ self._create_background()
25
+
26
+ self.figure.canvas.renderer.restore_region(self._background)
27
+ return
28
+
29
+ def _restore_region_background(self):
30
+ if not self._background:
31
+ self._create_background()
32
+
33
+ self.figure.canvas.renderer.restore_region(self._background_background)
34
+ return
35
+
36
+ def _create_background(self):
37
+ if self._creating_background:
38
+ return
39
+
40
+ self._creating_background = True
41
+ self._copy_bbox()
42
+ self._creating_background = False
43
+ return
44
+
45
+ def _copy_bbox(self):
46
+ renderer = self.figure.canvas.renderer
47
+
48
+ self.draw_background()
49
+ self._background_background = renderer.copy_from_bbox(self.figure.bbox)
50
+
51
+ self.draw_chart()
52
+ self.draw_artists()
53
+ self._background = renderer.copy_from_bbox(self.figure.bbox)
54
+
55
+ return
56
+
57
+
58
+ class BackgroundMixin(Base):
59
+ _background = None
60
+ _background_background = None
61
+ _creating_background = False
62
+
@@ -0,0 +1,66 @@
1
+ from matplotlib.axes import Axes
2
+ from matplotlib.backend_bases import PickEvent
3
+ from matplotlib.collections import LineCollection
4
+
5
+ from ..._config import ConfigData
6
+
7
+ from .a_canvas import Figure
8
+
9
+
10
+ class Base:
11
+ figure: Figure
12
+
13
+ _draw_canvas: callable
14
+ _set_figure_ratios: callable
15
+
16
+ def on_draw(self, e):
17
+ self._background = None
18
+ self._draw_canvas()
19
+ return
20
+
21
+ def on_resize(self, e):
22
+ self._set_figure_ratios()
23
+ return
24
+
25
+
26
+ class LegendMixin:
27
+ CONFIG: ConfigData
28
+
29
+ figure: Figure
30
+ ax_legend: Axes
31
+ collection_ma: LineCollection
32
+
33
+ def on_pick(self, e):
34
+ self._pick_legend_action(e)
35
+ return
36
+
37
+ def _pick_legend_action(self, e: PickEvent):
38
+ handle = e.artist
39
+ ax = handle.axes
40
+ # print(f'{(ax is self.ax_legend)=}')
41
+ if ax is not self.ax_legend:
42
+ return
43
+
44
+ visible = handle.get_alpha() == 0.2
45
+ handle.set_alpha(1.0 if visible else 0.2)
46
+
47
+ n = int(handle.get_label())
48
+ if visible:
49
+ self._visible_ma = {i for i in self.CONFIG.MA.ma_list if i in self._visible_ma or i == n}
50
+ else:
51
+ self._visible_ma = {i for i in self._visible_ma if i != n}
52
+
53
+ alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.CONFIG.MA.ma_list)]
54
+ self.collection_ma.set_alpha(alphas)
55
+
56
+ self.figure.canvas.draw()
57
+ return
58
+
59
+
60
+ class EventMixin(Base, LegendMixin):
61
+ def connect_events(self):
62
+ self.figure.canvas.mpl_connect('draw_event', lambda x: self.on_draw(x))
63
+ self.figure.canvas.mpl_connect('pick_event', lambda x: self.on_pick(x))
64
+ self.figure.canvas.mpl_connect('resize_event', lambda x: self.on_resize(x))
65
+ return
66
+