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.
- seolpyo_mplchart/__init__.py +53 -333
- seolpyo_mplchart/_chart/__init__.py +145 -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/base/__init__.py +111 -0
- seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
- seolpyo_mplchart/_chart/base/b_artist.py +143 -0
- seolpyo_mplchart/_chart/base/c_draw.py +100 -0
- seolpyo_mplchart/_chart/base/d_segment.py +262 -0
- seolpyo_mplchart/_chart/base/e_axis.py +267 -0
- seolpyo_mplchart/_chart/base/f_background.py +62 -0
- seolpyo_mplchart/_chart/base/g_event.py +66 -0
- seolpyo_mplchart/_chart/base/h_data.py +138 -0
- seolpyo_mplchart/_chart/base/test.py +58 -0
- seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
- seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
- seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
- seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
- seolpyo_mplchart/_chart/cursor/e_axis.py +65 -0
- seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
- seolpyo_mplchart/_chart/cursor/h_data.py +61 -0
- seolpyo_mplchart/_chart/cursor/test.py +69 -0
- seolpyo_mplchart/_chart/slider/__init__.py +169 -0
- seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
- seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
- seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
- seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
- seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
- seolpyo_mplchart/_chart/slider/f_background.py +37 -0
- seolpyo_mplchart/_chart/slider/g_event.py +353 -0
- seolpyo_mplchart/_chart/slider/h_data.py +102 -0
- seolpyo_mplchart/_chart/slider/test.py +71 -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 +31 -0
- seolpyo_mplchart/_config/config.py +21 -0
- seolpyo_mplchart/_config/cursor.py +49 -0
- seolpyo_mplchart/_config/figure.py +40 -0
- seolpyo_mplchart/_config/format.py +51 -0
- seolpyo_mplchart/_config/ma.py +17 -0
- seolpyo_mplchart/_config/slider/__init__.py +2 -0
- seolpyo_mplchart/_config/slider/config.py +24 -0
- seolpyo_mplchart/_config/slider/figure.py +19 -0
- seolpyo_mplchart/_config/slider/nav.py +10 -0
- seolpyo_mplchart/_config/unit.py +19 -0
- seolpyo_mplchart/_config/utils.py +67 -0
- seolpyo_mplchart/_config/volume.py +27 -0
- seolpyo_mplchart/_cursor.py +27 -25
- seolpyo_mplchart/_draw.py +7 -18
- seolpyo_mplchart/_slider.py +26 -20
- seolpyo_mplchart/_utils/__init__.py +10 -0
- seolpyo_mplchart/_utils/nums.py +67 -0
- seolpyo_mplchart/_utils/theme/__init__.py +15 -0
- seolpyo_mplchart/_utils/theme/dark.py +57 -0
- seolpyo_mplchart/_utils/theme/light.py +56 -0
- seolpyo_mplchart/_utils/utils.py +28 -0
- seolpyo_mplchart/_utils/xl/__init__.py +15 -0
- seolpyo_mplchart/_utils/xl/csv.py +46 -0
- seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
- seolpyo_mplchart/sample/apple.txt +6058 -0
- seolpyo_mplchart/sample/samsung.txt +5938 -0
- seolpyo_mplchart/test.py +172 -56
- seolpyo_mplchart/xl_to_dict.py +47 -0
- seolpyo_mplchart-2.1.0.dist-info/METADATA +718 -0
- seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
- {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +1 -1
- seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
- seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
- {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
|
+
|