seolpyo-mplchart 2.0.0.3__py3-none-any.whl → 2.1.0.4__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 +17 -133
- seolpyo_mplchart/_chart/__init__.py +39 -31
- 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 +268 -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 +64 -0
- seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
- seolpyo_mplchart/_chart/cursor/h_data.py +62 -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/_config/ax.py +2 -0
- seolpyo_mplchart/_config/candle.py +1 -0
- seolpyo_mplchart/_config/cursor.py +4 -0
- seolpyo_mplchart/_config/figure.py +4 -4
- seolpyo_mplchart/_config/ma.py +2 -0
- seolpyo_mplchart/_config/nums.py +67 -0
- seolpyo_mplchart/_config/slider/config.py +2 -2
- seolpyo_mplchart/_config/slider/figure.py +3 -4
- seolpyo_mplchart/_config/slider/nav.py +3 -2
- seolpyo_mplchart/_config/unit.py +1 -1
- seolpyo_mplchart/_config/volume.py +1 -0
- seolpyo_mplchart/_utils/__init__.py +10 -0
- seolpyo_mplchart/_utils/nums.py +1 -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 +5 -5
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/METADATA +22 -14
- seolpyo_mplchart-2.1.0.4.dist-info/RECORD +90 -0
- seolpyo_mplchart-2.0.0.3.dist-info/RECORD +0 -50
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,268 @@
|
|
|
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
|
+
|
|
120
|
+
class CandleMixin(Base):
|
|
121
|
+
def _set_candle_collection_segments(self, ind_start, ind_end):
|
|
122
|
+
# print(f'{self.edgecolor_candle[ind_start:ind_end]=}')
|
|
123
|
+
self.collection_candle.set_segments(self.segment_candle[ind_start:ind_end])
|
|
124
|
+
self.collection_candle.set_facecolor(self.facecolor_candle[ind_start:ind_end])
|
|
125
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[ind_start:ind_end])
|
|
126
|
+
self.collection_candle.set_linewidth(self.CONFIG.CANDLE.linewidth)
|
|
127
|
+
self.collection_candle.set_antialiased(False)
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
def _set_candle_collection_wick_segments(self, ind_start, ind_end):
|
|
131
|
+
# print(f'{self.edgecolor_candle[ind_start:ind_end]=}')
|
|
132
|
+
self.collection_candle.set_segments(self.segment_candle_wick[ind_start:ind_end])
|
|
133
|
+
self.collection_candle.set_facecolor([])
|
|
134
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[ind_start:ind_end])
|
|
135
|
+
self.collection_candle.set_linewidth(self.CONFIG.CANDLE.linewidth * 2)
|
|
136
|
+
self.collection_candle.set_antialiased(False)
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
def set_candle_collection_priceline_segments(self, ind_start, ind_end):
|
|
140
|
+
self.collection_candle.set_segments(self.segment_priceline[:, ind_start:ind_end])
|
|
141
|
+
self.collection_candle.set_facecolor([])
|
|
142
|
+
self.collection_candle.set_edgecolor(self.CONFIG.CANDLE.line_color)
|
|
143
|
+
self.collection_candle.set_linewidth(2)
|
|
144
|
+
self.collection_candle.set_antialiased(True)
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class CollectionMixin(CandleMixin, VolumeMixin, MaMixin):
|
|
149
|
+
limit_candle = 400
|
|
150
|
+
limit_wick = 2_000
|
|
151
|
+
|
|
152
|
+
def set_collections(self, ind_start, *, ind_end):
|
|
153
|
+
if ind_start < 0:
|
|
154
|
+
ind_start = 0
|
|
155
|
+
indsub = ind_end - ind_start
|
|
156
|
+
# print(f'{indsub=:,}')
|
|
157
|
+
|
|
158
|
+
if not self.limit_candle or indsub < self.limit_candle:
|
|
159
|
+
# print('candle')
|
|
160
|
+
self._set_candle_collection_segments(ind_start, ind_end=ind_end)
|
|
161
|
+
self._set_volume_collection_segments(ind_start, ind_end=ind_end)
|
|
162
|
+
else:
|
|
163
|
+
self._set_volume_collection_wick_segments(ind_start, ind_end=ind_end)
|
|
164
|
+
|
|
165
|
+
if not self.limit_wick or indsub < self.limit_wick:
|
|
166
|
+
# print('wick')
|
|
167
|
+
self._set_candle_collection_wick_segments(ind_start, ind_end=ind_end)
|
|
168
|
+
else:
|
|
169
|
+
# print('line')
|
|
170
|
+
self.set_candle_collection_priceline_segments(ind_start, ind_end=ind_end)
|
|
171
|
+
|
|
172
|
+
self._set_ma_collection_segments(ind_start, ind_end=ind_end)
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AxisMixin(LimMixin, CollectionMixin):
|
|
177
|
+
limit_candle = 400
|
|
178
|
+
limit_wick = 2_000
|
|
179
|
+
|
|
180
|
+
ax_price: Axes
|
|
181
|
+
ax_volume: Axes
|
|
182
|
+
|
|
183
|
+
vxmin: int
|
|
184
|
+
vxmax: int
|
|
185
|
+
price_ymin: int
|
|
186
|
+
price_ymax: int
|
|
187
|
+
volume_ymax: int
|
|
188
|
+
|
|
189
|
+
def axis(self, xmin, *, xmax):
|
|
190
|
+
"조회 영역 변경"
|
|
191
|
+
# print('base axis')
|
|
192
|
+
self.set_collections(xmin, ind_end=xmax+1)
|
|
193
|
+
|
|
194
|
+
self.vxmin, self.vxmax = (xmin, xmax+1)
|
|
195
|
+
ind_start, ind_end = self._get_indices(xmin, ind_end=xmax)
|
|
196
|
+
|
|
197
|
+
self.price_ymin, self.price_ymax = self._get_price_ylim(ind_start, ind_end=ind_end)
|
|
198
|
+
|
|
199
|
+
# 주가 차트 xlim
|
|
200
|
+
self.ax_price.set_xlim(self.vxmin, self.vxmax)
|
|
201
|
+
# 주가 차트 ylim
|
|
202
|
+
self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
|
|
203
|
+
|
|
204
|
+
# 거래량 차트 xlim
|
|
205
|
+
self.ax_volume.set_xlim(self.vxmin, self.vxmax)
|
|
206
|
+
self.volume_ymax = 1
|
|
207
|
+
if self.key_volume:
|
|
208
|
+
_, self.volume_ymax = self._get_volume_ylim(ind_start, ind_end=ind_end)
|
|
209
|
+
# 거래량 차트 ylim
|
|
210
|
+
self.ax_volume.set_ylim(0, self.volume_ymax)
|
|
211
|
+
|
|
212
|
+
self.set_xtick_labels(xmin, xmax=xmax)
|
|
213
|
+
return
|
|
214
|
+
|
|
215
|
+
def set_xtick_labels(self, xmin, *, xmax):
|
|
216
|
+
# x축에 일부 date 표시하기
|
|
217
|
+
xsub = xmax - xmin
|
|
218
|
+
xmiddle = xmin + (xsub // 2)
|
|
219
|
+
indices = [idx for idx in (xmin, xmiddle, xmax) if 0 <= idx and idx <= self.index_list[-1]]
|
|
220
|
+
# print(f'{xmiddle=}')
|
|
221
|
+
# print(f'{indices=}')
|
|
222
|
+
|
|
223
|
+
m = (xmiddle - xmin) // 2
|
|
224
|
+
ind_end = self.index_list[-1]
|
|
225
|
+
aligns = ['left', 'center', 'center']
|
|
226
|
+
if len(indices) < 2:
|
|
227
|
+
if xmin < 0 and self.index_list[-1] < xmax:
|
|
228
|
+
indices = [0, xmiddle, ind_end]
|
|
229
|
+
else:
|
|
230
|
+
if xmin <= 0:
|
|
231
|
+
if m <= xmax:
|
|
232
|
+
aligns = aligns[-2:]
|
|
233
|
+
indices = [0, xmax]
|
|
234
|
+
else:
|
|
235
|
+
aligns = aligns[-1:]
|
|
236
|
+
indices = [0]
|
|
237
|
+
else:
|
|
238
|
+
if xmin+m <= ind_end:
|
|
239
|
+
aligns = aligns[:2]
|
|
240
|
+
indices = [xmin, ind_end]
|
|
241
|
+
else:
|
|
242
|
+
aligns = aligns[:1]
|
|
243
|
+
indices = [ind_end]
|
|
244
|
+
elif len(indices) < 3:
|
|
245
|
+
if xmin < 0:
|
|
246
|
+
if 0 <= (xmiddle - m):
|
|
247
|
+
indices = [0] + indices
|
|
248
|
+
else:
|
|
249
|
+
aligns = aligns[-2:]
|
|
250
|
+
indices[0] = 0
|
|
251
|
+
else:
|
|
252
|
+
if (xmiddle + m) <= ind_end:
|
|
253
|
+
indices.append(ind_end)
|
|
254
|
+
else:
|
|
255
|
+
aligns = aligns[:2]
|
|
256
|
+
indices[-1] = ind_end
|
|
257
|
+
|
|
258
|
+
date_list = [self.df.iloc[idx]['date'] for idx in indices]
|
|
259
|
+
# 라벨을 노출할 틱 위치, major tick과 겹쳐서 무시되는 것 방지
|
|
260
|
+
self.ax_volume.set_xticks([idx+0.501 for idx in indices], minor=True)
|
|
261
|
+
# 라벨
|
|
262
|
+
self.ax_volume.set_xticklabels(date_list, minor=True)
|
|
263
|
+
labels: list[Text] = self.ax_volume.get_xticklabels(minor=True)
|
|
264
|
+
for label, align in zip(labels, aligns):
|
|
265
|
+
# 라벨 텍스트 정렬
|
|
266
|
+
label.set_horizontalalignment(align)
|
|
267
|
+
return
|
|
268
|
+
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from ..._config import ConfigData
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Base:
|
|
9
|
+
CONFIG: ConfigData
|
|
10
|
+
df: pd.DataFrame
|
|
11
|
+
|
|
12
|
+
key_date = 'date'
|
|
13
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
14
|
+
key_volume = 'volume'
|
|
15
|
+
|
|
16
|
+
ax_price: Axes
|
|
17
|
+
index_list: list[int] = []
|
|
18
|
+
|
|
19
|
+
set_segments: callable
|
|
20
|
+
axis: callable
|
|
21
|
+
|
|
22
|
+
def get_default_xlim(self):
|
|
23
|
+
"""
|
|
24
|
+
get_default_xlim.
|
|
25
|
+
|
|
26
|
+
space = int(self.index_list[-1] / 20)
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
(int, int): (-space, self.index_list[-1]+space)
|
|
30
|
+
"""
|
|
31
|
+
# print(f'{self.index_list[-1]=}')
|
|
32
|
+
space = int(self.index_list[-1] / 20)
|
|
33
|
+
return (-space, self.index_list[-1]+space)
|
|
34
|
+
|
|
35
|
+
def set_variables(self):
|
|
36
|
+
self.index_list.clear()
|
|
37
|
+
self.index_list = self.df.index.tolist()
|
|
38
|
+
self.xmin, self.xmax = (0, self.index_list[-1])
|
|
39
|
+
|
|
40
|
+
self.chart_price_ymax = round(self.df['high'].max() * 1.3, self.CONFIG.UNIT.digit+2)
|
|
41
|
+
if self.key_volume:
|
|
42
|
+
self.chart_volume_ymax = round(self.df['volume'].max() * 1.3, self.CONFIG.UNIT.digit_volume+2)
|
|
43
|
+
else:
|
|
44
|
+
self.chart_volume_ymax = 10
|
|
45
|
+
|
|
46
|
+
if not self.CONFIG.MA.ma_list:
|
|
47
|
+
self.CONFIG.MA.ma_list = []
|
|
48
|
+
else:
|
|
49
|
+
self.CONFIG.MA.ma_list = sorted(self.CONFIG.MA.ma_list)
|
|
50
|
+
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
def set_data(self, df: pd.DataFrame, *, change_xlim=True):
|
|
54
|
+
"""
|
|
55
|
+
`if change_xlim`: change xlim with `self.get_default_xlim()` value
|
|
56
|
+
|
|
57
|
+
`if not change_xlim`: Keep the current xlim
|
|
58
|
+
"""
|
|
59
|
+
self.df = self._convert_df(df)
|
|
60
|
+
|
|
61
|
+
self._add_columns()
|
|
62
|
+
# print(f'{self.df.columns=}')
|
|
63
|
+
|
|
64
|
+
self.set_variables()
|
|
65
|
+
|
|
66
|
+
self.set_segments()
|
|
67
|
+
|
|
68
|
+
if change_xlim:
|
|
69
|
+
xmin, xmax = self.get_default_xlim()
|
|
70
|
+
self.axis(xmin, xmax=xmax)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def _convert_df(self, df: pd.DataFrame):
|
|
74
|
+
keys = {
|
|
75
|
+
self.key_date: 'date',
|
|
76
|
+
self.key_open: 'open',
|
|
77
|
+
self.key_high: 'high',
|
|
78
|
+
self.key_low: 'low',
|
|
79
|
+
self.key_close: 'close',
|
|
80
|
+
self.key_volume: 'volume',
|
|
81
|
+
}
|
|
82
|
+
df.rename(columns=keys, inplace=True)
|
|
83
|
+
|
|
84
|
+
# df column 추출
|
|
85
|
+
if self.key_volume:
|
|
86
|
+
df = df[['date', 'open', 'high', 'low', 'close', 'volume']].copy()
|
|
87
|
+
else:
|
|
88
|
+
df = df[['date', 'open', 'high', 'low', 'close',]].copy()
|
|
89
|
+
df['volume'] = 0
|
|
90
|
+
df.loc[:, 'ymax_volume'] = df['volume'] * 1.2
|
|
91
|
+
|
|
92
|
+
# 오름차순 정렬
|
|
93
|
+
df = df.sort_values(['date'])
|
|
94
|
+
df = df.reset_index(drop=True)
|
|
95
|
+
|
|
96
|
+
return df
|
|
97
|
+
|
|
98
|
+
def _add_columns(self):
|
|
99
|
+
# 전일 종가 추가
|
|
100
|
+
self.df['pre_close'] = self.df['close'].shift(1).fillna(0)
|
|
101
|
+
# 거래정지인 경우 전일종가 적용
|
|
102
|
+
self.df.loc[self.df['close'] == 0, 'close'] = self.df['pre_close']
|
|
103
|
+
# 종가만 유효한 경우 종가로 통일
|
|
104
|
+
self.df.loc[(self.df['close'] != 0) & (self.df['open'] == 0), ['open', 'high', 'low']] = self.df['close']
|
|
105
|
+
|
|
106
|
+
# 가격이동평균선 계산
|
|
107
|
+
for ma in self.CONFIG.MA.ma_list:
|
|
108
|
+
self.df[f'ma{ma}'] = self.df['close'].rolling(ma).mean()
|
|
109
|
+
|
|
110
|
+
# 세그먼트 생성을 위한 column 추가
|
|
111
|
+
self.df['x'] = self.df.index + 0.5
|
|
112
|
+
self.df['left_candle'] = self.df['x'] - self.CONFIG.CANDLE.half_width
|
|
113
|
+
self.df['right_candle'] = self.df['x'] + self.CONFIG.CANDLE.half_width
|
|
114
|
+
self.df['left_volume'] = self.df['x'] - self.CONFIG.VOLUME.half_width
|
|
115
|
+
self.df['right_volume'] = self.df['x'] + self.CONFIG.VOLUME.half_width
|
|
116
|
+
self.df['zero'] = 0
|
|
117
|
+
|
|
118
|
+
self.df['is_up'] = np.where(self.df['open'] < self.df['close'], True, False)
|
|
119
|
+
self.df['top_candle'] = np.where(self.df['is_up'], self.df['close'], self.df['open'])
|
|
120
|
+
self.df['bottom_candle'] = np.where(self.df['is_up'], self.df['open'], self.df['close'])
|
|
121
|
+
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class DataMixin(Base):
|
|
126
|
+
key_date = 'date'
|
|
127
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
128
|
+
key_volume = 'volume'
|
|
129
|
+
|
|
130
|
+
index_list: list[int] = []
|
|
131
|
+
|
|
132
|
+
df: pd.DataFrame
|
|
133
|
+
|
|
134
|
+
chart_price_ymax: float
|
|
135
|
+
chart_volume_ymax: float
|
|
136
|
+
xmin: int
|
|
137
|
+
xmax: int
|
|
138
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
name_pkg = 'seolpyo_mplchart'
|
|
4
|
+
path_pkg = Path(__file__)
|
|
5
|
+
while path_pkg.name != name_pkg:
|
|
6
|
+
path_pkg = path_pkg.parent
|
|
7
|
+
sys.path = [path_pkg.parent.__str__()] + sys.path
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
|
|
14
|
+
from seolpyo_mplchart._utils.theme import set_theme
|
|
15
|
+
from seolpyo_mplchart._chart.base import Chart
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
path_file = path_pkg / 'sample' / 'samsung.txt'
|
|
19
|
+
with open(path_file, 'r', encoding='utf-8') as txt:
|
|
20
|
+
data = json.load(txt)
|
|
21
|
+
df = pd.DataFrame(data[:])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class C(Chart):
|
|
25
|
+
# limit_wick = 200
|
|
26
|
+
t = 'light'
|
|
27
|
+
# watermark = ''
|
|
28
|
+
def __init__(self):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.figure.canvas.mpl_connect('button_press_event', lambda x: self.theme(x))
|
|
31
|
+
|
|
32
|
+
def theme(self, e):
|
|
33
|
+
btn = getattr(e, 'button')
|
|
34
|
+
# print(f'{str(btn)=}')
|
|
35
|
+
if str(btn) == '3':
|
|
36
|
+
# print('refresh')
|
|
37
|
+
if self.t == 'light':
|
|
38
|
+
self.t = 'dark'
|
|
39
|
+
else:
|
|
40
|
+
self.t = 'light'
|
|
41
|
+
# print(f'{self.t=}')
|
|
42
|
+
self.CONFIG = set_theme(self.CONFIG, theme=self.t)
|
|
43
|
+
self.refresh()
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run():
|
|
48
|
+
chart = C()
|
|
49
|
+
chart.set_data(df)
|
|
50
|
+
plt.show()
|
|
51
|
+
plt.close()
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == '__main__':
|
|
56
|
+
run()
|
|
57
|
+
|
|
58
|
+
|