seolpyo-mplchart 0.1.3.4__py3-none-any.whl → 1.0.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.
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 +383 -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.0.dist-info}/METADATA +4 -8
- seolpyo_mplchart-1.0.0.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.0.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-0.1.3.4.dist-info → seolpyo_mplchart-1.0.0.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,347 @@ 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
|
-
|
|
139
|
-
|
|
140
|
-
self.candlecollection.set_segments(candleseg)
|
|
141
|
-
self.candlecollection.set_facecolor(self.df['facecolor'].values)
|
|
142
|
-
self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
|
|
186
|
+
self.segment_candle = segment_candle.reshape(segment_candle.shape[0], 12, 2)
|
|
143
187
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
'
|
|
147
|
-
'
|
|
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
|
|
269
|
+
|
|
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
|
+
self.collection_candle.set_segments(self.segment_candle[index_start:index_end])
|
|
280
|
+
self.collection_candle.set_facecolor(self.facecolor_candle[index_start:index_end])
|
|
281
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
282
|
+
|
|
283
|
+
if self.volume:
|
|
284
|
+
self.collection_volume.set_segments(self.segment_volume[index_start:index_end])
|
|
285
|
+
self.collection_volume.set_linewidth(0.7)
|
|
286
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
287
|
+
self.collection_volume.set_edgecolor(self.edgecolor_volume[index_start:index_end])
|
|
288
|
+
|
|
289
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
290
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
294
|
+
self.collection_candle.set_segments(self.segment_candle_wick[index_start:index_end])
|
|
295
|
+
self.collection_candle.set_facecolor([])
|
|
296
|
+
self.collection_candle.set_edgecolor(self.edgecolor_candle[index_start:index_end])
|
|
297
|
+
|
|
298
|
+
if self.volume:
|
|
299
|
+
seg = self.segment_volume_wick[index_start:index_end]
|
|
300
|
+
if simpler:
|
|
301
|
+
values = seg[:, 1, 1]
|
|
302
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
303
|
+
seg = seg[top_index]
|
|
304
|
+
self.collection_volume.set_segments(seg)
|
|
305
|
+
self.collection_volume.set_linewidth(1.3)
|
|
306
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
307
|
+
self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
|
|
308
|
+
|
|
309
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
310
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
314
|
+
self.collection_candle.set_segments(self.segment_priceline[:, index_start:index_end])
|
|
315
|
+
self.collection_candle.set_facecolor([])
|
|
316
|
+
self.collection_candle.set_edgecolor(self.color_priceline)
|
|
317
|
+
|
|
318
|
+
if self.volume:
|
|
319
|
+
seg = self.segment_volume_wick[index_start:index_end]
|
|
320
|
+
if simpler:
|
|
321
|
+
values = seg[:, 1, 1]
|
|
322
|
+
top_index = np.argsort(-values)[:self.limit_volume]
|
|
323
|
+
seg = seg[top_index]
|
|
324
|
+
self.collection_volume.set_segments(seg)
|
|
325
|
+
self.collection_volume.set_linewidth(1.3)
|
|
326
|
+
self.collection_volume.set_facecolor(self.facecolor_volume[index_start:index_end])
|
|
327
|
+
self.collection_volume.set_edgecolor(self.facecolor_volume[index_start:index_end])
|
|
328
|
+
|
|
329
|
+
if not set_ma: self.collection_ma.set_segments([])
|
|
330
|
+
else:
|
|
331
|
+
self.collection_ma.set_segments(self.segment_ma[:, index_start:index_end])
|
|
332
|
+
self.collection_ma.set_edgecolor(list(reversed(self.edgecolor_ma)))
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class EventMixin(SegmentMixin):
|
|
337
|
+
def __init__(self, *args, **kwargs):
|
|
338
|
+
super().__init__(*args, **kwargs)
|
|
201
339
|
|
|
202
|
-
|
|
203
|
-
|
|
340
|
+
self._connect_event()
|
|
341
|
+
return
|
|
342
|
+
|
|
343
|
+
def _connect_event(self):
|
|
344
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
204
345
|
return
|
|
205
346
|
|
|
206
347
|
def _on_pick(self, e):
|
|
207
348
|
self._pick_ma_action(e)
|
|
208
|
-
|
|
209
|
-
return self._draw()
|
|
349
|
+
return
|
|
210
350
|
|
|
211
351
|
def _pick_ma_action(self, e: PickEvent):
|
|
212
352
|
handle = e.artist
|
|
213
|
-
if
|
|
353
|
+
if handle.get_alpha() == 0.2:
|
|
214
354
|
visible = True
|
|
215
355
|
handle.set_alpha(1.0)
|
|
216
356
|
else:
|
|
@@ -218,130 +358,154 @@ class CollectionMixin(DataMixin):
|
|
|
218
358
|
handle.set_alpha(0.2)
|
|
219
359
|
|
|
220
360
|
n = int(handle.get_label())
|
|
221
|
-
|
|
222
361
|
if visible: self._visible_ma = {i for i in self.list_ma if i in self._visible_ma or i == n}
|
|
223
362
|
else: self._visible_ma = {i for i in self._visible_ma if i != n}
|
|
224
363
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
364
|
+
alphas = [(1 if i in self._visible_ma else 0) for i in reversed(self.list_ma)]
|
|
365
|
+
self.collection_ma.set_alpha(alphas)
|
|
366
|
+
|
|
367
|
+
self._draw()
|
|
228
368
|
return
|
|
229
369
|
|
|
230
370
|
def _draw(self):
|
|
231
|
-
|
|
232
|
-
self.canvas = self.fig.canvas
|
|
233
|
-
self.canvas.draw()
|
|
371
|
+
self.figure.canvas.draw()
|
|
234
372
|
return
|
|
235
373
|
|
|
236
374
|
|
|
237
|
-
class
|
|
238
|
-
background = None
|
|
375
|
+
class DrawMixin(EventMixin):
|
|
239
376
|
candle_on_ma = True
|
|
240
377
|
|
|
241
|
-
|
|
378
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, *args, **kwargs):
|
|
379
|
+
self._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *args, **kwargs)
|
|
380
|
+
self._get_segments()
|
|
242
381
|
|
|
243
|
-
|
|
244
|
-
|
|
382
|
+
vmin, vmax = self.get_default_lim()
|
|
383
|
+
self._set_lim(vmin, vmax)
|
|
245
384
|
return
|
|
246
385
|
|
|
247
386
|
def _connect_event(self):
|
|
248
387
|
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
|
|
388
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
261
389
|
return
|
|
262
390
|
|
|
263
|
-
def
|
|
391
|
+
def _on_draw(self, e):
|
|
264
392
|
self._draw_artist()
|
|
265
|
-
self.
|
|
393
|
+
self._blit()
|
|
266
394
|
return
|
|
267
395
|
|
|
268
396
|
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)
|
|
397
|
+
renderer = self.figure.canvas.renderer
|
|
275
398
|
|
|
276
399
|
self.ax_price.xaxis.draw(renderer)
|
|
277
400
|
self.ax_price.yaxis.draw(renderer)
|
|
278
401
|
|
|
279
402
|
if self.candle_on_ma:
|
|
280
|
-
self.
|
|
281
|
-
self.
|
|
403
|
+
self.collection_ma.draw(renderer)
|
|
404
|
+
self.collection_candle.draw(renderer)
|
|
282
405
|
else:
|
|
283
|
-
self.
|
|
284
|
-
self.
|
|
406
|
+
self.collection_candle.draw(renderer)
|
|
407
|
+
self.collection_ma.draw(renderer)
|
|
408
|
+
|
|
409
|
+
if self.watermark:
|
|
410
|
+
self.text_watermark.set_text(self.watermark)
|
|
411
|
+
self.text_watermark.draw(renderer)
|
|
285
412
|
|
|
286
413
|
self.ax_volume.xaxis.draw(renderer)
|
|
287
414
|
self.ax_volume.yaxis.draw(renderer)
|
|
288
415
|
|
|
289
|
-
self.
|
|
416
|
+
self.collection_volume.draw(renderer)
|
|
290
417
|
return
|
|
291
418
|
|
|
292
|
-
def
|
|
293
|
-
self.
|
|
294
|
-
self._restore_region()
|
|
419
|
+
def _blit(self):
|
|
420
|
+
self.figure.canvas.blit()
|
|
295
421
|
return
|
|
296
422
|
|
|
297
|
-
def
|
|
298
|
-
|
|
423
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
424
|
+
self.vxmin, self.vxmax = (xmin, xmax + 1)
|
|
425
|
+
if xmin < 0: xmin = 0
|
|
426
|
+
if xmax < 0: xmax = 0
|
|
427
|
+
if xmin == xmax: xmax += 1
|
|
428
|
+
|
|
429
|
+
ymin, ymax = (self.df[self.low][xmin:xmax].min(), self.df[self.high][xmin:xmax].max())
|
|
430
|
+
yspace = (ymax - ymin) / 15
|
|
431
|
+
# 주가 차트 ymin, ymax
|
|
432
|
+
self.price_ymin, self.price_ymax = (ymin-yspace, ymax+yspace)
|
|
433
|
+
|
|
434
|
+
# 거래량 차트 ymax
|
|
435
|
+
self.volume_ymax = self.df['_volymax'][xmin:xmax].max() if self.volume else 1
|
|
436
|
+
|
|
437
|
+
self._set_segments(xmin, xmax, simpler, set_ma)
|
|
438
|
+
self._change_lim(self.vxmin, self.vxmax)
|
|
439
|
+
return
|
|
440
|
+
|
|
441
|
+
def _change_lim(self, xmin, xmax):
|
|
442
|
+
# 주가 차트 xlim
|
|
443
|
+
self.ax_price.set_xlim(xmin, xmax)
|
|
444
|
+
# 거래량 차트 xlim
|
|
445
|
+
self.ax_volume.set_xlim(xmin, xmax)
|
|
299
446
|
|
|
300
|
-
|
|
447
|
+
# 주가 차트 ylim
|
|
448
|
+
self.ax_price.set_ylim(self.price_ymin, self.price_ymax)
|
|
449
|
+
# 거래량 차트 ylim
|
|
450
|
+
self.ax_volume.set_ylim(0, self.volume_ymax)
|
|
301
451
|
return
|
|
302
452
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
453
|
+
def get_default_lim(self):
|
|
454
|
+
return (0, self.list_index[-1])
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
class BackgroundMixin(DrawMixin):
|
|
458
|
+
background = None
|
|
307
459
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
self.
|
|
460
|
+
_creating_background = False
|
|
461
|
+
|
|
462
|
+
def _connect_event(self):
|
|
463
|
+
self.figure.canvas.mpl_connect('pick_event', lambda x: self._on_pick(x))
|
|
464
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self._on_draw(x))
|
|
312
465
|
return
|
|
313
466
|
|
|
314
|
-
def
|
|
315
|
-
|
|
467
|
+
def _create_background(self):
|
|
468
|
+
if self._creating_background: return
|
|
316
469
|
|
|
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)
|
|
470
|
+
self._creating_background = True
|
|
471
|
+
self._copy_bbox()
|
|
472
|
+
self._creating_background = False
|
|
473
|
+
return
|
|
326
474
|
|
|
327
|
-
|
|
328
|
-
|
|
475
|
+
def _copy_bbox(self):
|
|
476
|
+
self._draw_artist()
|
|
477
|
+
self.background = self.figure.canvas.renderer.copy_from_bbox(self.figure.bbox)
|
|
478
|
+
return
|
|
329
479
|
|
|
330
|
-
|
|
331
|
-
self.
|
|
332
|
-
self.
|
|
480
|
+
def _on_draw(self, e):
|
|
481
|
+
self.background = None
|
|
482
|
+
self._restore_region()
|
|
483
|
+
return
|
|
333
484
|
|
|
334
|
-
|
|
335
|
-
self.
|
|
336
|
-
if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
|
|
485
|
+
def _restore_region(self):
|
|
486
|
+
if not self.background: self._create_background()
|
|
337
487
|
|
|
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)
|
|
488
|
+
self.figure.canvas.renderer.restore_region(self.background)
|
|
341
489
|
return
|
|
342
490
|
|
|
343
491
|
|
|
344
|
-
class
|
|
492
|
+
class BaseMixin(BackgroundMixin):
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
class Chart(BaseMixin, Mixin):
|
|
497
|
+
def _add_collection(self):
|
|
498
|
+
super()._add_collection()
|
|
499
|
+
return self.add_collection()
|
|
500
|
+
|
|
501
|
+
def _draw_artist(self):
|
|
502
|
+
super()._draw_artist()
|
|
503
|
+
return self.draw_artist()
|
|
504
|
+
|
|
505
|
+
def _get_segments(self):
|
|
506
|
+
self.generate_data()
|
|
507
|
+
return super()._get_segments()
|
|
508
|
+
|
|
345
509
|
def _on_draw(self, e):
|
|
346
510
|
super()._on_draw(e)
|
|
347
511
|
return self.on_draw(e)
|
|
@@ -350,22 +514,3 @@ class Chart(DrawMixin, Mixin):
|
|
|
350
514
|
self.on_pick(e)
|
|
351
515
|
return super()._on_pick(e)
|
|
352
516
|
|
|
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()
|