seolpyo-mplchart 0.1.3.3__py3-none-any.whl → 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of seolpyo-mplchart might be problematic. Click here for more details.
- seolpyo_mplchart/__init__.py +221 -70
- seolpyo_mplchart/base.py +70 -67
- seolpyo_mplchart/cursor.py +246 -272
- seolpyo_mplchart/draw.py +383 -238
- seolpyo_mplchart/slider.py +414 -405
- seolpyo_mplchart/test.py +5 -32
- seolpyo_mplchart/utils.py +15 -4
- {seolpyo_mplchart-0.1.3.3.dist-info → seolpyo_mplchart-1.0.0.dist-info}/METADATA +5 -7
- seolpyo_mplchart-1.0.0.dist-info/RECORD +13 -0
- seolpyo_mplchart-0.1.3.3.dist-info/RECORD +0 -13
- {seolpyo_mplchart-0.1.3.3.dist-info → seolpyo_mplchart-1.0.0.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-0.1.3.3.dist-info → seolpyo_mplchart-1.0.0.dist-info}/top_level.txt +0 -0
seolpyo_mplchart/cursor.py
CHANGED
|
@@ -1,342 +1,335 @@
|
|
|
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
|
-
|
|
61
60
|
return
|
|
62
61
|
|
|
63
62
|
|
|
64
|
-
_set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
|
|
63
|
+
_set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume', '_boxheight', '_boxmin', '_boxmax', '_volumeboxmax',}
|
|
65
64
|
|
|
66
65
|
class DataMixin(CollectionMixin):
|
|
67
|
-
def
|
|
68
|
-
|
|
66
|
+
def _validate_column_key(self):
|
|
67
|
+
super()._validate_column_key()
|
|
68
|
+
for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
|
|
69
69
|
v = getattr(self, i)
|
|
70
|
-
if v in _set_key:
|
|
71
|
-
|
|
70
|
+
if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
|
|
71
|
+
return
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
df
|
|
73
|
+
def _generate_data(self, df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *_, **__):
|
|
74
|
+
super()._generate_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, *_, **__)
|
|
75
75
|
|
|
76
76
|
if not calc_info:
|
|
77
77
|
keys = set(df.keys())
|
|
78
|
-
|
|
78
|
+
list_key = ['rate', 'compare', 'rate_open', 'rate_high', 'rate_low',]
|
|
79
|
+
if self.volume: list_key.append('rate_volume')
|
|
80
|
+
for i in list_key:
|
|
79
81
|
if i not in keys:
|
|
80
82
|
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
|
|
81
83
|
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
|
-
df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
88
|
-
|
|
89
|
-
self.df = df
|
|
84
|
+
self.df['compare'] = (self.df[self.close] - self.df['_pre']).fillna(0)
|
|
85
|
+
self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
86
|
+
self.df['rate_open'] = ((self.df[self.Open] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
87
|
+
self.df['rate_high'] = ((self.df[self.high] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
88
|
+
self.df['rate_low'] = ((self.df[self.low] - self.df['_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
|
|
89
|
+
if self.volume: self.df['rate_volume'] = ((self.df[self.volume] - self.df[self.volume].shift(1)) / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
90
|
+
|
|
91
|
+
self.df['_boxheight'] = (self.df[self.high] - self.df[self.low]) / 5
|
|
92
|
+
self.df['_boxmin'] = self.df[self.low] - self.df['_boxheight']
|
|
93
|
+
self.df['_boxmax'] = self.df[self.high] + self.df['_boxheight']
|
|
94
|
+
if self.volume: self.df['_volumeboxmax'] = self.df[self.volume] * 1.13
|
|
90
95
|
return
|
|
91
96
|
|
|
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)
|
|
97
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
98
|
+
super()._set_lim(xmin, xmax, simpler, set_ma)
|
|
98
99
|
|
|
99
|
-
self.
|
|
100
|
-
self.
|
|
100
|
+
psub = (self.price_ymax - self.price_ymin)
|
|
101
|
+
self.min_candleboxheight = psub / 8
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
self.
|
|
103
|
+
pydistance = psub / 20
|
|
104
|
+
self.text_date_price.set_y(self.price_ymin + pydistance)
|
|
104
105
|
|
|
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)
|
|
106
|
+
self.min_volumeboxheight = self.volume_ymax / 4
|
|
111
107
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
self.text_date_volume.set_y(y)
|
|
115
|
-
# 거래량 정보 y 위치
|
|
116
|
-
self.text_volume_info.set_y(y)
|
|
108
|
+
vxsub = self.vxmax - self.vxmin
|
|
109
|
+
self.vmiddle = self.vxmax - int((vxsub) / 2)
|
|
117
110
|
|
|
118
|
-
|
|
111
|
+
vxdistance = vxsub / 50
|
|
112
|
+
self.v0, self.v1 = (self.vxmin + vxdistance, self.vxmax - vxdistance)
|
|
113
|
+
self.vsixth = self.vxmin + int((vxsub) / 6)
|
|
114
|
+
self.veighth = self.vxmin + int((vxsub) / 8)
|
|
115
|
+
|
|
116
|
+
yvolume = self.volume_ymax * 0.85
|
|
117
|
+
self.text_date_volume.set_y(yvolume)
|
|
119
118
|
|
|
119
|
+
# 정보 텍스트박스
|
|
120
|
+
self.text_price_info.set_y(self.price_ymax - pydistance)
|
|
121
|
+
self.text_volume_info.set_y(yvolume)
|
|
122
|
+
return
|
|
120
123
|
|
|
121
|
-
class LineMixin(DataMixin):
|
|
122
|
-
in_slider, in_price, in_volume = (False, False, False)
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
class EventMixin(DataMixin):
|
|
126
|
+
in_price_chart, in_volume_chart = (False, False)
|
|
127
|
+
intx = None
|
|
126
128
|
|
|
127
129
|
def _connect_event(self):
|
|
128
130
|
super()._connect_event()
|
|
129
|
-
self.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
131
|
+
self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
130
132
|
return
|
|
131
133
|
|
|
132
|
-
def
|
|
133
|
-
self.
|
|
134
|
+
def _on_move(self, e):
|
|
135
|
+
self._on_move_action(e)
|
|
134
136
|
return
|
|
135
137
|
|
|
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)
|
|
138
|
+
def _on_move_action(self, e: MouseEvent):
|
|
139
|
+
self._check_ax(e)
|
|
140
140
|
|
|
141
|
-
self.
|
|
141
|
+
self.intx = None
|
|
142
|
+
if self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
142
143
|
return
|
|
143
144
|
|
|
144
|
-
def
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
self.
|
|
151
|
-
if self.in_price or self.in_volume:
|
|
152
|
-
self._chart_move_action(e)
|
|
145
|
+
def _check_ax(self, e: MouseEvent):
|
|
146
|
+
ax = e.inaxes
|
|
147
|
+
if not ax or e.xdata is None or e.ydata is None:
|
|
148
|
+
self.in_price_chart, self.in_volume_chart = (False, False)
|
|
149
|
+
else:
|
|
150
|
+
self.in_price_chart = ax is self.ax_price
|
|
151
|
+
self.in_volume_chart = False if self.in_price_chart else ax is self.ax_volume
|
|
153
152
|
|
|
154
|
-
self._blit()
|
|
155
153
|
return
|
|
156
154
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
def _get_x(self, e: MouseEvent):
|
|
156
|
+
self.intx = e.xdata.__int__()
|
|
157
|
+
if self.intx < 0: self.intx = None
|
|
160
158
|
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
|
|
159
|
+
try: self.list_index[self.intx]
|
|
160
|
+
except: self.intx = None
|
|
169
161
|
return
|
|
170
162
|
|
|
171
|
-
def _check_ax(self, e: MouseEvent):
|
|
172
|
-
ax = e.inaxes
|
|
173
163
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
164
|
+
class LineMixin(EventMixin):
|
|
165
|
+
digit_price, digit_volume = (0, 0)
|
|
166
|
+
in_candle, in_volumebar = (False, False)
|
|
167
|
+
|
|
168
|
+
def _on_move(self, e):
|
|
169
|
+
super()._on_move(e)
|
|
170
|
+
|
|
171
|
+
self._restore_region()
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
|
|
173
|
+
if self.in_price_chart: self._on_move_price_chart(e)
|
|
174
|
+
elif self.in_volume_chart: self._on_move_volume_chart(e)
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
self.sliderline.set_segments([((x, self._slider_ymin), (x, self._slider_ymax))])
|
|
184
|
-
self.ax_slider.draw_artist(self.sliderline)
|
|
176
|
+
self._blit()
|
|
185
177
|
return
|
|
186
178
|
|
|
187
|
-
def
|
|
179
|
+
def _on_move_price_chart(self, e: MouseEvent):
|
|
188
180
|
x, y = (e.xdata, e.ydata)
|
|
189
|
-
if not y: return
|
|
190
|
-
roundy = y.__round__()
|
|
191
181
|
|
|
192
|
-
self.
|
|
193
|
-
self.
|
|
194
|
-
self.
|
|
195
|
-
self.ax_volume.draw_artist(self.volumeh_vline)
|
|
182
|
+
self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
183
|
+
self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
|
|
184
|
+
self._draw_crossline()
|
|
196
185
|
|
|
197
|
-
|
|
198
|
-
else: self._volume_move_action(x, y, roundy)
|
|
199
|
-
return
|
|
200
|
-
|
|
201
|
-
def _price_move_action(self, _, y, roundy):
|
|
202
|
-
# 수평선
|
|
203
|
-
self.price_hline.set_segments([((self.vmin, y), (self.vmax, y))])
|
|
204
|
-
self.ax_price.draw_artist(self.price_hline)
|
|
186
|
+
renderer = self.figure.canvas.renderer
|
|
205
187
|
|
|
206
188
|
# 가격
|
|
207
|
-
self.text_price.set_text(f'{
|
|
189
|
+
self.text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
|
|
190
|
+
self.text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
|
|
208
191
|
self.text_price.set_y(y)
|
|
209
|
-
self.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if self.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
low =
|
|
221
|
-
|
|
192
|
+
self.text_price.draw(renderer)
|
|
193
|
+
|
|
194
|
+
index = self.intx
|
|
195
|
+
if index is None: self.in_candle = False
|
|
196
|
+
else:
|
|
197
|
+
# 기준시간 표시
|
|
198
|
+
self.text_date_volume.set_text(f'{self.df[self.date][index]}')
|
|
199
|
+
self.text_date_volume.set_x(x)
|
|
200
|
+
self.text_date_volume.draw(renderer)
|
|
201
|
+
|
|
202
|
+
# 캔들 강조
|
|
203
|
+
low = self.df['_boxmin'][index]
|
|
204
|
+
high = self.df['_boxmax'][index]
|
|
205
|
+
sub = high - low
|
|
206
|
+
if sub < self.min_candleboxheight:
|
|
207
|
+
sub = (self.min_candleboxheight - sub) / 2
|
|
208
|
+
low -= sub
|
|
209
|
+
high += sub
|
|
210
|
+
|
|
211
|
+
if high < y or y < low: self.in_candle = False
|
|
222
212
|
else:
|
|
223
|
-
self.
|
|
224
|
-
x1, x2 = (
|
|
213
|
+
self.in_candle = True
|
|
214
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
225
215
|
self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
226
|
-
self.
|
|
216
|
+
self.price_box.draw(renderer)
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
def _draw_crossline(self):
|
|
220
|
+
renderer = self.figure.canvas.renderer
|
|
221
|
+
self.price_crossline.draw(renderer)
|
|
222
|
+
self.volume_crossline.draw(renderer)
|
|
227
223
|
return
|
|
228
224
|
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
225
|
+
def _on_move_volume_chart(self, e: MouseEvent):
|
|
226
|
+
x, y = (e.xdata, e.ydata)
|
|
227
|
+
|
|
228
|
+
self.price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax))])
|
|
229
|
+
self.volume_crossline.set_segments([((x, 0), (x, self.volume_ymax)), ((self.vxmin, y), (self.vxmax, y))])
|
|
230
|
+
self._draw_crossline()
|
|
231
|
+
|
|
232
|
+
if not self.volume: return
|
|
233
|
+
|
|
234
|
+
renderer = self.figure.canvas.renderer
|
|
233
235
|
|
|
234
236
|
# 거래량
|
|
235
|
-
self.text_volume.set_text(f'{
|
|
237
|
+
self.text_volume.set_text(f'{float_to_str(y, self.digit_volume)}{self.unit_volume}')
|
|
238
|
+
self.text_volume.set_x(self.v0 if self.veighth < x else self.vsixth)
|
|
236
239
|
self.text_volume.set_y(y)
|
|
237
|
-
self.
|
|
240
|
+
self.text_volume.draw(renderer)
|
|
238
241
|
|
|
239
|
-
|
|
240
|
-
if self.
|
|
241
|
-
|
|
242
|
+
index = self.intx
|
|
243
|
+
if index is None: self.in_volumebar = False
|
|
244
|
+
else:
|
|
245
|
+
# 기준시간 표시
|
|
246
|
+
self.text_date_price.set_text(f'{self.df[self.date][index]}')
|
|
247
|
+
self.text_date_price.set_x(x)
|
|
248
|
+
self.text_date_price.draw(renderer)
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
# 거래량 강조
|
|
251
|
+
high = self.df[self.volume][index] * 1.15
|
|
244
252
|
low = 0
|
|
245
|
-
self.
|
|
246
|
-
|
|
253
|
+
if high < self.min_volumeboxheight: high = self.min_volumeboxheight
|
|
254
|
+
|
|
255
|
+
if high < y or y < low: self.in_volumebar = False
|
|
247
256
|
else:
|
|
248
|
-
self.
|
|
249
|
-
x1, x2 = (
|
|
257
|
+
self.in_volumebar = True
|
|
258
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
250
259
|
self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
251
|
-
self.
|
|
260
|
+
self.volume_box.draw(renderer)
|
|
252
261
|
return
|
|
253
262
|
|
|
254
263
|
|
|
264
|
+
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})'
|
|
265
|
+
format_volumeinfo_ko = '{dt}\n\n거래량 : {volume}\n거래량증가율: {rate_volume}'
|
|
266
|
+
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})'
|
|
267
|
+
format_volumeinfo_en = '{dt}\n\nvolume: {volume}\nvolume rate: {rate_volume}'
|
|
268
|
+
|
|
255
269
|
class InfoMixin(LineMixin):
|
|
256
270
|
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)
|
|
271
|
+
format_candleinfo = format_candleinfo_ko
|
|
272
|
+
format_volumeinfo = format_volumeinfo_ko
|
|
263
273
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
self.slider_text.set_y(y)
|
|
267
|
-
|
|
268
|
-
v = self.df[self.volume].max()
|
|
269
|
-
self._length_text = len(f'{v:,}')
|
|
270
|
-
self.set_text_coordante(self.xmin, self.xmax, self._price_ymin, self._price_ymax, self._vol_ymax)
|
|
274
|
+
def set_data(self, df, sort_df=True, calc_ma=True, set_candlecolor=True, set_volumecolor=True, calc_info=True, *args, **kwargs):
|
|
275
|
+
super().set_data(df, sort_df, calc_ma, set_candlecolor, set_volumecolor, calc_info, *args, **kwargs)
|
|
271
276
|
|
|
277
|
+
self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
|
|
272
278
|
return
|
|
273
279
|
|
|
274
|
-
def
|
|
275
|
-
super().
|
|
280
|
+
def _on_move_price_chart(self, e):
|
|
281
|
+
super()._on_move_price_chart(e)
|
|
276
282
|
|
|
277
|
-
|
|
283
|
+
# 캔들 강조 확인
|
|
284
|
+
if not self.in_candle: return
|
|
278
285
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
self.slider_text.set_x(e.xdata)
|
|
282
|
-
self.ax_slider.draw_artist(self.slider_text)
|
|
283
|
-
return
|
|
286
|
+
# 캔들 정보
|
|
287
|
+
self.text_price_info.set_text(self._get_info(self.intx))
|
|
284
288
|
|
|
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)
|
|
289
|
+
if self.vmiddle < e.xdata: self.text_price_info.set_x(self.v0)
|
|
290
|
+
else:
|
|
291
|
+
# self.text_price_info.set_x(self.vmax - self.x_distance)
|
|
292
|
+
# self.text_price_info.set_horizontalalignment('right')
|
|
293
|
+
# 텍스트박스 크기 가져오기
|
|
294
|
+
bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
295
|
+
width = bbox.x1 - bbox.x0
|
|
296
|
+
self.text_price_info.set_x(self.v1 - width)
|
|
297
|
+
|
|
298
|
+
self.text_price_info.draw(self.figure.canvas.renderer)
|
|
309
299
|
return
|
|
310
300
|
|
|
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)
|
|
301
|
+
def _on_move_volume_chart(self, e):
|
|
302
|
+
super()._on_move_volume_chart(e)
|
|
303
|
+
|
|
304
|
+
# 거래량 강조 확인
|
|
305
|
+
if not self.in_volumebar: return
|
|
306
|
+
|
|
307
|
+
# 거래량 정보
|
|
308
|
+
self.text_volume_info.set_text(self._get_info(self.intx, is_price=False))
|
|
309
|
+
|
|
310
|
+
if self.vmiddle < e.xdata: self.text_volume_info.set_x(self.v0)
|
|
311
|
+
else:
|
|
312
|
+
# self.text_volume_info.set_x(self.vmax - self.x_distance)
|
|
313
|
+
# self.text_volume_info.set_horizontalalignment('right')
|
|
314
|
+
# 텍스트박스 크기 가져오기
|
|
315
|
+
bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
316
|
+
width = bbox.x1 - bbox.x0
|
|
317
|
+
self.text_volume_info.set_x(self.v1 - width)
|
|
318
|
+
|
|
319
|
+
self.text_volume_info.draw(self.figure.canvas.renderer)
|
|
333
320
|
return
|
|
334
321
|
|
|
335
322
|
def _get_info(self, index, is_price=True):
|
|
336
323
|
dt = self.df[self.date][index]
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
324
|
+
if not self.volume:
|
|
325
|
+
v, vr = ('-', '-%')
|
|
326
|
+
else:
|
|
327
|
+
v = self.df[self.volume][index]
|
|
328
|
+
v = float_to_str(v, self.digit_volume)
|
|
329
|
+
# if not v % 1: v = int(v)
|
|
330
|
+
vr = self.df['rate_volume'][index]
|
|
331
|
+
vr = f'{vr:+06,.2f}%'
|
|
332
|
+
|
|
340
333
|
if is_price:
|
|
341
334
|
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
335
|
rate, compare = (self.df['rate'][index], self.df['compare'][index])
|
|
@@ -348,27 +341,23 @@ class InfoMixin(LineMixin):
|
|
|
348
341
|
cd = divmod(c, 1)
|
|
349
342
|
if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
|
|
350
343
|
else: c = float_to_str(cd[0])
|
|
351
|
-
|
|
352
344
|
comd = divmod(compare, 1)
|
|
353
345
|
if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
|
|
354
346
|
else: com = float_to_str(comd[0], plus=True)
|
|
355
|
-
|
|
356
347
|
o = o.__round__(self.digit_price)
|
|
357
348
|
od = divmod(o, 1)
|
|
358
349
|
if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
|
|
359
350
|
else: o = float_to_str(od[0])
|
|
360
|
-
|
|
361
351
|
h = h.__round__(self.digit_price)
|
|
362
352
|
hd = divmod(h, 1)
|
|
363
353
|
if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
|
|
364
354
|
else: h = float_to_str(hd[0])
|
|
365
|
-
|
|
366
355
|
l = l.__round__(self.digit_price)
|
|
367
356
|
ld = divmod(l, 1)
|
|
368
357
|
if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
|
|
369
358
|
else: l = float_to_str(ld[0])
|
|
370
359
|
|
|
371
|
-
text = self.
|
|
360
|
+
text = self.format_candleinfo.format(
|
|
372
361
|
dt=dt,
|
|
373
362
|
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
374
363
|
rate=f'{r:>{self._length_text}}%',
|
|
@@ -376,13 +365,13 @@ class InfoMixin(LineMixin):
|
|
|
376
365
|
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
377
366
|
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
378
367
|
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=
|
|
368
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
380
369
|
)
|
|
381
370
|
else:
|
|
382
371
|
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
372
|
com = float_to_str(compare, self.digit_price, plus=True)
|
|
384
373
|
|
|
385
|
-
text = self.
|
|
374
|
+
text = self.format_candleinfo.format(
|
|
386
375
|
dt=dt,
|
|
387
376
|
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
388
377
|
rate=f'{r:>{self._length_text}}%',
|
|
@@ -390,23 +379,35 @@ class InfoMixin(LineMixin):
|
|
|
390
379
|
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
391
380
|
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
392
381
|
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=
|
|
382
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
394
383
|
)
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
text = self.volumeformat.format(
|
|
384
|
+
elif self.volume:
|
|
385
|
+
text = self.format_volumeinfo.format(
|
|
398
386
|
dt=dt,
|
|
399
387
|
volume=f'{v:>{self._length_text}}{self.unit_volume}',
|
|
400
|
-
rate_volume=f'{
|
|
388
|
+
rate_volume=f'{vr:>{self._length_text}}%',
|
|
401
389
|
)
|
|
390
|
+
else: text = ''
|
|
402
391
|
return text
|
|
403
392
|
|
|
404
393
|
|
|
405
|
-
class
|
|
394
|
+
class BaseMixin(InfoMixin):
|
|
406
395
|
pass
|
|
407
396
|
|
|
408
397
|
|
|
409
|
-
class Chart(
|
|
398
|
+
class Chart(BaseMixin, Mixin):
|
|
399
|
+
def _add_collection(self):
|
|
400
|
+
super()._add_collection()
|
|
401
|
+
return self.add_collection()
|
|
402
|
+
|
|
403
|
+
def _draw_artist(self):
|
|
404
|
+
super()._draw_artist()
|
|
405
|
+
return self.draw_artist()
|
|
406
|
+
|
|
407
|
+
def _get_segments(self):
|
|
408
|
+
self.generate_data()
|
|
409
|
+
return super()._get_segments()
|
|
410
|
+
|
|
410
411
|
def _on_draw(self, e):
|
|
411
412
|
super()._on_draw(e)
|
|
412
413
|
return self.on_draw(e)
|
|
@@ -419,30 +420,3 @@ class Chart(CursorMixin, CM, Mixin):
|
|
|
419
420
|
super()._on_move(e)
|
|
420
421
|
return self.on_move(e)
|
|
421
422
|
|
|
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
|
|
429
|
-
|
|
430
|
-
file = Path(__file__).parent / 'data/samsung.txt'
|
|
431
|
-
file = Path(__file__).parent / 'data/apple.txt'
|
|
432
|
-
with open(file, 'r', encoding='utf-8') as txt:
|
|
433
|
-
data = json.load(txt)
|
|
434
|
-
n = 2600
|
|
435
|
-
data = data[n:n+100]
|
|
436
|
-
df = pd.DataFrame(data)
|
|
437
|
-
print(f'{df.keys()=}')
|
|
438
|
-
|
|
439
|
-
t = time()
|
|
440
|
-
c = CursorMixin()
|
|
441
|
-
c.unit_price = '$'
|
|
442
|
-
# c.fraction = True
|
|
443
|
-
c.set_data(df[['date', 'open', 'high', 'low', 'close', 'volume']])
|
|
444
|
-
t2 = time() - t
|
|
445
|
-
print(f'{t2=}')
|
|
446
|
-
plt.show()
|
|
447
|
-
|
|
448
|
-
|