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/cursor.py
CHANGED
|
@@ -1,342 +1,380 @@
|
|
|
1
1
|
from fractions import Fraction
|
|
2
2
|
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
3
4
|
from matplotlib.backend_bases import MouseEvent
|
|
4
5
|
from matplotlib.collections import LineCollection
|
|
5
6
|
from matplotlib.text import Text
|
|
6
7
|
import pandas as pd
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
from .draw import DrawMixin, Chart as CM
|
|
9
|
+
from .draw import BaseMixin as BM, Mixin as M
|
|
10
10
|
from .utils import float_to_str
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
class Mixin:
|
|
14
|
-
def on_draw(self, e):
|
|
15
|
-
"This function works if draw event active."
|
|
16
|
-
return
|
|
13
|
+
class Mixin(M):
|
|
17
14
|
def on_move(self, e):
|
|
18
|
-
"
|
|
15
|
+
"If mouse move event active, This method work."
|
|
19
16
|
return
|
|
20
17
|
|
|
21
18
|
|
|
22
|
-
class CollectionMixin(
|
|
23
|
-
lineKwargs =
|
|
24
|
-
textboxKwargs =
|
|
19
|
+
class CollectionMixin(BM):
|
|
20
|
+
lineKwargs = {}
|
|
21
|
+
textboxKwargs = {}
|
|
22
|
+
textKwargs = {}
|
|
23
|
+
color_box = 'k'
|
|
25
24
|
|
|
26
25
|
def _add_collection(self):
|
|
27
26
|
super()._add_collection()
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.
|
|
35
|
-
|
|
27
|
+
|
|
28
|
+
lineKwargs = {'edgecolor': 'k', 'linewidth': 1, 'linestyle': '-'}
|
|
29
|
+
lineKwargs.update(self.lineKwargs)
|
|
30
|
+
lineKwargs.update({'segments': [], 'animated': True})
|
|
31
|
+
textboxKwargs = {'boxstyle': 'round', 'facecolor': 'w'}
|
|
32
|
+
textboxKwargs.update(self.textboxKwargs)
|
|
33
|
+
textKwargs = self.textKwargs
|
|
34
|
+
textKwargs.update({'animated': True, 'bbox': textboxKwargs, 'horizontalalignment': '', 'verticalalignment': ''})
|
|
35
|
+
(textKwargs.pop('horizontalalignment'), textKwargs.pop('verticalalignment'))
|
|
36
|
+
|
|
37
|
+
self.price_crossline = LineCollection(**lineKwargs)
|
|
38
|
+
self.ax_price.add_artist(self.price_crossline)
|
|
39
|
+
self.text_date_price = Text(**textKwargs, horizontalalignment='center', verticalalignment='bottom')
|
|
36
40
|
self.ax_price.add_artist(self.text_date_price)
|
|
37
|
-
self.text_price = Text(
|
|
41
|
+
self.text_price = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
|
|
38
42
|
self.ax_price.add_artist(self.text_price)
|
|
39
43
|
|
|
40
|
-
self.
|
|
41
|
-
self.ax_volume.add_artist(self.
|
|
42
|
-
self.text_date_volume = Text(
|
|
44
|
+
self.volume_crossline = LineCollection(**lineKwargs)
|
|
45
|
+
self.ax_volume.add_artist(self.volume_crossline)
|
|
46
|
+
self.text_date_volume = Text(**textKwargs, horizontalalignment='center', verticalalignment='top')
|
|
43
47
|
self.ax_volume.add_artist(self.text_date_volume)
|
|
44
|
-
self.text_volume = Text(
|
|
48
|
+
self.text_volume = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
|
|
45
49
|
self.ax_volume.add_artist(self.text_volume)
|
|
46
50
|
|
|
47
|
-
self.
|
|
48
|
-
self.ax_price.add_artist(self.price_hline)
|
|
49
|
-
self.price_box = LineCollection([], animated=True, linewidth=1.1, edgecolor='k')
|
|
51
|
+
self.price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
|
|
50
52
|
self.ax_price.add_artist(self.price_box)
|
|
51
|
-
self.text_price_info = Text(
|
|
53
|
+
self.text_price_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
|
|
52
54
|
self.ax_price.add_artist(self.text_price_info)
|
|
53
55
|
|
|
54
|
-
self.
|
|
55
|
-
self.ax_volume.add_artist(self.volume_hline)
|
|
56
|
-
self.volume_box = LineCollection([], animated=True, linewidth=1.1, edgecolor='k')
|
|
56
|
+
self.volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
|
|
57
57
|
self.ax_volume.add_artist(self.volume_box)
|
|
58
|
-
self.text_volume_info = Text(
|
|
58
|
+
self.text_volume_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
|
|
59
59
|
self.ax_volume.add_artist(self.text_volume_info)
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
def change_background_color(self, color):
|
|
63
|
+
super().change_background_color(color)
|
|
64
|
+
|
|
65
|
+
self.text_price.set_backgroundcolor(color)
|
|
66
|
+
self.text_volume.set_backgroundcolor(color)
|
|
67
|
+
|
|
68
|
+
self.text_date_price.set_backgroundcolor(color)
|
|
69
|
+
self.text_date_volume.set_backgroundcolor(color)
|
|
70
|
+
|
|
71
|
+
self.text_price_info.set_backgroundcolor(color)
|
|
72
|
+
self.text_volume_info.set_backgroundcolor(color)
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
def change_text_color(self, color):
|
|
76
|
+
super().change_text_color(color)
|
|
77
|
+
|
|
78
|
+
self.text_price.set_color(color)
|
|
79
|
+
self.text_volume.set_color(color)
|
|
80
|
+
|
|
81
|
+
self.text_date_price.set_color(color)
|
|
82
|
+
self.text_date_volume.set_color(color)
|
|
60
83
|
|
|
84
|
+
self.text_price_info.set_color(color)
|
|
85
|
+
self.text_volume_info.set_color(color)
|
|
61
86
|
return
|
|
62
87
|
|
|
88
|
+
def change_line_color(self, color):
|
|
89
|
+
self.price_crossline.set_edgecolor(color)
|
|
90
|
+
self.volume_crossline.set_edgecolor(color)
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
self.price_box.set_edgecolor(color)
|
|
93
|
+
self.volume_box.set_edgecolor(color)
|
|
94
|
+
|
|
95
|
+
self.text_price.get_bbox_patch().set_edgecolor(color)
|
|
96
|
+
self.text_volume.get_bbox_patch().set_edgecolor(color)
|
|
97
|
+
|
|
98
|
+
self.text_date_price.get_bbox_patch().set_edgecolor(color)
|
|
99
|
+
self.text_date_volume.get_bbox_patch().set_edgecolor(color)
|
|
100
|
+
|
|
101
|
+
self.text_price_info.get_bbox_patch().set_edgecolor(color)
|
|
102
|
+
self.text_volume_info.get_bbox_patch().set_edgecolor(color)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume', '_boxheight', '_boxmin', '_boxmax', '_volumeboxmax',}
|
|
65
107
|
|
|
66
108
|
class DataMixin(CollectionMixin):
|
|
67
|
-
def
|
|
68
|
-
|
|
109
|
+
def _validate_column_key(self):
|
|
110
|
+
super()._validate_column_key()
|
|
111
|
+
for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
|
|
69
112
|
v = getattr(self, i)
|
|
70
|
-
if v in _set_key:
|
|
71
|
-
|
|
113
|
+
if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
|
|
114
|
+
return
|
|
72
115
|
|
|
73
|
-
|
|
74
|
-
df
|
|
116
|
+
def _generate_data(self, df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *_, **__):
|
|
117
|
+
super()._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__)
|
|
75
118
|
|
|
76
119
|
if not calc_info:
|
|
77
120
|
keys = set(df.keys())
|
|
78
|
-
|
|
121
|
+
list_key = ['rate', 'compare', 'rate_open', 'rate_high', 'rate_low',]
|
|
122
|
+
if self.volume: list_key.append('rate_volume')
|
|
123
|
+
for i in list_key:
|
|
79
124
|
if i not in keys:
|
|
80
125
|
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
|
|
81
126
|
else:
|
|
82
|
-
df['
|
|
83
|
-
df['
|
|
84
|
-
df['rate_open'] = ((df[self.Open] - df[
|
|
85
|
-
df['rate_high'] = ((df[self.high] - df[
|
|
86
|
-
df['rate_low'] = ((df[self.low] - df[
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
127
|
+
self.df['compare'] = (self.df[self.close] - self.df['_pre']).fillna(0)
|
|
128
|
+
self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
129
|
+
self.df['rate_open'] = ((self.df[self.Open] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
130
|
+
self.df['rate_high'] = ((self.df[self.high] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
131
|
+
self.df['rate_low'] = ((self.df[self.low] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
132
|
+
if self.volume:
|
|
133
|
+
self.df['compare_volume'] = (self.df[self.volume] - self.df[self.volume].shift(1)).fillna(0)
|
|
134
|
+
self.df['rate_volume'] = (self.df['compare_volume'] / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
135
|
+
|
|
136
|
+
self.df['_boxheight'] = (self.df[self.high] - self.df[self.low]) / 5
|
|
137
|
+
self.df['_boxmin'] = self.df[self.low] - self.df['_boxheight']
|
|
138
|
+
self.df['_boxmax'] = self.df[self.high] + self.df['_boxheight']
|
|
139
|
+
if self.volume: self.df['_volumeboxmax'] = self.df[self.volume] * 1.13
|
|
90
140
|
return
|
|
91
141
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
x_distance = (vmax - vmin) / 30
|
|
95
|
-
self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
|
|
96
|
-
self.text_price.set_x(self.v0)
|
|
97
|
-
self.text_volume.set_x(self.v0)
|
|
142
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
143
|
+
super()._set_lim(xmin, xmax, simpler, set_ma)
|
|
98
144
|
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
145
|
+
psub = (self.price_ymax - self.price_ymin)
|
|
146
|
+
self.min_candleboxheight = psub / 8
|
|
101
147
|
|
|
102
|
-
|
|
103
|
-
self.
|
|
148
|
+
pydistance = psub / 20
|
|
149
|
+
self.text_date_price.set_y(self.price_ymin + pydistance)
|
|
104
150
|
|
|
105
|
-
|
|
106
|
-
y = (psub) / 20 + pmin
|
|
107
|
-
self.text_date_price.set_y(y)
|
|
108
|
-
# 주가 정보 y 위치
|
|
109
|
-
y = pmax - (psub) / 20
|
|
110
|
-
self.text_price_info.set_y(y)
|
|
151
|
+
self.min_volumeboxheight = self.volume_ymax / 4
|
|
111
152
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
self.text_date_volume.set_y(y)
|
|
115
|
-
# 거래량 정보 y 위치
|
|
116
|
-
self.text_volume_info.set_y(y)
|
|
153
|
+
vxsub = self.vxmax - self.vxmin
|
|
154
|
+
self.vmiddle = self.vxmax - int((vxsub) / 2)
|
|
117
155
|
|
|
118
|
-
|
|
156
|
+
vxdistance = vxsub / 50
|
|
157
|
+
self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
|
|
158
|
+
self.vsixth = self.vxmin + int((vxsub) / 6)
|
|
159
|
+
self.veighth = self.vxmin + int((vxsub) / 8)
|
|
160
|
+
|
|
161
|
+
yvolume = self.volume_ymax * 0.85
|
|
162
|
+
self.text_date_volume.set_y(yvolume)
|
|
119
163
|
|
|
164
|
+
# 정보 텍스트박스
|
|
165
|
+
self.text_price_info.set_y(self.price_ymax - pydistance)
|
|
166
|
+
self.text_volume_info.set_y(yvolume)
|
|
167
|
+
return
|
|
120
168
|
|
|
121
|
-
class LineMixin(DataMixin):
|
|
122
|
-
in_slider, in_price, in_volume = (False, False, False)
|
|
123
169
|
|
|
124
|
-
|
|
125
|
-
|
|
170
|
+
class EventMixin(DataMixin):
|
|
171
|
+
in_price_chart, in_volume_chart = (False, False)
|
|
172
|
+
intx = None
|
|
126
173
|
|
|
127
174
|
def _connect_event(self):
|
|
128
175
|
super()._connect_event()
|
|
129
|
-
self.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
176
|
+
self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
130
177
|
return
|
|
131
178
|
|
|
132
|
-
def
|
|
133
|
-
self.
|
|
179
|
+
def _on_move(self, e):
|
|
180
|
+
self._on_move_action(e)
|
|
134
181
|
return
|
|
135
182
|
|
|
136
|
-
def
|
|
137
|
-
|
|
138
|
-
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
|
|
139
|
-
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info=calc_info, *args, **kwargs)
|
|
183
|
+
def _on_move_action(self, e: MouseEvent):
|
|
184
|
+
self._check_ax(e)
|
|
140
185
|
|
|
141
|
-
self.
|
|
186
|
+
self.intx = None
|
|
187
|
+
if self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
142
188
|
return
|
|
143
189
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
self.
|
|
151
|
-
if self.in_price or self.in_volume:
|
|
152
|
-
self._chart_move_action(e)
|
|
190
|
+
def _check_ax(self, e: MouseEvent):
|
|
191
|
+
ax = e.inaxes
|
|
192
|
+
if not ax or e.xdata is None or e.ydata is None:
|
|
193
|
+
self.in_price_chart, self.in_volume_chart = (False, False)
|
|
194
|
+
else:
|
|
195
|
+
self.in_price_chart = ax is self.ax_price
|
|
196
|
+
self.in_volume_chart = False if self.in_price_chart else ax is self.ax_volume
|
|
153
197
|
|
|
154
|
-
self._blit()
|
|
155
198
|
return
|
|
156
199
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
200
|
+
def _get_x(self, e: MouseEvent):
|
|
201
|
+
self.intx = e.xdata.__int__()
|
|
202
|
+
if self.intx < 0: self.intx = None
|
|
160
203
|
else:
|
|
161
|
-
self.
|
|
162
|
-
|
|
163
|
-
self.intx = x.__int__()
|
|
164
|
-
if self.intx < 0: self.in_index = False
|
|
165
|
-
else:
|
|
166
|
-
try: self.df['x'][self.intx]
|
|
167
|
-
except: self.in_index = False
|
|
168
|
-
else: self.in_index = True
|
|
204
|
+
try: self.list_index[self.intx]
|
|
205
|
+
except: self.intx = None
|
|
169
206
|
return
|
|
170
207
|
|
|
171
|
-
def _check_ax(self, e: MouseEvent):
|
|
172
|
-
ax = e.inaxes
|
|
173
208
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return
|
|
209
|
+
class LineMixin(EventMixin):
|
|
210
|
+
digit_price, digit_volume = (0, 0)
|
|
211
|
+
in_candle, in_volumebar = (False, False)
|
|
178
212
|
|
|
179
|
-
def
|
|
180
|
-
|
|
213
|
+
def _on_move(self, e):
|
|
214
|
+
super()._on_move(e)
|
|
181
215
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
self.
|
|
216
|
+
self._restore_region()
|
|
217
|
+
|
|
218
|
+
if self.in_price_chart: self._on_move_price_chart(e)
|
|
219
|
+
elif self.in_volume_chart: self._on_move_volume_chart(e)
|
|
220
|
+
|
|
221
|
+
self._blit()
|
|
185
222
|
return
|
|
186
223
|
|
|
187
|
-
def
|
|
224
|
+
def _on_move_price_chart(self, e: MouseEvent):
|
|
188
225
|
x, y = (e.xdata, e.ydata)
|
|
189
|
-
if not y: return
|
|
190
|
-
roundy = y.__round__()
|
|
191
|
-
|
|
192
|
-
self.price_vline.set_segments([((x, self._price_ymin), (x, self._price_ymax))])
|
|
193
|
-
self.volumeh_vline.set_segments([((x, 0), (x, self._vol_ymax))])
|
|
194
|
-
self.ax_price.draw_artist(self.price_vline)
|
|
195
|
-
self.ax_volume.draw_artist(self.volumeh_vline)
|
|
196
226
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
227
|
+
self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
228
|
+
self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
|
|
229
|
+
self._draw_crossline()
|
|
200
230
|
|
|
201
|
-
|
|
202
|
-
# 수평선
|
|
203
|
-
self.price_hline.set_segments([((self.vmin, y), (self.vmax, y))])
|
|
204
|
-
self.ax_price.draw_artist(self.price_hline)
|
|
231
|
+
renderer = self.figure.canvas.renderer
|
|
205
232
|
|
|
206
233
|
# 가격
|
|
207
|
-
self.text_price.set_text(f'{
|
|
234
|
+
self.text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
|
|
235
|
+
self.text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
|
|
208
236
|
self.text_price.set_y(y)
|
|
209
|
-
self.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if self.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
low =
|
|
221
|
-
|
|
237
|
+
self.text_price.draw(renderer)
|
|
238
|
+
|
|
239
|
+
index = self.intx
|
|
240
|
+
if index is None: self.in_candle = False
|
|
241
|
+
else:
|
|
242
|
+
# 기준시간 표시
|
|
243
|
+
self.text_date_volume.set_text(f'{self.df[self.date][index]}')
|
|
244
|
+
self.text_date_volume.set_x(x)
|
|
245
|
+
self.text_date_volume.draw(renderer)
|
|
246
|
+
|
|
247
|
+
# 캔들 강조
|
|
248
|
+
low = self.df['_boxmin'][index]
|
|
249
|
+
high = self.df['_boxmax'][index]
|
|
250
|
+
sub = high - low
|
|
251
|
+
if sub < self.min_candleboxheight:
|
|
252
|
+
sub = (self.min_candleboxheight - sub) / 2
|
|
253
|
+
low -= sub
|
|
254
|
+
high += sub
|
|
255
|
+
|
|
256
|
+
if high < y or y < low: self.in_candle = False
|
|
222
257
|
else:
|
|
223
|
-
self.
|
|
224
|
-
x1, x2 = (
|
|
258
|
+
self.in_candle = True
|
|
259
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
225
260
|
self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
226
|
-
self.
|
|
261
|
+
self.price_box.draw(renderer)
|
|
227
262
|
return
|
|
228
263
|
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
self.
|
|
232
|
-
self.
|
|
264
|
+
def _draw_crossline(self):
|
|
265
|
+
renderer = self.figure.canvas.renderer
|
|
266
|
+
self.price_crossline.draw(renderer)
|
|
267
|
+
self.volume_crossline.draw(renderer)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
def _on_move_volume_chart(self, e: MouseEvent):
|
|
271
|
+
x, y = (e.xdata, e.ydata)
|
|
272
|
+
|
|
273
|
+
self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
|
|
274
|
+
self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
275
|
+
self._draw_crossline()
|
|
276
|
+
|
|
277
|
+
if not self.volume: return
|
|
278
|
+
|
|
279
|
+
renderer = self.figure.canvas.renderer
|
|
233
280
|
|
|
234
281
|
# 거래량
|
|
235
|
-
self.text_volume.set_text(f'{
|
|
282
|
+
self.text_volume.set_text(f'{float_to_str(y, self.digit_volume)}{self.unit_volume}')
|
|
283
|
+
self.text_volume.set_x(self.v0 if self.veighth < x else self.vsixth)
|
|
236
284
|
self.text_volume.set_y(y)
|
|
237
|
-
self.
|
|
285
|
+
self.text_volume.draw(renderer)
|
|
238
286
|
|
|
239
|
-
|
|
240
|
-
if self.
|
|
241
|
-
|
|
287
|
+
index = self.intx
|
|
288
|
+
if index is None: self.in_volumebar = False
|
|
289
|
+
else:
|
|
290
|
+
# 기준시간 표시
|
|
291
|
+
self.text_date_price.set_text(f'{self.df[self.date][index]}')
|
|
292
|
+
self.text_date_price.set_x(x)
|
|
293
|
+
self.text_date_price.draw(renderer)
|
|
242
294
|
|
|
243
|
-
|
|
295
|
+
# 거래량 강조
|
|
296
|
+
high = self.df[self.volume][index] * 1.15
|
|
244
297
|
low = 0
|
|
245
|
-
self.
|
|
246
|
-
|
|
298
|
+
if high < self.min_volumeboxheight: high = self.min_volumeboxheight
|
|
299
|
+
|
|
300
|
+
if high < y or y < low: self.in_volumebar = False
|
|
247
301
|
else:
|
|
248
|
-
self.
|
|
249
|
-
x1, x2 = (
|
|
302
|
+
self.in_volumebar = True
|
|
303
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
250
304
|
self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
251
|
-
self.
|
|
305
|
+
self.volume_box.draw(renderer)
|
|
252
306
|
return
|
|
253
307
|
|
|
254
308
|
|
|
309
|
+
format_candleinfo_ko = '{dt}\n\n종가: {close}\n등락률: {rate}\n대비: {compare}\n시가: {open}({rate_open})\n고가: {high}({rate_high})\n저가: {low}({rate_low})\n거래량: {volume}({rate_volume})'
|
|
310
|
+
format_volumeinfo_ko = '{dt}\n\n거래량: {volume}\n거래량증가율: {rate_volume}\n대비: {compare}'
|
|
311
|
+
format_candleinfo_en = '{dt}\n\nclose: {close}\nrate: {rate}\ncompare: {compare}\nopen: {open}({rate_open})\nhigh: {high}({rate_high})\nlow: {low}({rate_low})\nvolume: {volume}({rate_volume})'
|
|
312
|
+
format_volumeinfo_en = '{dt}\n\nvolume: {volume}\nvolume rate: {rate_volume}\ncompare: {compare}'
|
|
313
|
+
|
|
255
314
|
class InfoMixin(LineMixin):
|
|
256
315
|
fraction = False
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
digit_price, digit_volume = (0, 0)
|
|
260
|
-
|
|
261
|
-
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
|
|
262
|
-
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info, *args, **kwargs)
|
|
263
|
-
|
|
264
|
-
# 슬라이더 날짜 텍스트 y 위치
|
|
265
|
-
y = self._slider_ymax - (self._slider_ymax - self._slider_ymin) / 6
|
|
266
|
-
self.slider_text.set_y(y)
|
|
316
|
+
format_candleinfo = format_candleinfo_ko
|
|
317
|
+
format_volumeinfo = format_volumeinfo_ko
|
|
267
318
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
self.set_text_coordante(self.xmin, self.xmax, self._price_ymin, self._price_ymax, self._vol_ymax)
|
|
319
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
|
|
320
|
+
super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
|
|
271
321
|
|
|
322
|
+
self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
|
|
272
323
|
return
|
|
273
324
|
|
|
274
|
-
def
|
|
275
|
-
super().
|
|
325
|
+
def _on_move_price_chart(self, e):
|
|
326
|
+
super()._on_move_price_chart(e)
|
|
276
327
|
|
|
277
|
-
|
|
328
|
+
# 캔들 강조 확인
|
|
329
|
+
if not self.in_candle: return
|
|
278
330
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self.slider_text.set_x(e.xdata)
|
|
282
|
-
self.ax_slider.draw_artist(self.slider_text)
|
|
283
|
-
return
|
|
331
|
+
# 캔들 정보
|
|
332
|
+
self.text_price_info.set_text(self._get_info(self.intx))
|
|
284
333
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
self.
|
|
295
|
-
|
|
296
|
-
# 캔들 강조
|
|
297
|
-
if self.in_price and self._in_candle:
|
|
298
|
-
# 캔들 정보
|
|
299
|
-
self.text_price_info.set_text(self._get_info(intx))
|
|
300
|
-
if x < self.vmiddle:
|
|
301
|
-
# 텍스트박스 크기 가져오기
|
|
302
|
-
bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
303
|
-
width = bbox.x1 - bbox.x0
|
|
304
|
-
self.text_price_info.set_x(self.v1 - width)
|
|
305
|
-
else:
|
|
306
|
-
self.text_price_info.set_x(self.v0)
|
|
307
|
-
self.text_price_info.set_horizontalalignment('left')
|
|
308
|
-
self.ax_price.draw_artist(self.text_price_info)
|
|
334
|
+
if self.vmiddle < e.xdata: self.text_price_info.set_x(self.v0)
|
|
335
|
+
else:
|
|
336
|
+
# self.text_price_info.set_x(self.vmax - self.x_distance)
|
|
337
|
+
# self.text_price_info.set_horizontalalignment('right')
|
|
338
|
+
# 텍스트박스 크기 가져오기
|
|
339
|
+
bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
340
|
+
width = bbox.x1 - bbox.x0
|
|
341
|
+
self.text_price_info.set_x(self.v1 - width)
|
|
342
|
+
|
|
343
|
+
self.text_price_info.draw(self.figure.canvas.renderer)
|
|
309
344
|
return
|
|
310
345
|
|
|
311
|
-
def
|
|
312
|
-
super().
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
self.
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
#
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
self.text_volume_info.set_horizontalalignment('left')
|
|
331
|
-
self.text_volume_info.set_text(self._get_info(intx, False))
|
|
332
|
-
self.ax_volume.draw_artist(self.text_volume_info)
|
|
346
|
+
def _on_move_volume_chart(self, e):
|
|
347
|
+
super()._on_move_volume_chart(e)
|
|
348
|
+
|
|
349
|
+
# 거래량 강조 확인
|
|
350
|
+
if not self.in_volumebar: return
|
|
351
|
+
|
|
352
|
+
# 거래량 정보
|
|
353
|
+
self.text_volume_info.set_text(self._get_info(self.intx, is_price=False))
|
|
354
|
+
|
|
355
|
+
if self.vmiddle < e.xdata: self.text_volume_info.set_x(self.v0)
|
|
356
|
+
else:
|
|
357
|
+
# self.text_volume_info.set_x(self.vmax - self.x_distance)
|
|
358
|
+
# self.text_volume_info.set_horizontalalignment('right')
|
|
359
|
+
# 텍스트박스 크기 가져오기
|
|
360
|
+
bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
361
|
+
width = bbox.x1 - bbox.x0
|
|
362
|
+
self.text_volume_info.set_x(self.v1 - width)
|
|
363
|
+
|
|
364
|
+
self.text_volume_info.draw(self.figure.canvas.renderer)
|
|
333
365
|
return
|
|
334
366
|
|
|
335
367
|
def _get_info(self, index, is_price=True):
|
|
336
368
|
dt = self.df[self.date][index]
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
369
|
+
if not self.volume:
|
|
370
|
+
v, vr = ('-', '-%')
|
|
371
|
+
else:
|
|
372
|
+
v = self.df[self.volume][index]
|
|
373
|
+
v = float_to_str(v, self.digit_volume)
|
|
374
|
+
# if not v % 1: v = int(v)
|
|
375
|
+
vr = self.df['rate_volume'][index]
|
|
376
|
+
vr = f'{vr:+06,.2f}%'
|
|
377
|
+
|
|
340
378
|
if is_price:
|
|
341
379
|
o, h, l, c = (self.df[self.Open][index], self.df[self.high][index], self.df[self.low][index], self.df[self.close][index])
|
|
342
380
|
rate, compare = (self.df['rate'][index], self.df['compare'][index])
|
|
@@ -348,27 +386,23 @@ class InfoMixin(LineMixin):
|
|
|
348
386
|
cd = divmod(c, 1)
|
|
349
387
|
if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
|
|
350
388
|
else: c = float_to_str(cd[0])
|
|
351
|
-
|
|
352
389
|
comd = divmod(compare, 1)
|
|
353
390
|
if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
|
|
354
391
|
else: com = float_to_str(comd[0], plus=True)
|
|
355
|
-
|
|
356
392
|
o = o.__round__(self.digit_price)
|
|
357
393
|
od = divmod(o, 1)
|
|
358
394
|
if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
|
|
359
395
|
else: o = float_to_str(od[0])
|
|
360
|
-
|
|
361
396
|
h = h.__round__(self.digit_price)
|
|
362
397
|
hd = divmod(h, 1)
|
|
363
398
|
if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
|
|
364
399
|
else: h = float_to_str(hd[0])
|
|
365
|
-
|
|
366
400
|
l = l.__round__(self.digit_price)
|
|
367
401
|
ld = divmod(l, 1)
|
|
368
402
|
if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
|
|
369
403
|
else: l = float_to_str(ld[0])
|
|
370
404
|
|
|
371
|
-
text = self.
|
|
405
|
+
text = self.format_candleinfo.format(
|
|
372
406
|
dt=dt,
|
|
373
407
|
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
374
408
|
rate=f'{r:>{self._length_text}}%',
|
|
@@ -376,13 +410,13 @@ class InfoMixin(LineMixin):
|
|
|
376
410
|
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
377
411
|
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
378
412
|
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
379
|
-
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=
|
|
413
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
380
414
|
)
|
|
381
415
|
else:
|
|
382
416
|
o, h, l, c = (float_to_str(o, self.digit_price), float_to_str(h, self.digit_price), float_to_str(l, self.digit_price), float_to_str(c, self.digit_price))
|
|
383
417
|
com = float_to_str(compare, self.digit_price, plus=True)
|
|
384
418
|
|
|
385
|
-
text = self.
|
|
419
|
+
text = self.format_candleinfo.format(
|
|
386
420
|
dt=dt,
|
|
387
421
|
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
388
422
|
rate=f'{r:>{self._length_text}}%',
|
|
@@ -390,23 +424,38 @@ class InfoMixin(LineMixin):
|
|
|
390
424
|
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
391
425
|
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
392
426
|
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
393
|
-
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=
|
|
427
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
394
428
|
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
429
|
+
elif self.volume:
|
|
430
|
+
compare = self.df['compare_volume'][index]
|
|
431
|
+
com = float_to_str(compare, self.digit_volume, plus=True)
|
|
432
|
+
text = self.format_volumeinfo.format(
|
|
398
433
|
dt=dt,
|
|
399
434
|
volume=f'{v:>{self._length_text}}{self.unit_volume}',
|
|
400
|
-
rate_volume=f'{
|
|
435
|
+
rate_volume=f'{vr:>{self._length_text}}%',
|
|
436
|
+
compare=f'{com:>{self._length_text}}{self.unit_volume}',
|
|
401
437
|
)
|
|
438
|
+
else: text = ''
|
|
402
439
|
return text
|
|
403
440
|
|
|
404
441
|
|
|
405
|
-
class
|
|
442
|
+
class BaseMixin(InfoMixin):
|
|
406
443
|
pass
|
|
407
444
|
|
|
408
445
|
|
|
409
|
-
class Chart(
|
|
446
|
+
class Chart(BaseMixin, Mixin):
|
|
447
|
+
def _add_collection(self):
|
|
448
|
+
super()._add_collection()
|
|
449
|
+
return self.add_artist()
|
|
450
|
+
|
|
451
|
+
def _draw_artist(self):
|
|
452
|
+
super()._draw_artist()
|
|
453
|
+
return self.draw_artist()
|
|
454
|
+
|
|
455
|
+
def _get_segments(self):
|
|
456
|
+
self.generate_data()
|
|
457
|
+
return super()._get_segments()
|
|
458
|
+
|
|
410
459
|
def _on_draw(self, e):
|
|
411
460
|
super()._on_draw(e)
|
|
412
461
|
return self.on_draw(e)
|
|
@@ -415,34 +464,22 @@ class Chart(CursorMixin, CM, Mixin):
|
|
|
415
464
|
self.on_pick(e)
|
|
416
465
|
return super()._on_pick(e)
|
|
417
466
|
|
|
418
|
-
def
|
|
419
|
-
super().
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if __name__ == '__main__':
|
|
424
|
-
import json
|
|
425
|
-
from time import time
|
|
426
|
-
|
|
427
|
-
import matplotlib.pyplot as plt
|
|
428
|
-
from pathlib import Path
|
|
467
|
+
def _set_candle_segments(self, index_start, index_end):
|
|
468
|
+
super()._set_candle_segments(index_start, index_end)
|
|
469
|
+
self.set_segment(index_start, index_end)
|
|
470
|
+
return
|
|
429
471
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
n = 2600
|
|
435
|
-
data = data[n:n+100]
|
|
436
|
-
df = pd.DataFrame(data)
|
|
437
|
-
print(f'{df.keys()=}')
|
|
472
|
+
def _set_wick_segments(self, index_start, index_end, simpler=False):
|
|
473
|
+
super()._set_wick_segments(index_start, index_end, simpler)
|
|
474
|
+
self.set_segment(index_start, index_end, simpler)
|
|
475
|
+
return
|
|
438
476
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
c.set_data(df[['date', 'open', 'high', 'low', 'close', 'volume']])
|
|
444
|
-
t2 = time() - t
|
|
445
|
-
print(f'{t2=}')
|
|
446
|
-
plt.show()
|
|
477
|
+
def _set_line_segments(self, index_start, index_end, simpler=False, set_ma=True):
|
|
478
|
+
super()._set_line_segments(index_start, index_end, simpler, set_ma)
|
|
479
|
+
self.set_segment(index_start, index_end, simpler, set_ma)
|
|
480
|
+
return
|
|
447
481
|
|
|
482
|
+
def _on_move(self, e):
|
|
483
|
+
super()._on_move(e)
|
|
484
|
+
return self.on_move(e)
|
|
448
485
|
|