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