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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import matplotlib.style as mplstyle
|
|
3
|
+
from matplotlib.axes import Axes
|
|
4
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
|
|
5
|
+
from matplotlib.backend_bases import FigureManagerBase
|
|
6
|
+
from matplotlib.figure import Figure as Fig
|
|
7
|
+
from matplotlib.text import Text
|
|
8
|
+
|
|
9
|
+
from .._config import DEFAULTCONFIG
|
|
10
|
+
|
|
11
|
+
try: plt.switch_backend('TkAgg')
|
|
12
|
+
except: pass
|
|
13
|
+
|
|
14
|
+
# 한글 깨짐 문제 방지
|
|
15
|
+
try: plt.rcParams['font.family'] ='Malgun Gothic'
|
|
16
|
+
except: pass
|
|
17
|
+
|
|
18
|
+
mplstyle.use('fast')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Canvas(FigureCanvasAgg):
|
|
22
|
+
manager: FigureManagerBase
|
|
23
|
+
renderer = RendererAgg
|
|
24
|
+
|
|
25
|
+
class Figure(Fig):
|
|
26
|
+
canvas: Canvas
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Base:
|
|
30
|
+
watermark = 'seolpyo mplchart'
|
|
31
|
+
|
|
32
|
+
figure: Figure
|
|
33
|
+
|
|
34
|
+
def __init__(self, config=DEFAULTCONFIG):
|
|
35
|
+
# 기본 툴바 비활성화
|
|
36
|
+
plt.rcParams['toolbar'] = 'None'
|
|
37
|
+
# plt.rcParams['figure.dpi'] = 600
|
|
38
|
+
|
|
39
|
+
self.CONFIG = config
|
|
40
|
+
self.add_axes()
|
|
41
|
+
self.set_window()
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def add_axes(self):
|
|
45
|
+
self.figure, axes = plt.subplots(
|
|
46
|
+
3, # row 수
|
|
47
|
+
figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
|
|
48
|
+
height_ratios=(
|
|
49
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
50
|
+
self.CONFIG.FIGURE.RATIO.price,
|
|
51
|
+
self.CONFIG.FIGURE.RATIO.volume,
|
|
52
|
+
) # row 크기 비율
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
axes: list[Axes]
|
|
56
|
+
self.ax_legend, self.ax_price, self.ax_volume = axes
|
|
57
|
+
self.ax_legend.set_label('legend ax')
|
|
58
|
+
self.ax_price.set_label('price ax')
|
|
59
|
+
self.ax_volume.set_label('volume ax')
|
|
60
|
+
|
|
61
|
+
# 이평선 라벨 axis 그리지 않기
|
|
62
|
+
self.ax_legend.set_axis_off()
|
|
63
|
+
|
|
64
|
+
# y ticklabel foramt 설정
|
|
65
|
+
self.ax_price.yaxis.set_major_formatter(
|
|
66
|
+
lambda x, _: self.CONFIG.UNIT.func(
|
|
67
|
+
x,
|
|
68
|
+
word=self.CONFIG.UNIT.price,
|
|
69
|
+
digit=self.CONFIG.UNIT.digit
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
self.ax_volume.yaxis.set_major_formatter(
|
|
73
|
+
lambda x, _: self.CONFIG.UNIT.func(
|
|
74
|
+
x,
|
|
75
|
+
word=self.CONFIG.UNIT.volume,
|
|
76
|
+
digit=self.CONFIG.UNIT.digit
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# 공통 설정
|
|
81
|
+
for ax in (self.ax_price, self.ax_volume):
|
|
82
|
+
ax.xaxis.set_animated(True)
|
|
83
|
+
ax.yaxis.set_animated(True)
|
|
84
|
+
|
|
85
|
+
# x tick 외부 눈금 표시하지 않기
|
|
86
|
+
ax.xaxis.set_ticks_position('none')
|
|
87
|
+
# x tick label 제거
|
|
88
|
+
ax.set_xticklabels([])
|
|
89
|
+
# y tick 위치를 우측으로 이동
|
|
90
|
+
ax.tick_params(left=False, right=True, labelleft=False, labelright=True)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_window(self):
|
|
95
|
+
self._set_figure()
|
|
96
|
+
self._set_axes()
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
def _set_figure(self):
|
|
100
|
+
self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
|
|
101
|
+
|
|
102
|
+
# 차트 비율 변경
|
|
103
|
+
# print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
|
|
104
|
+
gs = self.ax_price.get_subplotspec().get_gridspec()
|
|
105
|
+
gs.set_height_ratios([
|
|
106
|
+
self.CONFIG.FIGURE.RATIO.legend,
|
|
107
|
+
self.CONFIG.FIGURE.RATIO.price,
|
|
108
|
+
self.CONFIG.FIGURE.RATIO.volume,
|
|
109
|
+
])
|
|
110
|
+
# print(f'{gs.get_height_ratios()=}')
|
|
111
|
+
self.figure.tight_layout()
|
|
112
|
+
|
|
113
|
+
# 플롯간 간격 설정(Configure subplots)
|
|
114
|
+
self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
|
|
115
|
+
|
|
116
|
+
self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
def _set_axes(self):
|
|
120
|
+
self._set_axes_legend()
|
|
121
|
+
|
|
122
|
+
# 공통 설정
|
|
123
|
+
for ax in (self.ax_price, self.ax_volume):
|
|
124
|
+
# 차트 영역 배경 색상
|
|
125
|
+
ax.set_facecolor(self.CONFIG.AX.facecolor)
|
|
126
|
+
|
|
127
|
+
# Axes 외곽선 색 변경(틱 색과 일치)
|
|
128
|
+
for i in ['top', 'bottom', 'left', 'right']:
|
|
129
|
+
ax.spines[i].set_color(self.CONFIG.AX.TICK.edgecolor)
|
|
130
|
+
# 틱 색상
|
|
131
|
+
ax.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
132
|
+
# 틱 라벨 색상
|
|
133
|
+
ticklabels: list[Text] = ax.get_xticklabels() + ax.get_yticklabels()
|
|
134
|
+
for ticklabel in ticklabels:
|
|
135
|
+
ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
|
|
136
|
+
|
|
137
|
+
# Axes grid(구분선, 격자) 그리기
|
|
138
|
+
# 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
|
|
139
|
+
ax.grid(**self.CONFIG.AX.GRID.__dict__)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def _set_axes_legend(self):
|
|
143
|
+
color = self.CONFIG.AX.facecolor
|
|
144
|
+
|
|
145
|
+
# 이평선 라벨 Axes 배경색
|
|
146
|
+
legends = self.ax_legend.get_legend()
|
|
147
|
+
if legends:
|
|
148
|
+
legends.get_frame().set_facecolor(color)
|
|
149
|
+
|
|
150
|
+
# 이평선 라벨 Axes 테두리색
|
|
151
|
+
color = self.CONFIG.AX.TICK.edgecolor
|
|
152
|
+
legends = self.ax_legend.get_legend()
|
|
153
|
+
if legends:
|
|
154
|
+
legends.get_frame().set_edgecolor(color)
|
|
155
|
+
|
|
156
|
+
# 이평선 라벨 폰트 색상
|
|
157
|
+
color = self.CONFIG.AX.TICK.fontcolor
|
|
158
|
+
legends = self.ax_legend.get_legend()
|
|
159
|
+
if legends:
|
|
160
|
+
legend_labels: list[Text] = legends.texts
|
|
161
|
+
for i in legend_labels:
|
|
162
|
+
i.set_color(color)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ArtistMixin(Base):
|
|
167
|
+
def __init__(self, *args, **kwargs):
|
|
168
|
+
super().__init__(*args, **kwargs)
|
|
169
|
+
|
|
170
|
+
self.add_artists()
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
def add_artists(self):
|
|
174
|
+
self._add_artists()
|
|
175
|
+
return
|
|
176
|
+
|
|
177
|
+
def _add_artists(self):
|
|
178
|
+
self._add_watermark()
|
|
179
|
+
self._set_wartermark()
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
def set_artists(self):
|
|
183
|
+
self._set_artists()
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def _set_artists(self):
|
|
187
|
+
self._set_wartermark()
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
def _add_watermark(self):
|
|
191
|
+
self.artist_watermark = Text(
|
|
192
|
+
x=0.5, y=0.5, text='',
|
|
193
|
+
animated=True,
|
|
194
|
+
horizontalalignment='center', verticalalignment='center',
|
|
195
|
+
transform=self.ax_price.transAxes
|
|
196
|
+
)
|
|
197
|
+
self.ax_price.add_artist(self.artist_watermark)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
def _set_wartermark(self):
|
|
201
|
+
self.artist_watermark.set_text(self.watermark)
|
|
202
|
+
self.artist_watermark.set_fontsize(self.CONFIG.FIGURE.WATERMARK.fontsize)
|
|
203
|
+
self.artist_watermark.set_color(self.CONFIG.FIGURE.WATERMARK.color)
|
|
204
|
+
self.artist_watermark.set_alpha(self.CONFIG.FIGURE.WATERMARK.alpha)
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class BaseMixin(ArtistMixin):
|
|
209
|
+
def refresh(self):
|
|
210
|
+
self._refresh()
|
|
211
|
+
self.figure.canvas.draw()
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
def _refresh(self):
|
|
215
|
+
self.set_window()
|
|
216
|
+
self.set_artists()
|
|
217
|
+
return
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
from matplotlib.collections import LineCollection
|
|
2
|
+
from matplotlib.text import Text
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .._draw import BaseMixin as Base
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ArtistMixin(Base):
|
|
9
|
+
def _add_artists(self):
|
|
10
|
+
super()._add_artists()
|
|
11
|
+
|
|
12
|
+
self._add_crosslines()
|
|
13
|
+
self._set_crosslines()
|
|
14
|
+
|
|
15
|
+
self._add_label_texts()
|
|
16
|
+
self._set_label_texts()
|
|
17
|
+
|
|
18
|
+
self._add_info_texts()
|
|
19
|
+
self._set_info_texts()
|
|
20
|
+
|
|
21
|
+
self._add_box_collections()
|
|
22
|
+
self._set_box_collections()
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
def _set_artists(self):
|
|
26
|
+
super()._set_artists()
|
|
27
|
+
|
|
28
|
+
self._set_crosslines()
|
|
29
|
+
self._set_label_texts()
|
|
30
|
+
self._set_info_texts()
|
|
31
|
+
self._set_box_collections()
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
def _add_crosslines(self):
|
|
35
|
+
kwargs = {'segments': [], 'animated': True}
|
|
36
|
+
|
|
37
|
+
self.collection_price_crossline = LineCollection(**kwargs)
|
|
38
|
+
self.ax_price.add_artist(self.collection_price_crossline)
|
|
39
|
+
|
|
40
|
+
self.collection_volume_crossline = LineCollection(**kwargs)
|
|
41
|
+
self.ax_volume.add_artist(self.collection_volume_crossline)
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
def _set_crosslines(self):
|
|
45
|
+
kwargs = self.CONFIG.CURSOR.CROSSLINE.__dict__
|
|
46
|
+
kwargs.update({'segments': [], 'animated': True})
|
|
47
|
+
|
|
48
|
+
self.collection_price_crossline.set(**kwargs)
|
|
49
|
+
self.collection_volume_crossline.set(**kwargs)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
def _add_label_texts(self):
|
|
53
|
+
kwargs = {'text': '', 'animated': True, 'horizontalalignment': 'center', 'verticalalignment': 'top'}
|
|
54
|
+
|
|
55
|
+
self.artist_text_x = Text(**kwargs)
|
|
56
|
+
self.figure.add_artist(self.artist_text_x)
|
|
57
|
+
|
|
58
|
+
kwargs.update({'horizontalalignment': 'left', 'verticalalignment': 'center'})
|
|
59
|
+
self.artist_text_y = Text(**kwargs)
|
|
60
|
+
self.figure.add_artist(self.artist_text_y)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
def _set_label_texts(self):
|
|
64
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
65
|
+
kwargs.update({'text': ' ', 'animated': True, 'horizontalalignment': 'center', 'verticalalignment': 'top'})
|
|
66
|
+
|
|
67
|
+
self.artist_text_x.set(**kwargs)
|
|
68
|
+
|
|
69
|
+
kwargs.update({'horizontalalignment': 'left', 'verticalalignment': 'center'})
|
|
70
|
+
self.artist_text_y.set(**kwargs)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def _add_box_collections(self):
|
|
74
|
+
kwargs = {'segments': [], 'animated': True,}
|
|
75
|
+
|
|
76
|
+
self.collection_box_price = LineCollection(**kwargs)
|
|
77
|
+
self.ax_price.add_artist(self.collection_box_price)
|
|
78
|
+
self.collection_box_volume = LineCollection(**kwargs)
|
|
79
|
+
self.ax_volume.add_artist(self.collection_box_volume)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
def _set_box_collections(self):
|
|
83
|
+
kwargs = self.CONFIG.CURSOR.BOX.__dict__
|
|
84
|
+
kwargs.update({'segments': [], 'animated': True,})
|
|
85
|
+
|
|
86
|
+
self.collection_box_price.set(**kwargs)
|
|
87
|
+
self.collection_box_volume.set(**kwargs)
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
def _add_info_texts(self):
|
|
91
|
+
kwargs = {'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',}
|
|
92
|
+
|
|
93
|
+
self.artist_info_candle = Text(**kwargs)
|
|
94
|
+
self.ax_price.add_artist(self.artist_info_candle)
|
|
95
|
+
self.artist_info_volume = Text(**kwargs)
|
|
96
|
+
self.ax_volume.add_artist(self.artist_info_volume)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
def _set_info_texts(self):
|
|
100
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
101
|
+
kwargs.update({'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',})
|
|
102
|
+
|
|
103
|
+
self.artist_info_candle.set(**kwargs)
|
|
104
|
+
self.artist_info_volume.set(**kwargs)
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DataMixin(ArtistMixin):
|
|
109
|
+
def _set_data(self, df: pd.DataFrame, *args, **kwargs):
|
|
110
|
+
super()._set_data(df, *args, **kwargs)
|
|
111
|
+
|
|
112
|
+
self.df['compare'] = (self.df['close'] - self.df['pre_close']).fillna(0)
|
|
113
|
+
self.df['rate'] = (self.df['compare'] * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
114
|
+
self.df['rate_open'] = ((self.df['open'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
115
|
+
self.df['rate_high'] = ((self.df['high'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
116
|
+
self.df['rate_low'] = ((self.df['low'] - self.df['pre_close']) * 100 / self.df['pre_close']).__round__(2).fillna(0)
|
|
117
|
+
if self.key_volume:
|
|
118
|
+
self.df['pre_volume'] = self.df['volume'].shift(1)
|
|
119
|
+
self.df['compare_volume'] = (self.df['volume'] - self.df['pre_volume']).fillna(0)
|
|
120
|
+
self.df['rate_volume'] = (self.df['compare_volume'] * 100 / self.df['pre_volume']).__round__(2).fillna(0)
|
|
121
|
+
|
|
122
|
+
self.df['space_box_candle'] = (self.df['high'] - self.df['low']) / 5
|
|
123
|
+
self.df['bottom_box_candle'] = self.df['low'] - self.df['space_box_candle']
|
|
124
|
+
self.df['top_box_candle'] = self.df['high'] + self.df['space_box_candle']
|
|
125
|
+
self.df['height_box_candle'] = self.df['top_box_candle'] - self.df['bottom_box_candle']
|
|
126
|
+
if self.key_volume: self.df['max_box_volume'] = self.df['volume'] * 1.15
|
|
127
|
+
|
|
128
|
+
self._set_label_texts_position()
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
def _refresh(self):
|
|
132
|
+
super()._refresh()
|
|
133
|
+
|
|
134
|
+
self._set_label_texts_position()
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
def _axis(self, xmin, xmax, simpler=False, draw_ma=True):
|
|
138
|
+
super()._axis(xmin, xmax=xmax, simpler=simpler, draw_ma=draw_ma)
|
|
139
|
+
|
|
140
|
+
psub = (self.price_ymax - self.price_ymin)
|
|
141
|
+
self.min_height_box_candle = psub / 8
|
|
142
|
+
|
|
143
|
+
pydistance = psub / 20
|
|
144
|
+
|
|
145
|
+
self.min_height_box_volume = 10
|
|
146
|
+
if self.key_volume:
|
|
147
|
+
self.min_height_box_volume = self.key_volume_ymax / 4
|
|
148
|
+
|
|
149
|
+
vxsub = self.vxmax - self.vxmin
|
|
150
|
+
self.vmiddle = self.vxmax - int((vxsub) / 2)
|
|
151
|
+
|
|
152
|
+
vxdistance = vxsub / 50
|
|
153
|
+
self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
|
|
154
|
+
|
|
155
|
+
yvolume = self.key_volume_ymax * 0.85
|
|
156
|
+
|
|
157
|
+
# 정보 텍스트박스 y축 설정
|
|
158
|
+
self.artist_info_candle.set_y(self.price_ymax - pydistance)
|
|
159
|
+
self.artist_info_volume.set_y(yvolume)
|
|
160
|
+
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
def _set_label_texts_position(self):
|
|
164
|
+
self._set_label_x_position()
|
|
165
|
+
self._set_label_y_position()
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
def _set_label_x_position(self):
|
|
169
|
+
renderer = getattr(self.figure.canvas, 'renderer', None)
|
|
170
|
+
if not renderer:
|
|
171
|
+
renderer
|
|
172
|
+
|
|
173
|
+
# Axes 우측 경계 좌표
|
|
174
|
+
x1 = self.ax_price.get_position().x1
|
|
175
|
+
# Text bbox 너비
|
|
176
|
+
bbox = self.artist_text_y.get_bbox_patch()\
|
|
177
|
+
.get_window_extent(renderer)
|
|
178
|
+
bbox_width = bbox.width
|
|
179
|
+
# 밀어야 하는 x값
|
|
180
|
+
fig_width = self.figure.bbox.width
|
|
181
|
+
# fig_width = self.figure.get_size_inches()[0] * self.figure.dpi
|
|
182
|
+
box_width_fig = (bbox_width+14) / fig_width
|
|
183
|
+
# print(f'{box_width_fig=}')
|
|
184
|
+
|
|
185
|
+
# x축 값(가격 또는 거래량)
|
|
186
|
+
# self.artist_text_y.set_x(x1)
|
|
187
|
+
x = x1 + (box_width_fig / 2)
|
|
188
|
+
# print(f'{(x1, x)=}')
|
|
189
|
+
self.artist_text_y.set_x(x)
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
def _set_label_y_position(self):
|
|
193
|
+
renderer = getattr(self.figure.canvas, 'renderer', None)
|
|
194
|
+
if not renderer:
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
# Axes 하단 경계 좌표
|
|
198
|
+
y0 = self.ax_volume.get_position().y0
|
|
199
|
+
# Text bbox 높이
|
|
200
|
+
bbox = self.artist_text_x.get_bbox_patch()\
|
|
201
|
+
.get_window_extent(renderer)
|
|
202
|
+
height_px = bbox.height
|
|
203
|
+
# print(f'{height_px=}')
|
|
204
|
+
# 밀어야 하는 y값
|
|
205
|
+
fig_height_px = self.figure.bbox.height
|
|
206
|
+
box_height_fig = (height_px+14) / fig_height_px
|
|
207
|
+
|
|
208
|
+
# y축 값(날짜)
|
|
209
|
+
y = y0 - (box_height_fig/2)
|
|
210
|
+
# print(f'{(y0, y)=}')
|
|
211
|
+
self.artist_text_x.set_y(y)
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class BaseMixin(DataMixin):
|
|
216
|
+
pass
|
|
217
|
+
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from matplotlib.backend_bases import MouseEvent
|
|
2
|
+
|
|
3
|
+
from ._artist import BaseMixin as Base
|
|
4
|
+
from ..._config.utils import float_to_str
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class EventMixin(Base):
|
|
8
|
+
in_price_chart, in_volume_chart = (False, False)
|
|
9
|
+
intx = None
|
|
10
|
+
|
|
11
|
+
def _connect_events(self):
|
|
12
|
+
super()._connect_events()
|
|
13
|
+
|
|
14
|
+
self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self.on_move(x))
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
def on_move(self, e):
|
|
18
|
+
self._on_move_action(e)
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def _on_move_action(self, e: MouseEvent):
|
|
22
|
+
self._check_ax(e)
|
|
23
|
+
|
|
24
|
+
self.intx = None
|
|
25
|
+
if self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
def _get_x(self, e: MouseEvent):
|
|
29
|
+
self.intx = e.xdata.__int__()
|
|
30
|
+
if self.intx < 0: self.intx = None
|
|
31
|
+
else:
|
|
32
|
+
try: self.index_list[self.intx]
|
|
33
|
+
except: self.intx = None
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
def _check_ax(self, e: MouseEvent):
|
|
37
|
+
ax = e.inaxes
|
|
38
|
+
if not ax or e.xdata is None or e.ydata is None:
|
|
39
|
+
self.in_price_chart, self.in_volume_chart = (False, False)
|
|
40
|
+
else:
|
|
41
|
+
if ax is self.ax_price:
|
|
42
|
+
self.in_price_chart = True
|
|
43
|
+
self.in_volume_chart = False
|
|
44
|
+
elif ax is self.ax_volume:
|
|
45
|
+
self.in_price_chart = False
|
|
46
|
+
self.in_volume_chart = True
|
|
47
|
+
else:
|
|
48
|
+
self.in_price_chart = False
|
|
49
|
+
self.in_volume_chart = False
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CrossLineMixin(EventMixin):
|
|
54
|
+
in_candle, in_volumebar = (False, False)
|
|
55
|
+
|
|
56
|
+
def _on_move_action(self, e):
|
|
57
|
+
super()._on_move_action(e)
|
|
58
|
+
|
|
59
|
+
if self.in_price_chart or self.in_volume_chart:
|
|
60
|
+
self._restore_region()
|
|
61
|
+
self._draw_crossline(e)
|
|
62
|
+
self.figure.canvas.blit()
|
|
63
|
+
else:
|
|
64
|
+
if self._erase_crossline():
|
|
65
|
+
self._restore_region()
|
|
66
|
+
self.figure.canvas.blit()
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
def _erase_crossline(self):
|
|
70
|
+
seg = self.collection_price_crossline.get_segments()
|
|
71
|
+
if seg:
|
|
72
|
+
self.collection_price_crossline.set_segments([])
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def _draw_crossline(self, e: MouseEvent):
|
|
77
|
+
x, y = (e.xdata, e.ydata)
|
|
78
|
+
|
|
79
|
+
if self.in_price_chart:
|
|
80
|
+
self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
81
|
+
self.collection_volume_crossline.set_segments([((x, 0), (x, self.key_volume_ymax))])
|
|
82
|
+
else:
|
|
83
|
+
self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
|
|
84
|
+
self.collection_volume_crossline.set_segments([((x, 0), (x, self.key_volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
85
|
+
|
|
86
|
+
renderer = self.figure.canvas.renderer
|
|
87
|
+
self.collection_price_crossline.draw(renderer)
|
|
88
|
+
self.collection_volume_crossline.draw(renderer)
|
|
89
|
+
|
|
90
|
+
self._draw_text_artist(e)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
def _draw_text_artist(self, e: MouseEvent):
|
|
94
|
+
x, y = (e.xdata, e.ydata)
|
|
95
|
+
|
|
96
|
+
display_coords = e.inaxes.transData.transform((e.xdata, e.ydata))
|
|
97
|
+
figure_coords = self.figure.transFigure.inverted().transform(display_coords)
|
|
98
|
+
# print(f'{figure_coords=}')
|
|
99
|
+
|
|
100
|
+
renderer = self.figure.canvas.renderer
|
|
101
|
+
if self.in_price_chart:
|
|
102
|
+
text = f'{float_to_str(y, digit=self.CONFIG.UNIT.digit)}{self.CONFIG.UNIT.price}'
|
|
103
|
+
else:
|
|
104
|
+
text = f'{float_to_str(y, digit=self.CONFIG.UNIT.digit_volume)}{self.CONFIG.UNIT.volume}'
|
|
105
|
+
|
|
106
|
+
# y축 값(가격 또는 거래량)
|
|
107
|
+
self.artist_text_y.set_text(text)
|
|
108
|
+
self.artist_text_y.set_y(figure_coords[1])
|
|
109
|
+
self.artist_text_y.draw(renderer)
|
|
110
|
+
|
|
111
|
+
if self.intx is not None:
|
|
112
|
+
# x축 값(날짜)
|
|
113
|
+
self.artist_text_x.set_text(f'{self.df['date'][self.intx]}')
|
|
114
|
+
self.artist_text_x.set_x(figure_coords[0])
|
|
115
|
+
self.artist_text_x.draw(renderer)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class BoxMixin(CrossLineMixin):
|
|
120
|
+
def _draw_crossline(self, e):
|
|
121
|
+
super()._draw_crossline(e)
|
|
122
|
+
self._draw_box_artist(e)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
def _draw_box_artist(self, e: MouseEvent):
|
|
126
|
+
y = e.ydata
|
|
127
|
+
|
|
128
|
+
renderer = self.figure.canvas.renderer
|
|
129
|
+
|
|
130
|
+
self.in_candle = False
|
|
131
|
+
self.in_volumebar = False
|
|
132
|
+
if self.intx is not None:
|
|
133
|
+
if self.in_price_chart:
|
|
134
|
+
# 박스 크기
|
|
135
|
+
high = self.df['top_box_candle'][self.intx]
|
|
136
|
+
low = self.df['bottom_box_candle'][self.intx]
|
|
137
|
+
height = self.df['height_box_candle'][self.intx]
|
|
138
|
+
if height < self.min_height_box_candle:
|
|
139
|
+
sub = (self.min_height_box_candle - height) / 2
|
|
140
|
+
high, low = (high+sub, low-sub)
|
|
141
|
+
|
|
142
|
+
# 커서가 캔들 사이에 있는지 확인
|
|
143
|
+
if low <= y and y <= high:
|
|
144
|
+
# 캔들 강조
|
|
145
|
+
self.in_candle = True
|
|
146
|
+
x1, x2 = (self.intx-0.3, self.intx+1.3)
|
|
147
|
+
self.collection_box_price.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
148
|
+
self.collection_box_price.draw(renderer)
|
|
149
|
+
elif self.in_volume_chart and self.key_volume:
|
|
150
|
+
# 거래량 강조
|
|
151
|
+
high = self.df['max_box_volume'][self.intx]
|
|
152
|
+
low = 0
|
|
153
|
+
if high < self.min_height_box_volume: high = self.min_height_box_volume
|
|
154
|
+
|
|
155
|
+
if low <= y and y <= high:
|
|
156
|
+
self.in_volumebar = True
|
|
157
|
+
x1, x2 = (self.intx-0.3, self.intx+1.3)
|
|
158
|
+
self.collection_box_volume.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
159
|
+
self.collection_box_volume.draw(renderer)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class BaseMixin(BoxMixin):
|
|
164
|
+
pass
|
|
165
|
+
|