seolpyo-mplchart 2.0.0.3__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 +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 +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/_config/candle.py +1 -0
- seolpyo_mplchart/_config/figure.py +3 -4
- seolpyo_mplchart/_config/ma.py +2 -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/volume.py +1 -0
- 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 +5 -5
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/METADATA +21 -13
- seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -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.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -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
|
+
|
|
@@ -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
|
+
|