seolpyo-mplchart 1.4.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 +144 -308
- 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 +27 -25
- seolpyo_mplchart/_draw.py +7 -18
- seolpyo_mplchart/_slider.py +26 -20
- seolpyo_mplchart/test.py +172 -56
- 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-1.4.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/WHEEL +1 -1
- seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
- seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
- {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
|
|
3
|
+
from matplotlib.backend_bases import MouseEvent
|
|
4
|
+
|
|
5
|
+
from ._cursor import BaseMixin as Base
|
|
6
|
+
from ..._config.utils import float_to_str
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InfoMixin(Base):
|
|
10
|
+
fraction = False
|
|
11
|
+
|
|
12
|
+
def _set_data(self, df, *args, **kwargs):
|
|
13
|
+
super()._set_data(df, *args, **kwargs)
|
|
14
|
+
|
|
15
|
+
self._set_length_text()
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
def _set_length_text(self):
|
|
19
|
+
func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit, word=self.CONFIG.UNIT.price))
|
|
20
|
+
self._length_text = self.df['high'].apply(func).max()
|
|
21
|
+
|
|
22
|
+
if self.key_volume:
|
|
23
|
+
func = lambda x: len(self.CONFIG.UNIT.func(x, digit=self.CONFIG.UNIT.digit_volume, word=self.CONFIG.UNIT.volume))
|
|
24
|
+
lenth_volume = self.df['volume'].apply(func).max()
|
|
25
|
+
# print(f'{self._length_text=}')
|
|
26
|
+
# print(f'{lenth_volume=}')
|
|
27
|
+
if self._length_text < lenth_volume:
|
|
28
|
+
self._length_text = lenth_volume
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
def _draw_box_artist(self, e):
|
|
32
|
+
super()._draw_box_artist(e)
|
|
33
|
+
|
|
34
|
+
if self.intx is not None:
|
|
35
|
+
if self.in_candle:
|
|
36
|
+
self._draw_candle_info_artist(e)
|
|
37
|
+
elif self.key_volume and self.in_volumebar:
|
|
38
|
+
self._draw_volume_info_artist(e)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
def _draw_candle_info_artist(self, e: MouseEvent):
|
|
42
|
+
# 캔들 정보
|
|
43
|
+
self.artist_info_candle.set_text(self._get_info(self.intx))
|
|
44
|
+
|
|
45
|
+
# 정보 텍스트를 중앙에 몰리게 설정할 수도 있지만,
|
|
46
|
+
# 그런 경우 차트를 가리므로 좌우 끝단에 위치하도록 설정
|
|
47
|
+
if self.vmiddle < e.xdata:
|
|
48
|
+
self.artist_info_candle.set_x(self.v0)
|
|
49
|
+
else:
|
|
50
|
+
# self.artist_info_candle.set_x(self.vmax - self.x_distance)
|
|
51
|
+
# self.artist_info_candle.set_horizontalalignment('right')
|
|
52
|
+
# 텍스트박스 크기 가져오기
|
|
53
|
+
bbox = self.artist_info_candle.get_window_extent()\
|
|
54
|
+
.transformed(self.ax_price.transData.inverted())
|
|
55
|
+
width = bbox.x1 - bbox.x0
|
|
56
|
+
self.artist_info_candle.set_x(self.v1 - width)
|
|
57
|
+
|
|
58
|
+
self.artist_info_candle.draw(self.figure.canvas.renderer)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
def _draw_volume_info_artist(self, e: MouseEvent):
|
|
62
|
+
# 거래량 정보
|
|
63
|
+
self.artist_info_volume.set_text(self._get_info(self.intx, is_price=False))
|
|
64
|
+
|
|
65
|
+
if self.vmiddle < e.xdata:
|
|
66
|
+
self.artist_info_volume.set_x(self.v0)
|
|
67
|
+
else:
|
|
68
|
+
# self.artist_info_volume.set_x(self.vmax - self.x_distance)
|
|
69
|
+
# self.artist_info_volume.set_horizontalalignment('right')
|
|
70
|
+
# 텍스트박스 크기 가져오기
|
|
71
|
+
bbox = self.artist_info_volume.get_window_extent()\
|
|
72
|
+
.transformed(self.ax_price.transData.inverted())
|
|
73
|
+
width = bbox.x1 - bbox.x0
|
|
74
|
+
self.artist_info_volume.set_x(self.v1 - width)
|
|
75
|
+
|
|
76
|
+
self.artist_info_volume.draw(self.figure.canvas.renderer)
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def get_info_kwargs(self, is_price: bool, **kwargs)-> dict:
|
|
80
|
+
"""
|
|
81
|
+
get text info kwargs
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
is_price (bool): is price chart info or not
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
dict[str, any]: text info kwargs
|
|
88
|
+
"""
|
|
89
|
+
return kwargs
|
|
90
|
+
|
|
91
|
+
def _get_info(self, index, is_price=True):
|
|
92
|
+
# print(f'{self._length_text=}')
|
|
93
|
+
dt = self.df.loc[index, 'date']
|
|
94
|
+
if not self.key_volume:
|
|
95
|
+
v, vr = ('-', '-')
|
|
96
|
+
else:
|
|
97
|
+
v = self.df.loc[index, 'volume']
|
|
98
|
+
# print(f'{self.CONFIG.UNIT.digit_volume=}')
|
|
99
|
+
v = float_to_str(v, digit=self.CONFIG.UNIT.digit_volume)
|
|
100
|
+
# if not v % 1:
|
|
101
|
+
# v = int(v)
|
|
102
|
+
vr = self.df.loc[index, 'rate_volume']
|
|
103
|
+
vr = f'{vr:+06,.2f}'
|
|
104
|
+
|
|
105
|
+
if is_price:
|
|
106
|
+
o, h, l, c = (self.df.loc[index, 'open'], self.df.loc[index, 'high'], self.df.loc[index, 'low'], self.df.loc[index, 'close'])
|
|
107
|
+
rate, compare = (self.df.loc[index, 'rate'], self.df.loc[index, 'compare'])
|
|
108
|
+
r = f'{rate:+06,.2f}'
|
|
109
|
+
Or, hr, lr = (self.df.loc[index, 'rate_open'], self.df.loc[index, 'rate_high'], self.df.loc[index, 'rate_low'])
|
|
110
|
+
|
|
111
|
+
if self.fraction:
|
|
112
|
+
data = {}
|
|
113
|
+
c = round(c, self.CONFIG.UNIT.digit)
|
|
114
|
+
for value, key in [
|
|
115
|
+
[c, 'close'],
|
|
116
|
+
[compare, 'compare'],
|
|
117
|
+
[o, 'open'],
|
|
118
|
+
[h, 'high'],
|
|
119
|
+
[l, 'low'],
|
|
120
|
+
]:
|
|
121
|
+
div = divmod(value, 1)
|
|
122
|
+
if div[1]:
|
|
123
|
+
if div[0]:
|
|
124
|
+
data[key] = f'{float_to_str(div[0])} {Fraction((div[1]))}'
|
|
125
|
+
else:
|
|
126
|
+
data[key] = f' {Fraction((div[1]))}'
|
|
127
|
+
else:
|
|
128
|
+
data[key] = float_to_str(div[0])
|
|
129
|
+
# print(f'{data=}')
|
|
130
|
+
|
|
131
|
+
kwargs = self.get_info_kwargs(
|
|
132
|
+
is_price=is_price,
|
|
133
|
+
dt=dt,
|
|
134
|
+
close=f'{data["close"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
135
|
+
rate=f'{r:>{self._length_text}}%',
|
|
136
|
+
compare=f'{data["compare"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
137
|
+
open=f'{data["open"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
|
|
138
|
+
high=f'{data["high"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
|
|
139
|
+
low=f'{data["low"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
|
|
140
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
|
|
141
|
+
)
|
|
142
|
+
text = self.CONFIG.FORMAT.candle.format(**kwargs)
|
|
143
|
+
else:
|
|
144
|
+
o, h, l, c = (
|
|
145
|
+
float_to_str(o, digit=self.CONFIG.UNIT.digit),
|
|
146
|
+
float_to_str(h, digit=self.CONFIG.UNIT.digit),
|
|
147
|
+
float_to_str(l, digit=self.CONFIG.UNIT.digit),
|
|
148
|
+
float_to_str(c, digit=self.CONFIG.UNIT.digit),
|
|
149
|
+
)
|
|
150
|
+
com = float_to_str(compare, digit=self.CONFIG.UNIT.digit, plus=True)
|
|
151
|
+
|
|
152
|
+
kwargs = self.get_info_kwargs(
|
|
153
|
+
is_price=is_price,
|
|
154
|
+
dt=dt,
|
|
155
|
+
close=f'{c:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
156
|
+
rate=f'{r:>{self._length_text}}%',
|
|
157
|
+
compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
158
|
+
open=f'{o:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
|
|
159
|
+
high=f'{h:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
|
|
160
|
+
low=f'{l:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
|
|
161
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
|
|
162
|
+
)
|
|
163
|
+
text = self.CONFIG.FORMAT.candle.format(**kwargs)
|
|
164
|
+
elif self.key_volume:
|
|
165
|
+
compare = self.df.loc[index, 'compare_volume']
|
|
166
|
+
com = float_to_str(compare, digit=self.CONFIG.UNIT.digit_volume, plus=True)
|
|
167
|
+
kwargs = self.get_info_kwargs(
|
|
168
|
+
is_price=is_price,
|
|
169
|
+
dt=dt,
|
|
170
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}',
|
|
171
|
+
rate_volume=f'{vr:>{self._length_text}}%',
|
|
172
|
+
compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.volume}',
|
|
173
|
+
)
|
|
174
|
+
text = self.CONFIG.FORMAT.volume.format(**kwargs)
|
|
175
|
+
else:
|
|
176
|
+
text = ''
|
|
177
|
+
|
|
178
|
+
return text
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class BaseMixin(InfoMixin):
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class Chart(BaseMixin):
|
|
186
|
+
pass
|
|
187
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from matplotlib.collections import LineCollection
|
|
2
|
+
|
|
3
|
+
from .._base import BaseMixin as Base
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CollectionMixin(Base):
|
|
7
|
+
def _add_artists(self):
|
|
8
|
+
super()._add_artists()
|
|
9
|
+
|
|
10
|
+
self.collection_candle = LineCollection([], animated=True, linewidths=0.8)
|
|
11
|
+
self.ax_price.add_collection(self.collection_candle)
|
|
12
|
+
|
|
13
|
+
self.collection_ma = LineCollection([], animated=True, linewidths=1)
|
|
14
|
+
self.ax_price.add_collection(self.collection_ma)
|
|
15
|
+
|
|
16
|
+
self.collection_volume = LineCollection([], animated=True, linewidths=1)
|
|
17
|
+
self.ax_volume.add_collection(self.collection_volume)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class SegmentMixin(CollectionMixin):
|
|
22
|
+
def get_candle_segment(self, *, is_up, x, left, right, top, bottom, high, low):
|
|
23
|
+
"""
|
|
24
|
+
get candle segment
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
is_up (bool): (True if open < close else False)
|
|
28
|
+
x (float): center of candle
|
|
29
|
+
left (float): left of candle
|
|
30
|
+
right (float): right of candle
|
|
31
|
+
top (float): top of candle(close if `is_up` else open)
|
|
32
|
+
bottom (float): bottom of candle(open if `is_up` else close)
|
|
33
|
+
high (float): top of candle wick
|
|
34
|
+
low (float): bottom of candle wick
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
tuple[tuple[float, float]]: candle segment
|
|
38
|
+
"""
|
|
39
|
+
return (
|
|
40
|
+
(x, top),
|
|
41
|
+
(left, top), (left, bottom),
|
|
42
|
+
(x, bottom), (x, low), (x, bottom),
|
|
43
|
+
(right, bottom), (right, top),
|
|
44
|
+
(x, top), (x, high)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class BaseMixin(SegmentMixin):
|
|
49
|
+
pass
|
|
50
|
+
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from matplotlib.lines import Line2D
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from ._artist import BaseMixin as Base
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DataMixin(Base):
|
|
9
|
+
df: pd.DataFrame
|
|
10
|
+
|
|
11
|
+
key_date = 'date'
|
|
12
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
13
|
+
key_volume = 'volume'
|
|
14
|
+
|
|
15
|
+
def set_data(self, df: pd.DataFrame, *args, **kwargs):
|
|
16
|
+
# print(f'{kwargs=}')
|
|
17
|
+
self._set_data(df, *args, **kwargs)
|
|
18
|
+
self.figure.canvas.draw_idle()
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def _set_data(self, df: pd.DataFrame, *args, **kwargs):
|
|
22
|
+
keys = {
|
|
23
|
+
self.key_date: 'date',
|
|
24
|
+
self.key_open: 'open',
|
|
25
|
+
self.key_high: 'high',
|
|
26
|
+
self.key_low: 'low',
|
|
27
|
+
self.key_close: 'close',
|
|
28
|
+
self.key_volume: 'volume',
|
|
29
|
+
}
|
|
30
|
+
df.rename(columns=keys, inplace=True)
|
|
31
|
+
|
|
32
|
+
# 오름차순 정렬
|
|
33
|
+
df = df.sort_values(['date'])
|
|
34
|
+
df = df.reset_index(drop=True)
|
|
35
|
+
|
|
36
|
+
if self.key_volume:
|
|
37
|
+
df = df[['date', 'open', 'high', 'low', 'close', 'volume']]
|
|
38
|
+
else:
|
|
39
|
+
df = df[['date', 'open', 'high', 'low', 'close',]]
|
|
40
|
+
df['volume'] = 0
|
|
41
|
+
|
|
42
|
+
# 전일 종가
|
|
43
|
+
df['pre_close'] = df['close'].shift(1).fillna(0)
|
|
44
|
+
# 거래정지인 경우 전일종가 적용
|
|
45
|
+
df.loc[df['close'] == 0, 'close'] = df['pre_close']
|
|
46
|
+
# 종가만 유효한 경우 종가로 통일
|
|
47
|
+
df.loc[(df['close'] != 0) & (df['open'] == 0), ['open', 'high', 'low']] = df['close']
|
|
48
|
+
|
|
49
|
+
self.chart_price_ymax = df['high'].max() * 1.3
|
|
50
|
+
if self.key_volume:
|
|
51
|
+
self.chart_volume_ymax = df['volume'].max() * 1.3
|
|
52
|
+
else:
|
|
53
|
+
self.chart_volume_ymax = 10
|
|
54
|
+
# 거래량 차트 영역 최소화
|
|
55
|
+
self.CONFIG.FIGURE.RATIO.volume = 0
|
|
56
|
+
# tick 그리지 않기
|
|
57
|
+
self.ax_volume.set_yticklabels([])
|
|
58
|
+
self.ax_volume.set_yticks([])
|
|
59
|
+
self._set_figure()
|
|
60
|
+
|
|
61
|
+
self.index_list = df.index.tolist()
|
|
62
|
+
self.xmin, self.xmax = (0, self.index_list[-1])
|
|
63
|
+
|
|
64
|
+
if not self.CONFIG.MA.ma_list:
|
|
65
|
+
self.CONFIG.MA.ma_list = tuple()
|
|
66
|
+
else:
|
|
67
|
+
self.CONFIG.MA.ma_list = sorted(self.CONFIG.MA.ma_list)
|
|
68
|
+
# 가격이동평균선 계산
|
|
69
|
+
for i in self.CONFIG.MA.ma_list:
|
|
70
|
+
df[f'ma{i}'] = df['close'].rolling(i).mean()
|
|
71
|
+
|
|
72
|
+
df['x'] = df.index + 0.5
|
|
73
|
+
df['left_candle'] = df['x'] - self.CONFIG.CANDLE.half_width
|
|
74
|
+
df['right_candle'] = df['x'] + self.CONFIG.CANDLE.half_width
|
|
75
|
+
df['left_volume'] = df['x'] - self.CONFIG.VOLUME.half_width
|
|
76
|
+
df['right_volume'] = df['x'] + self.CONFIG.VOLUME.half_width
|
|
77
|
+
df['zero'] = 0
|
|
78
|
+
|
|
79
|
+
df['is_up'] = np.where(df['open'] < df['close'], True, False)
|
|
80
|
+
df['top_candle'] = np.where(df['is_up'], df['close'], df['open'])
|
|
81
|
+
df['bottom_candle'] = np.where(df['is_up'], df['open'], df['close'])
|
|
82
|
+
|
|
83
|
+
if self.key_volume:
|
|
84
|
+
df['ymax_volume'] = df['volume'] * 1.2
|
|
85
|
+
|
|
86
|
+
self.df = df
|
|
87
|
+
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CandleSegmentMixin(DataMixin):
|
|
92
|
+
def set_segments(self):
|
|
93
|
+
self.set_candle_segments()
|
|
94
|
+
self.set_candle_color_segments()
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
def set_color_segments(self):
|
|
98
|
+
self.set_candle_color_segments()
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
def set_candle_segments(self):
|
|
102
|
+
# 캔들 세그먼트
|
|
103
|
+
segment_candle = []
|
|
104
|
+
segment_wick = []
|
|
105
|
+
segment_priceline = []
|
|
106
|
+
for x, left, right, top, bottom, is_up, high, low in zip(
|
|
107
|
+
self.df['x'].to_numpy().tolist(),
|
|
108
|
+
self.df['left_candle'].to_numpy().tolist(), self.df['right_candle'].to_numpy().tolist(),
|
|
109
|
+
self.df['top_candle'].to_numpy().tolist(), self.df['bottom_candle'].to_numpy().tolist(),
|
|
110
|
+
self.df['is_up'].to_numpy().tolist(),
|
|
111
|
+
self.df['high'].to_numpy().tolist(), self.df['low'].to_numpy().tolist(),
|
|
112
|
+
):
|
|
113
|
+
segment_candle.append(
|
|
114
|
+
self.get_candle_segment(
|
|
115
|
+
is_up=is_up,
|
|
116
|
+
x=x, left=left, right=right,
|
|
117
|
+
top=top, bottom=bottom,
|
|
118
|
+
high=high, low=low,
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
self.segment_candle = np.array(segment_candle)
|
|
123
|
+
# 심지 세그먼트
|
|
124
|
+
segment_wick = self.df[[
|
|
125
|
+
'x', 'high',
|
|
126
|
+
'x', 'low',
|
|
127
|
+
]].values
|
|
128
|
+
self.segment_candle_wick = segment_wick.reshape(segment_wick.shape[0], 2, 2)
|
|
129
|
+
# 종가 세그먼트
|
|
130
|
+
segment_priceline = segment_wick = self.df[['x', 'close']].values
|
|
131
|
+
self.segment_priceline = segment_priceline.reshape(1, *segment_wick.shape)
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
def set_candle_color_segments(self):
|
|
135
|
+
self.add_candle_color_column()
|
|
136
|
+
|
|
137
|
+
self.facecolor_candle = self.df['facecolor'].values
|
|
138
|
+
self.edgecolor_candle = self.df['edgecolor'].values
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
def add_candle_color_column(self):
|
|
142
|
+
columns = ['facecolor', 'edgecolor']
|
|
143
|
+
face_bull_rise = self.CONFIG.CANDLE.FACECOLOR.bull_rise
|
|
144
|
+
face_bull_fall = self.CONFIG.CANDLE.FACECOLOR.bull_fall
|
|
145
|
+
face_bear_rise = self.CONFIG.CANDLE.FACECOLOR.bear_rise
|
|
146
|
+
face_bear_fall = self.CONFIG.CANDLE.FACECOLOR.bear_fall
|
|
147
|
+
edge_bull_rise = self.CONFIG.CANDLE.EDGECOLOR.bull_rise
|
|
148
|
+
edge_bull_fall = self.CONFIG.CANDLE.EDGECOLOR.bull_fall
|
|
149
|
+
edge_bear_rise = self.CONFIG.CANDLE.EDGECOLOR.bear_rise
|
|
150
|
+
edge_bear_fall = self.CONFIG.CANDLE.EDGECOLOR.bear_fall
|
|
151
|
+
doji = self.CONFIG.CANDLE.EDGECOLOR.doji
|
|
152
|
+
|
|
153
|
+
# 상승양봉
|
|
154
|
+
self.df.loc[:, columns] = (face_bull_rise, edge_bull_rise)
|
|
155
|
+
if face_bull_rise != face_bear_fall or edge_bull_rise != edge_bear_fall:
|
|
156
|
+
# 하락음봉
|
|
157
|
+
self.df.loc[self.df['close'] < self.df['open'], columns] = (face_bear_fall, edge_bear_fall)
|
|
158
|
+
if face_bull_rise != doji or face_bear_fall != doji or edge_bull_rise != doji or edge_bear_fall != doji:
|
|
159
|
+
# 보합
|
|
160
|
+
self.df.loc[self.df['close'] == self.df['open'], columns] = (doji, doji)
|
|
161
|
+
|
|
162
|
+
if face_bull_rise != face_bull_fall or edge_bull_rise != edge_bull_fall:
|
|
163
|
+
# 하락양봉(비우기)
|
|
164
|
+
self.df.loc[(self.df['facecolor'] == face_bull_rise) & (self.df['close'] <= self.df['pre_close']), columns] = (face_bull_fall, edge_bull_fall)
|
|
165
|
+
if face_bear_fall != face_bear_rise or edge_bear_fall != edge_bear_rise:
|
|
166
|
+
# 상승음봉(비우기)
|
|
167
|
+
self.df.loc[(self.df['facecolor'] == face_bear_fall) & (self.df['pre_close'] <= self.df['close']), columns] = (face_bear_rise, edge_bear_rise)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class MaSegmentMixin(CandleSegmentMixin):
|
|
172
|
+
_visible_ma = set()
|
|
173
|
+
|
|
174
|
+
def set_segments(self):
|
|
175
|
+
super().set_segments()
|
|
176
|
+
|
|
177
|
+
self.set_ma_segments()
|
|
178
|
+
self._set_ma_color_segments()
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
def set_color_segments(self):
|
|
182
|
+
self._set_ma_color_segments()
|
|
183
|
+
|
|
184
|
+
super().set_color_segments()
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
def set_ma_segments(self):
|
|
188
|
+
# 주가 차트 가격이동평균선
|
|
189
|
+
key_ma = []
|
|
190
|
+
for i in reversed(self.CONFIG.MA.ma_list):
|
|
191
|
+
key_ma.append('x')
|
|
192
|
+
key_ma.append(f'ma{i}')
|
|
193
|
+
if key_ma:
|
|
194
|
+
segment_ma = self.df[key_ma].values
|
|
195
|
+
self.segment_ma = segment_ma.reshape(
|
|
196
|
+
segment_ma.shape[0], len(self.CONFIG.MA.ma_list), 2
|
|
197
|
+
).swapaxes(0, 1)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
def _set_ma_color_segments(self):
|
|
201
|
+
# 기존 legend 제거
|
|
202
|
+
legends = self.ax_legend.get_legend()
|
|
203
|
+
if legends:
|
|
204
|
+
legends.remove()
|
|
205
|
+
|
|
206
|
+
self._visible_ma.clear()
|
|
207
|
+
|
|
208
|
+
# 이평선 색상 가져오기
|
|
209
|
+
handle_list, label_list, list_color = ([], [], [])
|
|
210
|
+
arr = [0, 1]
|
|
211
|
+
for n, i in enumerate(self.CONFIG.MA.ma_list):
|
|
212
|
+
try:
|
|
213
|
+
c = self.CONFIG.MA.color_list[n]
|
|
214
|
+
except:
|
|
215
|
+
c = self.CONFIG.MA.color_default
|
|
216
|
+
list_color.append(c)
|
|
217
|
+
|
|
218
|
+
handle_list.append(Line2D(arr, arr, color=c, linewidth=5, label=i))
|
|
219
|
+
label_list.append(self.CONFIG.MA.format.format(i))
|
|
220
|
+
|
|
221
|
+
self._visible_ma.add(i)
|
|
222
|
+
self.edgecolor_ma = list(reversed(list_color))
|
|
223
|
+
|
|
224
|
+
# 가격이동평균선 legend 생성
|
|
225
|
+
if handle_list:
|
|
226
|
+
legends = self.ax_legend.legend(
|
|
227
|
+
handle_list, label_list, loc='lower left', ncol=10,
|
|
228
|
+
facecolor=self.CONFIG.AX.facecolor, edgecolor=self.CONFIG.AX.TICK.edgecolor,
|
|
229
|
+
labelcolor=self.CONFIG.AX.TICK.fontcolor,
|
|
230
|
+
)
|
|
231
|
+
for i in legends.legend_handles:
|
|
232
|
+
# legend 클릭시 pick event가 발생할 수 있도록 설정
|
|
233
|
+
i.set_picker(5)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class VolumeSegmentMixin(MaSegmentMixin):
|
|
238
|
+
def set_segments(self):
|
|
239
|
+
super().set_segments()
|
|
240
|
+
|
|
241
|
+
if self.key_volume:
|
|
242
|
+
self.set_volume_segments()
|
|
243
|
+
self.set_volume_color_segments()
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
def set_color_segments(self):
|
|
247
|
+
super().set_color_segments()
|
|
248
|
+
|
|
249
|
+
self.set_volume_color_segments()
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
def set_volume_segments(self):
|
|
253
|
+
# 거래량 바 세그먼트
|
|
254
|
+
segment_volume_wick = self.df[[
|
|
255
|
+
'left_volume', 'zero',
|
|
256
|
+
'left_volume', 'volume',
|
|
257
|
+
'right_volume', 'volume',
|
|
258
|
+
'right_volume', 'zero',
|
|
259
|
+
]].values
|
|
260
|
+
|
|
261
|
+
self.segment_volume = segment_volume_wick.reshape(segment_volume_wick.shape[0], 4, 2)
|
|
262
|
+
|
|
263
|
+
# segment_volume = []
|
|
264
|
+
# for x, left, right, top in zip(
|
|
265
|
+
# self.df['x'].to_numpy().tolist(),
|
|
266
|
+
# self.df['left_volume'].to_numpy().tolist(), self.df['right_volume'].to_numpy().tolist(),
|
|
267
|
+
# self.df[self.key_volume].to_numpy().tolist(),
|
|
268
|
+
# ):
|
|
269
|
+
# segment_volume.append(
|
|
270
|
+
# self.get_volume_segment(x=x, left=left, right=right, top=top)
|
|
271
|
+
# )
|
|
272
|
+
# self.segment_volume = np.array(segment_volume)
|
|
273
|
+
|
|
274
|
+
# 거래량 심지 세그먼트
|
|
275
|
+
segment_volume_wick = self.df[[
|
|
276
|
+
'x', 'zero',
|
|
277
|
+
'x', 'volume',
|
|
278
|
+
]].values
|
|
279
|
+
self.segment_volume_wick = segment_volume_wick.reshape(segment_volume_wick.shape[0], 2, 2)
|
|
280
|
+
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
def _add_volume_color_column(self):
|
|
284
|
+
columns = ['facecolor_volume', 'edgecolor_volume']
|
|
285
|
+
face_rise = self.CONFIG.VOLUME.FACECOLOR.rise
|
|
286
|
+
face_fall = self.CONFIG.VOLUME.FACECOLOR.fall
|
|
287
|
+
edge_rise = self.CONFIG.VOLUME.EDGECOLOR.rise
|
|
288
|
+
edge_fall = self.CONFIG.VOLUME.EDGECOLOR.fall
|
|
289
|
+
face_doji = self.CONFIG.VOLUME.FACECOLOR.doji
|
|
290
|
+
edge_doji = self.CONFIG.VOLUME.EDGECOLOR.doji
|
|
291
|
+
|
|
292
|
+
# 주가 상승
|
|
293
|
+
self.df.loc[:, columns] = (face_rise, edge_rise)
|
|
294
|
+
if face_rise != face_fall or edge_rise != edge_fall:
|
|
295
|
+
# 주가 하락
|
|
296
|
+
condition = self.df['close'] < self.df['pre_close']
|
|
297
|
+
self.df.loc[condition, columns] = (face_fall, edge_fall)
|
|
298
|
+
if face_rise != face_doji or edge_rise != edge_doji:
|
|
299
|
+
# 보합
|
|
300
|
+
condition = self.df['close'] == self.df['pre_close']
|
|
301
|
+
self.df.loc[condition, columns] = (edge_doji, edge_doji)
|
|
302
|
+
return
|
|
303
|
+
|
|
304
|
+
def set_volume_color_segments(self):
|
|
305
|
+
self._add_volume_color_column()
|
|
306
|
+
|
|
307
|
+
self.facecolor_volume = self.df['facecolor_volume'].values
|
|
308
|
+
self.edgecolor_volume = self.df['edgecolor_volume'].values
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class BaseMixin(VolumeSegmentMixin):
|
|
313
|
+
pass
|
|
314
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from ._lim import BaseMixin as Base
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DrawMixin(Base):
|
|
5
|
+
candle_on_ma = True
|
|
6
|
+
|
|
7
|
+
def _connect_events(self):
|
|
8
|
+
super()._connect_events()
|
|
9
|
+
self.figure.canvas.mpl_connect('draw_event', lambda x: self.on_draw(x))
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
def on_draw(self, e):
|
|
13
|
+
self._on_draw(e)
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
def _on_draw(self, e):
|
|
17
|
+
self.draw_artists()
|
|
18
|
+
self.figure.canvas.blit()
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def draw_artists(self):
|
|
22
|
+
self._draw_artists()
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def _draw_artists(self):
|
|
26
|
+
self._draw_ax_price()
|
|
27
|
+
self._draw_ax_volume()
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
def _draw_ax_price(self):
|
|
31
|
+
renderer = self.figure.canvas.renderer
|
|
32
|
+
# print(f'{renderer=}')
|
|
33
|
+
|
|
34
|
+
self.ax_price.xaxis.draw(renderer)
|
|
35
|
+
self.ax_price.yaxis.draw(renderer)
|
|
36
|
+
|
|
37
|
+
if self.candle_on_ma:
|
|
38
|
+
self.collection_ma.draw(renderer)
|
|
39
|
+
self.collection_candle.draw(renderer)
|
|
40
|
+
else:
|
|
41
|
+
self.collection_candle.draw(renderer)
|
|
42
|
+
self.collection_ma.draw(renderer)
|
|
43
|
+
|
|
44
|
+
if self.watermark:
|
|
45
|
+
if self.watermark != self.artist_watermark.get_text():
|
|
46
|
+
self.artist_watermark.set_text(self.watermark)
|
|
47
|
+
self.artist_watermark.draw(renderer)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
def _draw_ax_volume(self):
|
|
51
|
+
renderer = self.figure.canvas.renderer
|
|
52
|
+
|
|
53
|
+
self.ax_volume.xaxis.draw(renderer)
|
|
54
|
+
self.ax_volume.yaxis.draw(renderer)
|
|
55
|
+
|
|
56
|
+
self.collection_volume.draw(renderer)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BackgroundMixin(DrawMixin):
|
|
61
|
+
background = None
|
|
62
|
+
|
|
63
|
+
_creating_background = False
|
|
64
|
+
|
|
65
|
+
def _create_background(self):
|
|
66
|
+
if self._creating_background:
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
self._creating_background = True
|
|
70
|
+
self._copy_bbox()
|
|
71
|
+
self._creating_background = False
|
|
72
|
+
return
|
|
73
|
+
|
|
74
|
+
def _copy_bbox(self):
|
|
75
|
+
self.draw_artists()
|
|
76
|
+
|
|
77
|
+
renderer = self.figure.canvas.renderer
|
|
78
|
+
self.background = renderer.copy_from_bbox(self.figure.bbox)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
def _on_draw(self, e):
|
|
82
|
+
self.background = None
|
|
83
|
+
self._restore_region()
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
def _restore_region(self):
|
|
87
|
+
if not self.background:
|
|
88
|
+
self._create_background()
|
|
89
|
+
|
|
90
|
+
self.figure.canvas.renderer.restore_region(self.background)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class BaseMixin(BackgroundMixin):
|
|
95
|
+
def _refresh(self):
|
|
96
|
+
self.set_segments()
|
|
97
|
+
self.set_collections(self.vxmin, xmax=self.vxmax)
|
|
98
|
+
return super()._refresh()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Chart(BaseMixin):
|
|
102
|
+
pass
|
|
103
|
+
|