seolpyo-mplchart 0.1.3.4__py3-none-any.whl → 1.0.1__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.
Potentially problematic release.
This version of seolpyo-mplchart might be problematic. Click here for more details.
- seolpyo_mplchart/__init__.py +221 -70
- seolpyo_mplchart/base.py +70 -67
- seolpyo_mplchart/cursor.py +246 -272
- seolpyo_mplchart/draw.py +389 -238
- seolpyo_mplchart/slider.py +414 -405
- seolpyo_mplchart/test.py +5 -32
- seolpyo_mplchart/utils.py +15 -4
- {seolpyo_mplchart-0.1.3.4.dist-info → seolpyo_mplchart-1.0.1.dist-info}/METADATA +5 -9
- seolpyo_mplchart-1.0.1.dist-info/RECORD +13 -0
- seolpyo_mplchart-0.1.3.4.dist-info/RECORD +0 -13
- {seolpyo_mplchart-0.1.3.4.dist-info → seolpyo_mplchart-1.0.1.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-0.1.3.4.dist-info → seolpyo_mplchart-1.0.1.dist-info}/top_level.txt +0 -0
seolpyo_mplchart/draw.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from matplotlib.backend_bases import PickEvent
|
|
2
2
|
from matplotlib.collections import LineCollection
|
|
3
3
|
from matplotlib.lines import Line2D
|
|
4
|
+
from matplotlib.text import Text
|
|
4
5
|
import numpy as np
|
|
5
6
|
import pandas as pd
|
|
6
7
|
|
|
@@ -9,208 +10,353 @@ from .base import Base
|
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class Mixin:
|
|
13
|
+
def add_collection(self):
|
|
14
|
+
"This method work when ```__init__()``` run."
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
def draw_artist(self):
|
|
18
|
+
"This method work before ```figure.canvas.blit()```."
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def generate_data(self):
|
|
22
|
+
"This method work before create segments."
|
|
23
|
+
return
|
|
24
|
+
|
|
12
25
|
def on_draw(self, e):
|
|
13
|
-
"
|
|
26
|
+
"If draw event active, This method work."
|
|
14
27
|
return
|
|
28
|
+
|
|
15
29
|
def on_pick(self, e):
|
|
16
|
-
"
|
|
30
|
+
"If draw pick active, This method work."
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CollectionMixin(Base):
|
|
35
|
+
facecolor_volume, edgecolor_volume = ('#1f77b4', 'k')
|
|
36
|
+
watermark = 'seolpyo mplchart'
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args, **kwargs):
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
self._add_collection()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def _add_collection(self):
|
|
45
|
+
self.collection_ma = LineCollection([], animated=True, linewidths=1)
|
|
46
|
+
self.ax_price.add_collection(self.collection_ma)
|
|
47
|
+
|
|
48
|
+
self.collection_candle = LineCollection([], animated=True, linewidths=0.8)
|
|
49
|
+
self.ax_price.add_collection(self.collection_candle)
|
|
50
|
+
|
|
51
|
+
self.collection_volume = LineCollection([], animated=True, linewidths=1)
|
|
52
|
+
self.ax_volume.add_collection(self.collection_volume)
|
|
53
|
+
|
|
54
|
+
x = (self.adjust['right']-self.adjust['left']) / 2
|
|
55
|
+
self.text_watermark = Text(
|
|
56
|
+
x=x, y=0.51, text=self.watermark,
|
|
57
|
+
animated=True,
|
|
58
|
+
fontsize=20, color=self.color_tick_label, alpha=0.2,
|
|
59
|
+
horizontalalignment='center', verticalalignment='center',
|
|
60
|
+
)
|
|
61
|
+
self.figure.add_artist(self.text_watermark)
|
|
17
62
|
return
|
|
18
63
|
|
|
19
64
|
|
|
20
|
-
_set_key = {'
|
|
65
|
+
_set_key = {'_x', '_left', '_right', '_volleft', '_volright', '_top', '_bottom', '_pre', '_zero', '_volymax',}
|
|
21
66
|
|
|
22
|
-
class DataMixin(
|
|
67
|
+
class DataMixin(CollectionMixin):
|
|
23
68
|
df: pd.DataFrame
|
|
69
|
+
|
|
24
70
|
date = 'date'
|
|
25
71
|
Open, high, low, close = ('open', 'high', 'low', 'close')
|
|
26
72
|
volume = 'volume'
|
|
27
|
-
|
|
28
|
-
_visible_ma = set()
|
|
29
|
-
label_ma = '{}일선'
|
|
30
73
|
list_ma = (5, 20, 60, 120, 240)
|
|
31
|
-
# https://matplotlib.org/stable/gallery/color/named_colors.html
|
|
32
|
-
list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
|
|
33
74
|
|
|
75
|
+
candle_width_half, volume_width_half = (0.24, 0.36)
|
|
34
76
|
color_up, color_down = ('#fe3032', '#0095ff')
|
|
35
77
|
color_flat = 'k'
|
|
36
78
|
color_up_down, color_down_up = ('w', 'w')
|
|
37
|
-
colors_volume = '#1f77b4'
|
|
38
79
|
|
|
39
|
-
|
|
80
|
+
def _generate_data(self, df: pd.DataFrame, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__):
|
|
81
|
+
self._validate_column_key()
|
|
40
82
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
|
|
45
|
-
if i != 'date':
|
|
46
|
-
dtype = df[k].dtype
|
|
47
|
-
if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)):
|
|
48
|
-
raise TypeError(f'column dtype must be one of "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
|
|
83
|
+
# 오름차순 정렬
|
|
84
|
+
if sort_df: df = df.sort_values([self.date])
|
|
85
|
+
df = df.reset_index()
|
|
49
86
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
df = df.sort_values([self.date]).reset_index()
|
|
87
|
+
self.list_index = df.index.tolist()
|
|
88
|
+
self.xmin, self.xmax = (0, self.list_index[-1])
|
|
53
89
|
|
|
54
90
|
if not self.list_ma: self.list_ma = tuple()
|
|
55
|
-
if calc_ma:
|
|
56
|
-
for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
|
|
57
91
|
else:
|
|
92
|
+
self.list_ma = sorted(self.list_ma)
|
|
93
|
+
# 가격이동평균선 계산
|
|
94
|
+
if calc_ma:
|
|
95
|
+
for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
|
|
96
|
+
else:
|
|
97
|
+
set_key = set(self.df.keys())
|
|
98
|
+
for i in self.list_ma:
|
|
99
|
+
key = f'ma{i}'
|
|
100
|
+
if key not in set_key:
|
|
101
|
+
raise KeyError(f'"{key}" column not found.\nset calc_ma=True or add "{key}" column.')
|
|
102
|
+
|
|
103
|
+
df['_x'] = df.index + 0.5
|
|
104
|
+
df['_left'] = df['_x'] - self.candle_width_half
|
|
105
|
+
df['_right'] = df['_x'] + self.candle_width_half
|
|
106
|
+
df['_volleft'] = df['_x'] - self.volume_width_half
|
|
107
|
+
df['_volright'] = df['_x'] + self.volume_width_half
|
|
108
|
+
df.loc[:, '_zero'] = 0
|
|
109
|
+
|
|
110
|
+
df['_top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
|
|
111
|
+
df['_top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
|
|
112
|
+
df['_bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
|
|
113
|
+
df['_bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
|
|
114
|
+
|
|
115
|
+
df['_pre'] = df[self.close].shift(1)
|
|
116
|
+
if self.volume: df['_volymax'] = df[self.volume] * 1.2
|
|
117
|
+
|
|
118
|
+
if not set_candlecolor:
|
|
119
|
+
keys = set(df.keys())
|
|
120
|
+
for i in ('facecolor', 'edgecolor', 'volumefacecolor',):
|
|
121
|
+
if i not in keys:
|
|
122
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_candlecolor=True.')
|
|
123
|
+
else:
|
|
124
|
+
# 양봉
|
|
125
|
+
df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
|
|
126
|
+
if self.color_up != self.color_down:
|
|
127
|
+
# 음봉
|
|
128
|
+
df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
|
|
129
|
+
if self.color_up != self.color_flat:
|
|
130
|
+
# 보합
|
|
131
|
+
df.loc[df[self.close] == df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
|
|
132
|
+
if self.color_up != self.color_up_down:
|
|
133
|
+
# 양봉(비우기)
|
|
134
|
+
df.loc[(df['facecolor'] == self.color_up) & (df[self.close] < df['_pre']), 'facecolor'] = self.color_up_down
|
|
135
|
+
if self.color_down != self.color_down_up:
|
|
136
|
+
# 음봉(비우기)
|
|
137
|
+
df.loc[(df['facecolor'] == self.color_down) & (df['_pre'] < df[self.close]), ['facecolor']] = self.color_down_up
|
|
138
|
+
|
|
139
|
+
if not set_volumecolor:
|
|
58
140
|
keys = set(df.keys())
|
|
59
|
-
for i in
|
|
60
|
-
if
|
|
61
|
-
raise Exception(f'"
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
df['right'] = df['x'] + self.candlewidth_half
|
|
66
|
-
df['vleft'] = df['x'] - self.volumewidth_half
|
|
67
|
-
df['vright'] = df['x'] + self.volumewidth_half
|
|
68
|
-
|
|
69
|
-
df['top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
|
|
70
|
-
df['top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
|
|
71
|
-
df['bottom'] = np.where(df[self.Open] <= df[self.close], df[self.Open], df[self.close])
|
|
72
|
-
df['bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
|
|
73
|
-
|
|
74
|
-
# 양봉
|
|
75
|
-
df.loc[:, ['zero', 'facecolor', 'edgecolor']] = (0, self.color_up, self.color_up)
|
|
76
|
-
if self.color_up != self.color_down:
|
|
77
|
-
# 음봉
|
|
78
|
-
df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
|
|
79
|
-
if self.color_up != self.color_flat:
|
|
80
|
-
# 보합
|
|
81
|
-
df.loc[df[self.close] == df[self.Open], ['facecolor', 'edgecolor']] = (self.color_flat, self.color_flat)
|
|
82
|
-
if self.color_up != self.color_up_down:
|
|
83
|
-
# 양봉(비우기)
|
|
84
|
-
df.loc[(df['facecolor'] == self.color_up) & (df[self.close] < df[self.close].shift(1)), 'facecolor'] = self.color_up_down
|
|
85
|
-
if self.color_down != self.color_down_up:
|
|
86
|
-
# 음봉(비우기)
|
|
87
|
-
df.loc[(df['facecolor'] == self.color_down) & (df[self.close].shift(1) < df[self.close]), ['facecolor']] = self.color_down_up
|
|
141
|
+
for i in ('volumefacecolor', 'volumeedgecolor',):
|
|
142
|
+
if i not in keys:
|
|
143
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set set_volumecolor=True.')
|
|
144
|
+
else:
|
|
145
|
+
# 거래량
|
|
146
|
+
df.loc[:, ['volumefacecolor', 'volumeedgecolor']] = (self.facecolor_volume, self.edgecolor_volume)
|
|
88
147
|
|
|
89
148
|
self.df = df
|
|
90
149
|
return
|
|
91
150
|
|
|
92
|
-
|
|
93
|
-
|
|
151
|
+
def _validate_column_key(self):
|
|
152
|
+
for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
|
|
153
|
+
v = getattr(self, i)
|
|
154
|
+
if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
|
|
155
|
+
return
|
|
94
156
|
|
|
95
|
-
_masegment, _macolors = ({}, {})
|
|
96
157
|
|
|
97
|
-
|
|
98
|
-
|
|
158
|
+
class SegmentMixin(DataMixin):
|
|
159
|
+
_visible_ma = set()
|
|
99
160
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
limit_candle = 800
|
|
162
|
+
limit_wick = 4_000
|
|
163
|
+
limit_volume = 800
|
|
103
164
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
165
|
+
color_priceline = 'k'
|
|
166
|
+
format_ma = '{}일선'
|
|
167
|
+
# https://matplotlib.org/stable/gallery/color/named_colors.html
|
|
168
|
+
list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
|
|
107
169
|
|
|
108
|
-
def
|
|
109
|
-
|
|
110
|
-
self.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
def _set_collection(self):
|
|
126
|
-
candleseg = self.df[[
|
|
127
|
-
'x', self.high,
|
|
128
|
-
'x', 'top',
|
|
129
|
-
'left', 'top',
|
|
130
|
-
'left', 'bottom',
|
|
131
|
-
'x', 'bottom',
|
|
132
|
-
'x', self.low,
|
|
133
|
-
'x', 'bottom',
|
|
134
|
-
'right', 'bottom',
|
|
135
|
-
'right', 'top',
|
|
136
|
-
'x', 'top',
|
|
170
|
+
def _get_segments(self):
|
|
171
|
+
# 캔들 세그먼트
|
|
172
|
+
segment_candle = self.df[[
|
|
173
|
+
'_x', self.high,
|
|
174
|
+
'_x', '_top',
|
|
175
|
+
'_left', '_top',
|
|
176
|
+
'_left', '_bottom',
|
|
177
|
+
'_x', '_bottom',
|
|
178
|
+
'_x', self.low,
|
|
179
|
+
'_x', '_bottom',
|
|
180
|
+
'_right', '_bottom',
|
|
181
|
+
'_right', '_top',
|
|
182
|
+
'_x', '_top',
|
|
183
|
+
'_x', self.high,
|
|
184
|
+
'_x', '_top',
|
|
137
185
|
]].values
|
|
138
|
-
|
|
186
|
+
self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
|
|
139
187
|
|
|
140
|
-
|
|
141
|
-
self.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
volseg = self.df[[
|
|
145
|
-
'left', 'zero',
|
|
146
|
-
'left', self.volume,
|
|
147
|
-
'right', self.volume,
|
|
148
|
-
'right', 'zero',
|
|
188
|
+
# 심지 세그먼트
|
|
189
|
+
segment_wick = self.df[[
|
|
190
|
+
'_x', self.high,
|
|
191
|
+
'_x', self.low,
|
|
149
192
|
]].values
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
self.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
|
|
194
|
+
|
|
195
|
+
# 종가 세그먼트
|
|
196
|
+
segment_priceline = segment_wick = self.df[['_x', self.close]].values
|
|
197
|
+
self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
|
|
198
|
+
|
|
199
|
+
self.facecolor_candle = self.df['facecolor'].values
|
|
200
|
+
self.edgecolor_candle = self.df['edgecolor'].values
|
|
201
|
+
|
|
202
|
+
if self.volume:
|
|
203
|
+
# 거래량 바 세그먼트
|
|
204
|
+
segment_volume = self.df[[
|
|
205
|
+
'_volleft', '_zero',
|
|
206
|
+
'_volleft', self.volume,
|
|
207
|
+
'_volright', self.volume,
|
|
208
|
+
'_volright', '_zero',
|
|
209
|
+
]].values
|
|
210
|
+
self.segment_volume = segment_volume.reshape(segment_volume.shape[0], 4, 2)
|
|
211
|
+
|
|
212
|
+
# 거래량 심지 세그먼트
|
|
213
|
+
segment_volume_wick = self.df[[
|
|
214
|
+
'_x', '_zero',
|
|
215
|
+
'_x', self.volume,
|
|
216
|
+
]].values
|
|
217
|
+
self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
|
|
218
|
+
|
|
219
|
+
self.facecolor_volume = self.df['volumefacecolor'].values
|
|
220
|
+
self.edgecolor_volume = self.df['volumeedgecolor'].values
|
|
221
|
+
|
|
222
|
+
self._get_ma_segment()
|
|
174
223
|
return
|
|
175
224
|
|
|
176
|
-
def
|
|
225
|
+
def _get_ma_segment(self):
|
|
177
226
|
# 기존 legend 제거
|
|
178
227
|
legends = self.ax_legend.get_legend()
|
|
179
228
|
if legends: legends.remove()
|
|
180
229
|
|
|
181
|
-
self._masegment.clear(), self._macolors.clear()
|
|
182
|
-
handles, labels = ([], [])
|
|
183
230
|
self._visible_ma.clear()
|
|
231
|
+
|
|
232
|
+
if not self.list_ma: return
|
|
233
|
+
|
|
234
|
+
list_handle, list_label, list_color = ([], [], [])
|
|
235
|
+
arr = (0, 1)
|
|
184
236
|
for n, i in enumerate(self.list_ma):
|
|
185
237
|
try: c = self.list_macolor[n]
|
|
186
|
-
except: c = self.
|
|
187
|
-
|
|
188
|
-
# seg = self.df['x', f'ma{i}'].values
|
|
189
|
-
seg = self.df.loc[self.df[f'ma{i}'] != np.nan, ['x', f'ma{i}']].values
|
|
190
|
-
# print(f'{seg[:5]=}')
|
|
191
|
-
self._masegment[i] = seg
|
|
238
|
+
except: c = self.color_priceline
|
|
239
|
+
list_color.append(c)
|
|
192
240
|
|
|
193
|
-
|
|
194
|
-
|
|
241
|
+
list_handle.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
|
|
242
|
+
list_label.append(self.format_ma.format(i))
|
|
195
243
|
|
|
196
244
|
self._visible_ma.add(i)
|
|
197
245
|
|
|
246
|
+
# 주가 차트 가격이동평균선
|
|
247
|
+
key_ma = []
|
|
248
|
+
for i in reversed(self.list_ma):
|
|
249
|
+
key_ma.append('_x')
|
|
250
|
+
key_ma.append(f'ma{i}')
|
|
251
|
+
segment_ma = self.df[key_ma].values
|
|
252
|
+
self.segment_ma = segment_ma.reshape(segment_ma.shape[0], len(self.list_ma), 2).swapaxes(0, 1)
|
|
253
|
+
self.edgecolor_ma = list(reversed(list_color))
|
|
254
|
+
|
|
198
255
|
# 가격이동평균선 legend 생성
|
|
199
|
-
if
|
|
200
|
-
legends = self.ax_legend.legend(
|
|
256
|
+
if list_handle:
|
|
257
|
+
legends = self.ax_legend.legend(
|
|
258
|
+
list_handle, list_label, loc='lower left', ncol=10,
|
|
259
|
+
facecolor=self.color_background, edgecolor=self.color_tick,
|
|
260
|
+
labelcolor=self.color_tick_label,
|
|
261
|
+
)
|
|
262
|
+
for i in legends.legend_handles: i.set_picker(5)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
def _set_segments(self, index_start, index_end, simpler, set_ma):
|
|
266
|
+
indsub = index_end - index_start
|
|
267
|
+
if index_start < 0: index_start = 0
|
|
268
|
+
if index_end < 1: index_end = 1
|
|
201
269
|
|
|
202
|
-
|
|
203
|
-
|
|
270
|
+
if indsub < self.limit_candle:
|
|
271
|
+
self._set_candle_segments(index_start, index_end)
|
|
272
|
+
elif indsub < self.limit_wick:
|
|
273
|
+
self._set_wick_segments(index_start, index_end, simpler)
|
|
274
|
+
else:
|
|
275
|
+
self._set_line_segments(index_start, index_end, simpler, set_ma)
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
279
|
+
index_end += 1
|
|
280
|
+
|
|
281
|
+
self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
|
|
282
|
+
self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
|
|
283
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
284
|
+
|
|
285
|
+
if self.volume:
|
|
286
|
+
self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
|
|
287
|
+
self.collection_volume.set_linewidth(0.7)
|
|
288
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
289
|
+
self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
|
|
290
|
+
|
|
291
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
292
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
296
|
+
index_end += 1
|
|
297
|
+
|
|
298
|
+
self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
|
|
299
|
+
self.collection_candle.set_facecolor([])
|
|
300
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
301
|
+
|
|
302
|
+
if self.volume:
|
|
303
|
+
seg = self.segment_volume_wick[index_start:index_end]
|
|
304
|
+
if simpler:
|
|
305
|
+
values = seg[:, 1, 1]
|
|
306
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
307
|
+
seg = seg[top_index]
|
|
308
|
+
self.collection_volume.set_segments(seg)
|
|
309
|
+
self.collection_volume.set_linewidth(1.3)
|
|
310
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
311
|
+
self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
|
|
312
|
+
|
|
313
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
314
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
318
|
+
index_end += 1
|
|
319
|
+
|
|
320
|
+
self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
|
|
321
|
+
self.collection_candle.set_facecolor([])
|
|
322
|
+
self.collection_candle.set_edgecolor(self.color_priceline)
|
|
323
|
+
|
|
324
|
+
if self.volume:
|
|
325
|
+
seg = self.segment_volume_wick[index_start:index_end]
|
|
326
|
+
if simpler:
|
|
327
|
+
values = seg[:, 1, 1]
|
|
328
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
329
|
+
seg = seg[top_index]
|
|
330
|
+
self.collection_volume.set_segments(seg)
|
|
331
|
+
self.collection_volume.set_linewidth(1.3)
|
|
332
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
333
|
+
self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
|
|
334
|
+
|
|
335
|
+
if not set_ma: self.collection_ma.set_segments([])
|
|
336
|
+
else:
|
|
337
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
338
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
339
|
+
return
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class EventMixin(SegmentMixin):
|
|
343
|
+
def __init__(self, *args, **kwargs):
|
|
344
|
+
super().__init__(*args, **kwargs)
|
|
345
|
+
|
|
346
|
+
self._connect_event()
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
def _connect_event(self):
|
|
350
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
204
351
|
return
|
|
205
352
|
|
|
206
353
|
def _on_pick(self, e):
|
|
207
354
|
self._pick_ma_action(e)
|
|
208
|
-
|
|
209
|
-
return self._draw()
|
|
355
|
+
return
|
|
210
356
|
|
|
211
357
|
def _pick_ma_action(self, e: PickEvent):
|
|
212
358
|
handle = e.artist
|
|
213
|
-
if
|
|
359
|
+
if handle.get_alpha() == 0.2:
|
|
214
360
|
visible = True
|
|
215
361
|
handle.set_alpha(1.0)
|
|
216
362
|
else:
|
|
@@ -218,130 +364,154 @@ class CollectionMixin(DataMixin):
|
|
|
218
364
|
handle.set_alpha(0.2)
|
|
219
365
|
|
|
220
366
|
n = int(handle.get_label())
|
|
221
|
-
|
|
222
367
|
if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
|
|
223
368
|
else: self._visible_ma = {i for i in self._visible_ma if i != n}
|
|
224
369
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
370
|
+
alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
|
|
371
|
+
self.collection_ma.set_alpha(alphas)
|
|
372
|
+
|
|
373
|
+
self._draw()
|
|
228
374
|
return
|
|
229
375
|
|
|
230
376
|
def _draw(self):
|
|
231
|
-
|
|
232
|
-
self.canvas = self.fig.canvas
|
|
233
|
-
self.canvas.draw()
|
|
377
|
+
self.figure.canvas.draw()
|
|
234
378
|
return
|
|
235
379
|
|
|
236
380
|
|
|
237
|
-
class
|
|
238
|
-
background = None
|
|
381
|
+
class DrawMixin(EventMixin):
|
|
239
382
|
candle_on_ma = True
|
|
240
383
|
|
|
241
|
-
|
|
384
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
|
|
385
|
+
self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
|
|
386
|
+
self._get_segments()
|
|
242
387
|
|
|
243
|
-
|
|
244
|
-
|
|
388
|
+
vmin, vmax = self.get_default_lim()
|
|
389
|
+
self._set_lim(vmin, vmax)
|
|
245
390
|
return
|
|
246
391
|
|
|
247
392
|
def _connect_event(self):
|
|
248
393
|
super()._connect_event()
|
|
249
|
-
self.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
def _create_background(self):
|
|
253
|
-
if self._creating_background: return
|
|
254
|
-
|
|
255
|
-
if self.fig.canvas is not self.canvas:
|
|
256
|
-
self.canvas = self.fig.canvas
|
|
257
|
-
|
|
258
|
-
self._creating_background = True
|
|
259
|
-
self._copy_bbox()
|
|
260
|
-
self._creating_background = False
|
|
394
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
261
395
|
return
|
|
262
396
|
|
|
263
|
-
def
|
|
397
|
+
def _on_draw(self, e):
|
|
264
398
|
self._draw_artist()
|
|
265
|
-
self.
|
|
399
|
+
self._blit()
|
|
266
400
|
return
|
|
267
401
|
|
|
268
402
|
def _draw_artist(self):
|
|
269
|
-
renderer = self.canvas.renderer
|
|
270
|
-
|
|
271
|
-
self.ax_slider.xaxis.draw(renderer)
|
|
272
|
-
self.ax_slider.yaxis.draw(renderer)
|
|
273
|
-
|
|
274
|
-
self.slidercollection.draw(renderer)
|
|
403
|
+
renderer = self.figure.canvas.renderer
|
|
275
404
|
|
|
276
405
|
self.ax_price.xaxis.draw(renderer)
|
|
277
406
|
self.ax_price.yaxis.draw(renderer)
|
|
278
407
|
|
|
279
408
|
if self.candle_on_ma:
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
409
|
+
self.collection_ma.draw(renderer)
|
|
410
|
+
self.collection_candle.draw(renderer)
|
|
282
411
|
else:
|
|
283
|
-
self.
|
|
284
|
-
self.
|
|
412
|
+
self.collection_candle.draw(renderer)
|
|
413
|
+
self.collection_ma.draw(renderer)
|
|
414
|
+
|
|
415
|
+
if self.watermark:
|
|
416
|
+
self.text_watermark.set_text(self.watermark)
|
|
417
|
+
self.text_watermark.draw(renderer)
|
|
285
418
|
|
|
286
419
|
self.ax_volume.xaxis.draw(renderer)
|
|
287
420
|
self.ax_volume.yaxis.draw(renderer)
|
|
288
421
|
|
|
289
|
-
self.
|
|
422
|
+
self.collection_volume.draw(renderer)
|
|
290
423
|
return
|
|
291
424
|
|
|
292
|
-
def
|
|
293
|
-
self.
|
|
294
|
-
self._restore_region()
|
|
425
|
+
def _blit(self):
|
|
426
|
+
self.figure.canvas.blit()
|
|
295
427
|
return
|
|
296
428
|
|
|
297
|
-
def
|
|
298
|
-
|
|
429
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
430
|
+
self.vxmin, self.vxmax = (xmin, xmax + 1)
|
|
431
|
+
if xmin < 0: xmin = 0
|
|
432
|
+
if xmax < 0: xmax = 0
|
|
433
|
+
if xmin == xmax: xmax += 1
|
|
434
|
+
|
|
435
|
+
ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
|
|
436
|
+
yspace = (ymax - ymin) / 15
|
|
437
|
+
# 주가 차트 ymin, ymax
|
|
438
|
+
self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
|
|
439
|
+
|
|
440
|
+
# 거래량 차트 ymax
|
|
441
|
+
self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
|
|
442
|
+
|
|
443
|
+
self._set_segments(xmin, xmax, simpler, set_ma)
|
|
444
|
+
self._change_lim(self.vxmin, self.vxmax)
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
def _change_lim(self, xmin, xmax):
|
|
448
|
+
# 주가 차트 xlim
|
|
449
|
+
self.ax_price.set_xlim(xmin, xmax)
|
|
450
|
+
# 거래량 차트 xlim
|
|
451
|
+
self.ax_volume.set_xlim(xmin, xmax)
|
|
299
452
|
|
|
300
|
-
|
|
453
|
+
# 주가 차트 ylim
|
|
454
|
+
self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
|
|
455
|
+
# 거래량 차트 ylim
|
|
456
|
+
self.ax_volume.set_ylim(0, self.volume_ymax)
|
|
301
457
|
return
|
|
302
458
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
459
|
+
def get_default_lim(self):
|
|
460
|
+
return (0, self.list_index[-1])
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class BackgroundMixin(DrawMixin):
|
|
464
|
+
background = None
|
|
307
465
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self.
|
|
466
|
+
_creating_background = False
|
|
467
|
+
|
|
468
|
+
def _connect_event(self):
|
|
469
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
470
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
312
471
|
return
|
|
313
472
|
|
|
314
|
-
def
|
|
315
|
-
|
|
473
|
+
def _create_background(self):
|
|
474
|
+
if self._creating_background: return
|
|
316
475
|
|
|
317
|
-
|
|
318
|
-
self.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if change_lim:
|
|
322
|
-
# 주가 xlim
|
|
323
|
-
self.ax_price.set_xlim(self.xmin, self.xmax)
|
|
324
|
-
# 거래량 xlim
|
|
325
|
-
self.ax_volume.set_xlim(self.xmin, self.xmax)
|
|
476
|
+
self._creating_background = True
|
|
477
|
+
self._copy_bbox()
|
|
478
|
+
self._creating_background = False
|
|
479
|
+
return
|
|
326
480
|
|
|
327
|
-
|
|
328
|
-
|
|
481
|
+
def _copy_bbox(self):
|
|
482
|
+
self._draw_artist()
|
|
483
|
+
self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
484
|
+
return
|
|
329
485
|
|
|
330
|
-
|
|
331
|
-
self.
|
|
332
|
-
self.
|
|
486
|
+
def _on_draw(self, e):
|
|
487
|
+
self.background = None
|
|
488
|
+
self._restore_region()
|
|
489
|
+
return
|
|
333
490
|
|
|
334
|
-
|
|
335
|
-
self.
|
|
336
|
-
if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
|
|
491
|
+
def _restore_region(self):
|
|
492
|
+
if not self.background: self._create_background()
|
|
337
493
|
|
|
338
|
-
|
|
339
|
-
self._vol_ymax = self.df[self.volume].max() * 1.2
|
|
340
|
-
if change_lim: self.ax_volume.set_ylim(0, self._vol_ymax)
|
|
494
|
+
self.figure.canvas.renderer.restore_region(self.background)
|
|
341
495
|
return
|
|
342
496
|
|
|
343
497
|
|
|
344
|
-
class
|
|
498
|
+
class BaseMixin(BackgroundMixin):
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
class Chart(BaseMixin, Mixin):
|
|
503
|
+
def _add_collection(self):
|
|
504
|
+
super()._add_collection()
|
|
505
|
+
return self.add_collection()
|
|
506
|
+
|
|
507
|
+
def _draw_artist(self):
|
|
508
|
+
super()._draw_artist()
|
|
509
|
+
return self.draw_artist()
|
|
510
|
+
|
|
511
|
+
def _get_segments(self):
|
|
512
|
+
self.generate_data()
|
|
513
|
+
return super()._get_segments()
|
|
514
|
+
|
|
345
515
|
def _on_draw(self, e):
|
|
346
516
|
super()._on_draw(e)
|
|
347
517
|
return self.on_draw(e)
|
|
@@ -350,22 +520,3 @@ class Chart(DrawMixin, Mixin):
|
|
|
350
520
|
self.on_pick(e)
|
|
351
521
|
return super()._on_pick(e)
|
|
352
522
|
|
|
353
|
-
|
|
354
|
-
if __name__ == '__main__':
|
|
355
|
-
import json
|
|
356
|
-
from time import time
|
|
357
|
-
|
|
358
|
-
import matplotlib.pyplot as plt
|
|
359
|
-
from pathlib import Path
|
|
360
|
-
|
|
361
|
-
with open(Path(__file__).parent / 'data/samsung.txt', 'r', encoding='utf-8') as txt:
|
|
362
|
-
data = json.load(txt)
|
|
363
|
-
print(f'{len(data)=}')
|
|
364
|
-
# data = data[:200]
|
|
365
|
-
df = pd.DataFrame(data)
|
|
366
|
-
|
|
367
|
-
t = time()
|
|
368
|
-
DrawMixin().set_data(df)
|
|
369
|
-
t2 = time() - t
|
|
370
|
-
print(f'{t2=}')
|
|
371
|
-
plt.show()
|