seolpyo-mplchart 0.0.1__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 +49 -0
- seolpyo_mplchart/base.py +113 -0
- seolpyo_mplchart/cursor.py +433 -0
- seolpyo_mplchart/data/apple.txt +64187 -0
- seolpyo_mplchart/data/samsung.txt +120002 -0
- seolpyo_mplchart/draw.py +367 -0
- seolpyo_mplchart/slider.py +601 -0
- seolpyo_mplchart/test.py +38 -0
- seolpyo_mplchart/utils.py +45 -0
- seolpyo_mplchart-0.0.1.dist-info/METADATA +36 -0
- seolpyo_mplchart-0.0.1.dist-info/RECORD +13 -0
- seolpyo_mplchart-0.0.1.dist-info/WHEEL +5 -0
- seolpyo_mplchart-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This software includes Matplotlib, which is licensed under the BSD License.
|
|
3
|
+
Matplotlib Copyright (c) 2012- Matplotlib Development Team.
|
|
4
|
+
Full license can be found in the LICENSE file or at https://matplotlib.org/stable/users/license.html
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import matplotlib.pyplot as plt
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from .slider import Chart
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_name = {'samsung', 'apple'}
|
|
20
|
+
def sample(name: Literal['samsung', 'apple']='samsung'):
|
|
21
|
+
if name not in _name:
|
|
22
|
+
print('name should be either samsung or apple.')
|
|
23
|
+
return
|
|
24
|
+
file = Path(__file__).parent / f'data/{name}.txt'
|
|
25
|
+
with open(file, 'r', encoding='utf-8') as txt:
|
|
26
|
+
data = json.load(txt)
|
|
27
|
+
data = data
|
|
28
|
+
df = pd.DataFrame(data)
|
|
29
|
+
|
|
30
|
+
c = Chart()
|
|
31
|
+
if name == 'apple':
|
|
32
|
+
c.unit_price = '$'
|
|
33
|
+
c.unit_volume = ' vol'
|
|
34
|
+
c.digit_price = 3
|
|
35
|
+
c.label_ma = 'ma{}'
|
|
36
|
+
c.candleformat = '{}\n\nend: {}\nrate: {}\ncompare: {}\nopen: {}({})\nhigh: {}({})\nlow: {}({})\nvolume: {}({})'
|
|
37
|
+
c.volumeformat = '{}\n\nvolume: {}\nvolume rate: {}'
|
|
38
|
+
c.set_data(df)
|
|
39
|
+
show()
|
|
40
|
+
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def show():
|
|
45
|
+
return plt.show()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == '__main__':
|
|
49
|
+
sample('apple')
|
seolpyo_mplchart/base.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from time import time
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import matplotlib.style as mplstyle
|
|
5
|
+
from matplotlib.axes import Axes
|
|
6
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
from .utils import convert_unit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
try: plt.switch_backend('TkAgg')
|
|
13
|
+
except: pass
|
|
14
|
+
|
|
15
|
+
# 한글 깨짐 문제 방지
|
|
16
|
+
try: plt.rcParams['font.family'] ='Malgun Gothic'
|
|
17
|
+
except: pass
|
|
18
|
+
|
|
19
|
+
mplstyle.use('fast')
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Base:
|
|
23
|
+
canvas: FigureCanvasAgg
|
|
24
|
+
unit_price, unit_volume = ('원', '주')
|
|
25
|
+
|
|
26
|
+
figsize = (12, 6)
|
|
27
|
+
ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume = (3, 2, 18, 5)
|
|
28
|
+
adjust = dict(
|
|
29
|
+
top=0.95, bottom=0.05, left=0.01, right=0.93, # 여백
|
|
30
|
+
wspace=0, hspace=0 # 플롯간 간격
|
|
31
|
+
)
|
|
32
|
+
color_grid = '#d0d0d0'
|
|
33
|
+
color_background = '#fafafa'
|
|
34
|
+
|
|
35
|
+
slider_top = True
|
|
36
|
+
title = 'seolpyo mplchart'
|
|
37
|
+
|
|
38
|
+
def __init__(self, *args, **kwargs):
|
|
39
|
+
# 기본 툴바 비활성화
|
|
40
|
+
plt.rcParams['toolbar'] = 'None'
|
|
41
|
+
|
|
42
|
+
self._set_plot()
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
def _set_plot(self):
|
|
46
|
+
if self.slider_top:
|
|
47
|
+
fig, ax = plt.subplots(
|
|
48
|
+
4, # row 수
|
|
49
|
+
figsize=self.figsize, # 기본 크기
|
|
50
|
+
height_ratios=(self.ratio_ax_slider, self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
|
|
51
|
+
)
|
|
52
|
+
ax: list[Axes]
|
|
53
|
+
ax_slider, ax_legend, ax_price, ax_volume = ax
|
|
54
|
+
else:
|
|
55
|
+
fig, ax = plt.subplots(
|
|
56
|
+
5, # row 수
|
|
57
|
+
figsize=self.figsize, # 기본 크기
|
|
58
|
+
height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume, self.ratio_ax_legend, self.ratio_ax_slider) # row 크기 비율
|
|
59
|
+
)
|
|
60
|
+
ax: list[Axes]
|
|
61
|
+
ax_legend, ax_price, ax_volume, ax_none, ax_slider = ax
|
|
62
|
+
# 사용하지 않는 axes 숨기기
|
|
63
|
+
ax_none.axis('off')
|
|
64
|
+
ax_legend.axis('off')
|
|
65
|
+
|
|
66
|
+
ax_slider.xaxis.set_animated(True)
|
|
67
|
+
ax_slider.yaxis.set_animated(True)
|
|
68
|
+
|
|
69
|
+
ax_price.xaxis.set_animated(True)
|
|
70
|
+
ax_price.yaxis.set_animated(True)
|
|
71
|
+
|
|
72
|
+
ax_volume.xaxis.set_animated(True)
|
|
73
|
+
ax_volume.yaxis.set_animated(True)
|
|
74
|
+
|
|
75
|
+
fig.canvas.manager.set_window_title(f'{self.title}')
|
|
76
|
+
|
|
77
|
+
# 플롯간 간격 제거(Configure subplots)
|
|
78
|
+
fig.subplots_adjust(**self.adjust)
|
|
79
|
+
|
|
80
|
+
# y ticklabel foramt 설정
|
|
81
|
+
ax_slider.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price))
|
|
82
|
+
ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price))
|
|
83
|
+
ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume))
|
|
84
|
+
|
|
85
|
+
# 공통 설정
|
|
86
|
+
for a in [ax_slider, ax_price, ax_volume]:
|
|
87
|
+
# y tick 우측으로 이동
|
|
88
|
+
a.tick_params(left=False, right=True, labelleft=False, labelright=True)
|
|
89
|
+
# 차트 영역 배경 색상
|
|
90
|
+
a.set_facecolor(self.color_background)
|
|
91
|
+
# grid(구분선, 격자) 그리기
|
|
92
|
+
a.grid(True, color=self.color_grid, linewidth=1)
|
|
93
|
+
# x tick 제거
|
|
94
|
+
a.set_xticklabels([])
|
|
95
|
+
|
|
96
|
+
self.fig, self.canvas = (fig, fig.canvas)
|
|
97
|
+
self.ax_slider, self.ax_legend, self.ax_price, self.ax_volume = (ax_slider, ax_legend, ax_price, ax_volume)
|
|
98
|
+
|
|
99
|
+
return self.set_plot()
|
|
100
|
+
|
|
101
|
+
def set_plot(self):
|
|
102
|
+
"This function works after set plot process is done."
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class Chart(Base):
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == '__main__':
|
|
111
|
+
Base()
|
|
112
|
+
|
|
113
|
+
plt.show()
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
from time import time
|
|
3
|
+
|
|
4
|
+
from matplotlib.backend_bases import MouseEvent
|
|
5
|
+
from matplotlib.collections import LineCollection
|
|
6
|
+
from matplotlib.text import Text
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .draw import DrawMixin, Chart as CM
|
|
11
|
+
from .utils import float_to_str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Mixin:
|
|
15
|
+
def create_background(self):
|
|
16
|
+
"This function works befor canvas.copy_from_bbox()."
|
|
17
|
+
return
|
|
18
|
+
def on_draw(self, e):
|
|
19
|
+
"This function works if draw event active."
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CollectionMixin(DrawMixin):
|
|
24
|
+
lineKwargs = dict(edgecolor='k', linewidth=1, linestyle='-')
|
|
25
|
+
textboxKwargs = dict(boxstyle='round', facecolor='w')
|
|
26
|
+
|
|
27
|
+
def _add_collection(self):
|
|
28
|
+
super()._add_collection()
|
|
29
|
+
self.sliderline = LineCollection([], animated=True, **self.lineKwargs)
|
|
30
|
+
self.ax_slider.add_artist(self.sliderline)
|
|
31
|
+
self.slider_text = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='center')
|
|
32
|
+
self.ax_slider.add_artist(self.slider_text)
|
|
33
|
+
|
|
34
|
+
self.price_vline = LineCollection([], animated=True, **self.lineKwargs)
|
|
35
|
+
self.ax_price.add_artist(self.price_vline)
|
|
36
|
+
self.text_date_price = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='bottom', horizontalalignment='center')
|
|
37
|
+
self.ax_price.add_artist(self.text_date_price)
|
|
38
|
+
self.text_price = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
|
|
39
|
+
self.ax_price.add_artist(self.text_price)
|
|
40
|
+
|
|
41
|
+
self.volumeh_vline = LineCollection([], animated=True, **self.lineKwargs)
|
|
42
|
+
self.ax_volume.add_artist(self.volumeh_vline)
|
|
43
|
+
self.text_date_volume = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='center')
|
|
44
|
+
self.ax_volume.add_artist(self.text_date_volume)
|
|
45
|
+
self.text_volume = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='center', horizontalalignment='left')
|
|
46
|
+
self.ax_volume.add_artist(self.text_volume)
|
|
47
|
+
|
|
48
|
+
self.price_hline = LineCollection([], animated=True, **self.lineKwargs)
|
|
49
|
+
self.ax_price.add_artist(self.price_hline)
|
|
50
|
+
self.price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor='k')
|
|
51
|
+
self.ax_price.add_artist(self.price_box)
|
|
52
|
+
self.text_price_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
|
|
53
|
+
self.ax_price.add_artist(self.text_price_info)
|
|
54
|
+
|
|
55
|
+
self.volume_hline = LineCollection([], animated=True, **self.lineKwargs)
|
|
56
|
+
self.ax_volume.add_artist(self.volume_hline)
|
|
57
|
+
self.volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor='k')
|
|
58
|
+
self.ax_volume.add_artist(self.volume_box)
|
|
59
|
+
self.text_volume_info = Text(animated=True, bbox=self.textboxKwargs, verticalalignment='top', horizontalalignment='left')
|
|
60
|
+
self.ax_volume.add_artist(self.text_volume_info)
|
|
61
|
+
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
|
|
66
|
+
|
|
67
|
+
class DataMixin(CollectionMixin):
|
|
68
|
+
def _generate_data(self, df: pd.DataFrame):
|
|
69
|
+
for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
|
|
70
|
+
v = getattr(self, i)
|
|
71
|
+
if v in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={v!r}')
|
|
72
|
+
|
|
73
|
+
super()._generate_data(df)
|
|
74
|
+
|
|
75
|
+
df['rate'] = ((df[self.close] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
76
|
+
df['compare'] = (df[self.close] - df[self.close].shift(1)).fillna(0)
|
|
77
|
+
df['rate_open'] = ((df[self.Open] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
78
|
+
df['rate_high'] = ((df[self.high] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
79
|
+
df['rate_low'] = ((df[self.low] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
80
|
+
df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class LineMixin(DataMixin):
|
|
85
|
+
in_slider, in_price, in_volume = (False, False, False)
|
|
86
|
+
|
|
87
|
+
intx, in_index = (None, False)
|
|
88
|
+
_in_candle, _in_volumebar = (False, False)
|
|
89
|
+
|
|
90
|
+
def _connect_event(self):
|
|
91
|
+
super()._connect_event()
|
|
92
|
+
self.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
def _blit(self):
|
|
96
|
+
self.canvas.blit()
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
def set_data(self, df):
|
|
100
|
+
super().set_data(df)
|
|
101
|
+
|
|
102
|
+
self.vmin, self.vmax = (self.xmin, self.xmax)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
def _on_move(self, e):
|
|
106
|
+
self._restore_region()
|
|
107
|
+
|
|
108
|
+
self._on_move_action(e)
|
|
109
|
+
|
|
110
|
+
if self.in_slider or self.in_price or self.in_volume:
|
|
111
|
+
self._slider_move_action(e)
|
|
112
|
+
if self.in_price or self.in_volume:
|
|
113
|
+
self._chart_move_action(e)
|
|
114
|
+
|
|
115
|
+
self._blit()
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
def _on_move_action(self, e: MouseEvent):
|
|
119
|
+
if not e.inaxes:
|
|
120
|
+
self.intx, self.in_index = (None, False)
|
|
121
|
+
else:
|
|
122
|
+
self._check_ax(e)
|
|
123
|
+
x, y = (e.xdata, e.ydata)
|
|
124
|
+
self.intx = x.__int__()
|
|
125
|
+
if self.intx < 0: self.in_index = False
|
|
126
|
+
else:
|
|
127
|
+
try: self.df['x'][self.intx]
|
|
128
|
+
except: self.in_index = False
|
|
129
|
+
else: self.in_index = True
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
def _check_ax(self, e: MouseEvent):
|
|
133
|
+
ax = e.inaxes
|
|
134
|
+
|
|
135
|
+
self.in_slider = ax is self.ax_slider
|
|
136
|
+
self.in_price = False if self.in_slider else ax is self.ax_price
|
|
137
|
+
self.in_volume = False if (self.in_slider or self.in_price) else ax is self.ax_volume
|
|
138
|
+
return
|
|
139
|
+
|
|
140
|
+
def _slider_move_action(self, e: MouseEvent):
|
|
141
|
+
x = e.xdata
|
|
142
|
+
|
|
143
|
+
# 수직선
|
|
144
|
+
self.sliderline.set_segments([((x, self._slider_ymin), (x, self._slider_ymax))])
|
|
145
|
+
self.ax_slider.draw_artist(self.sliderline)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
def _chart_move_action(self, e: MouseEvent):
|
|
149
|
+
x, y = (e.xdata, e.ydata)
|
|
150
|
+
if not y: return
|
|
151
|
+
roundy = y.__round__()
|
|
152
|
+
|
|
153
|
+
self.price_vline.set_segments([((x, self._price_ymin), (x, self._price_ymax))])
|
|
154
|
+
self.volumeh_vline.set_segments([((x, 0), (x, self._vol_ymax))])
|
|
155
|
+
self.ax_price.draw_artist(self.price_vline)
|
|
156
|
+
self.ax_volume.draw_artist(self.volumeh_vline)
|
|
157
|
+
|
|
158
|
+
if self.in_price: self._price_move_action(x, y, roundy)
|
|
159
|
+
else: self._volume_move_action(x, y, roundy)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
def _price_move_action(self, _, y, roundy):
|
|
163
|
+
# 수평선
|
|
164
|
+
self.price_hline.set_segments([((self.vmin, y), (self.vmax, y))])
|
|
165
|
+
self.ax_price.draw_artist(self.price_hline)
|
|
166
|
+
|
|
167
|
+
# 가격
|
|
168
|
+
self.text_price.set_text(f'{roundy:,}{self.unit_price}')
|
|
169
|
+
self.text_price.set_y(y)
|
|
170
|
+
self.ax_price.draw_artist(self.text_price)
|
|
171
|
+
|
|
172
|
+
# 캔들 강조
|
|
173
|
+
if self.in_index:
|
|
174
|
+
intx = self.intx
|
|
175
|
+
|
|
176
|
+
high = self.df[self.high][intx] * 1.02
|
|
177
|
+
low = self.df[self.low][intx] * 0.98
|
|
178
|
+
if high < y or y < low: self._in_candle = False
|
|
179
|
+
else:
|
|
180
|
+
self._in_candle = True
|
|
181
|
+
x1, x2 = (intx-0.3, intx+1.4)
|
|
182
|
+
self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
183
|
+
self.ax_price.draw_artist(self.price_box)
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
def _volume_move_action(self, _, y, roundy):
|
|
187
|
+
# 수평선
|
|
188
|
+
self.volume_hline.set_segments([((self.vmin, y), (self.vmax, y))])
|
|
189
|
+
self.ax_volume.draw_artist(self.volume_hline)
|
|
190
|
+
|
|
191
|
+
# 거래량
|
|
192
|
+
self.text_volume.set_text(f'{roundy:,}{self.unit_volume}')
|
|
193
|
+
self.text_volume.set_y(y)
|
|
194
|
+
self.ax_volume.draw_artist(self.text_volume)
|
|
195
|
+
|
|
196
|
+
# 거래량 강조
|
|
197
|
+
if self.in_index:
|
|
198
|
+
intx = self.intx
|
|
199
|
+
|
|
200
|
+
high = self.df[self.volume][intx] * 1.1
|
|
201
|
+
low = 0
|
|
202
|
+
self._volumerange = (0, high)
|
|
203
|
+
if high < y or y < low: self._in_volumebar: False
|
|
204
|
+
else:
|
|
205
|
+
self._in_volumebar = True
|
|
206
|
+
x1, x2 = (intx-0.3, intx+1.4)
|
|
207
|
+
self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
208
|
+
self.ax_volume.draw_artist(self.volume_box)
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
class InfoMixin(LineMixin):
|
|
213
|
+
fraction = False
|
|
214
|
+
candleformat = '{}\n\n종가: {}\n등락률: {}\n대비: {}\n시가: {}({})\n고가: {}({})\n저가: {}({})\n거래량: {}({})'
|
|
215
|
+
volumeformat = '{}\n\n거래량 : {}\n거래량증가율: {}'
|
|
216
|
+
digit_price, digit_volume = (0, 0)
|
|
217
|
+
|
|
218
|
+
def set_data(self, df):
|
|
219
|
+
super().set_data(df)
|
|
220
|
+
|
|
221
|
+
# 슬라이더 날짜 텍스트 y 위치
|
|
222
|
+
y = self._slider_ymax - (self._slider_ymax - self._slider_ymin) / 6
|
|
223
|
+
self.slider_text.set_y(y)
|
|
224
|
+
|
|
225
|
+
v = self.df[self.volume].max()
|
|
226
|
+
self._length_text = len(f'{v:,}')
|
|
227
|
+
self.set_text_coordante(self.xmin, self.xmax, self._price_ymin, self._price_ymax, self._vol_ymax)
|
|
228
|
+
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
|
|
232
|
+
# 주가, 거래량 텍스트 x 위치
|
|
233
|
+
x_distance = (vmax - vmin) / 30
|
|
234
|
+
self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
|
|
235
|
+
self.text_price.set_x(self.v0)
|
|
236
|
+
self.text_volume.set_x(self.v0)
|
|
237
|
+
|
|
238
|
+
self.vmin, self.vmax = (vmin, vmax)
|
|
239
|
+
self.vmiddle = vmax - int((vmax - vmin) / 2)
|
|
240
|
+
|
|
241
|
+
# 주가 날짜 텍스트 y 위치
|
|
242
|
+
y = (pmax - pmin) / 20 + pmin
|
|
243
|
+
self.text_date_price.set_y(y)
|
|
244
|
+
# 주가 정보 y 위치
|
|
245
|
+
y = pmax - (pmax - pmin) / 20
|
|
246
|
+
self.text_price_info.set_y(y)
|
|
247
|
+
|
|
248
|
+
# 거래량 날짜 텍스트 y 위치
|
|
249
|
+
y = volmax * 0.85
|
|
250
|
+
self.text_date_volume.set_y(y)
|
|
251
|
+
# 거래량 정보 y 위치
|
|
252
|
+
self.text_volume_info.set_y(y)
|
|
253
|
+
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
def _slider_move_action(self, e):
|
|
257
|
+
super()._slider_move_action(e)
|
|
258
|
+
|
|
259
|
+
intx = self.intx
|
|
260
|
+
|
|
261
|
+
if self.in_slider and self.in_index:
|
|
262
|
+
self.slider_text.set_text(f'{self.df[self.date][intx]}')
|
|
263
|
+
self.slider_text.set_x(e.xdata)
|
|
264
|
+
self.ax_slider.draw_artist(self.slider_text)
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
def _price_move_action(self, x, y, roundy):
|
|
268
|
+
super()._price_move_action(x, y, roundy)
|
|
269
|
+
if not self.in_index: return
|
|
270
|
+
intx = self.intx
|
|
271
|
+
|
|
272
|
+
# 텍스트
|
|
273
|
+
text = f'{self.df[self.date][intx]}'
|
|
274
|
+
self.text_date_volume.set_text(text)
|
|
275
|
+
self.text_date_volume.set_x(x)
|
|
276
|
+
self.ax_volume.draw_artist(self.text_date_volume)
|
|
277
|
+
|
|
278
|
+
# 캔들 강조
|
|
279
|
+
if self.in_price and self._in_candle:
|
|
280
|
+
# 캔들 정보
|
|
281
|
+
self.text_price_info.set_text(self._get_info(intx))
|
|
282
|
+
if x < self.vmiddle:
|
|
283
|
+
# 텍스트박스 크기 가져오기
|
|
284
|
+
bbox = self.text_price_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
285
|
+
width = bbox.x1 - bbox.x0
|
|
286
|
+
self.text_price_info.set_x(self.v1 - width)
|
|
287
|
+
else:
|
|
288
|
+
self.text_price_info.set_x(self.v0)
|
|
289
|
+
self.text_price_info.set_horizontalalignment('left')
|
|
290
|
+
self.ax_price.draw_artist(self.text_price_info)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
def _volume_move_action(self, x, y, roundy):
|
|
294
|
+
super()._volume_move_action(x, y, roundy)
|
|
295
|
+
if not self.in_index: return
|
|
296
|
+
intx = self.intx
|
|
297
|
+
|
|
298
|
+
text = f'{self.df[self.date][intx]}'
|
|
299
|
+
self.text_date_price.set_text(text)
|
|
300
|
+
self.text_date_price.set_x(x)
|
|
301
|
+
self.ax_price.draw_artist(self.text_date_price)
|
|
302
|
+
|
|
303
|
+
# 거래량 강조
|
|
304
|
+
if self.in_volume and self._in_volumebar:
|
|
305
|
+
# 거래량 정보
|
|
306
|
+
if x < self.vmiddle:
|
|
307
|
+
bbox = self.text_volume_info.get_window_extent().transformed(self.ax_price.transData.inverted())
|
|
308
|
+
width = bbox.x1 - bbox.x0
|
|
309
|
+
self.text_volume_info.set_x(self.v1 - width)
|
|
310
|
+
else:
|
|
311
|
+
self.text_volume_info.set_x(self.v0)
|
|
312
|
+
self.text_volume_info.set_horizontalalignment('left')
|
|
313
|
+
self.text_volume_info.set_text(self._get_info(intx, False))
|
|
314
|
+
self.ax_volume.draw_artist(self.text_volume_info)
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
def _get_info(self, index, is_price=True):
|
|
318
|
+
dt = self.df[self.date][index]
|
|
319
|
+
v = self.df[self.volume][index]
|
|
320
|
+
v = float_to_str(v, self.digit_volume)
|
|
321
|
+
vr = self.df['rate_volume'][index]
|
|
322
|
+
if is_price:
|
|
323
|
+
o, h, l, c = (self.df[self.Open][index], self.df[self.high][index], self.df[self.low][index], self.df[self.close][index])
|
|
324
|
+
rate, compare = (self.df['rate'][index], self.df['compare'][index])
|
|
325
|
+
r = f'{rate:+06,.2f}'
|
|
326
|
+
Or, hr, lr = (self.df['rate_open'][index], self.df['rate_high'][index], self.df['rate_low'][index])
|
|
327
|
+
|
|
328
|
+
if self.fraction:
|
|
329
|
+
c = c.__round__(self.digit_price)
|
|
330
|
+
cd = divmod(c, 1)
|
|
331
|
+
if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
|
|
332
|
+
else: c = float_to_str(cd[0])
|
|
333
|
+
comd = divmod(compare, 1)
|
|
334
|
+
if comd[1]: com = f'{float_to_str(comd[0])} {Fraction(comd[1])}'
|
|
335
|
+
else: com = float_to_str(comd[0])
|
|
336
|
+
o = o.__round__(self.digit_price)
|
|
337
|
+
od = divmod(o, 1)
|
|
338
|
+
if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
|
|
339
|
+
else: o = float_to_str(od[0])
|
|
340
|
+
h = h.__round__(self.digit_price)
|
|
341
|
+
hd = divmod(h, 1)
|
|
342
|
+
if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
|
|
343
|
+
else: h = float_to_str(hd[0])
|
|
344
|
+
l = l.__round__(self.digit_price)
|
|
345
|
+
ld = divmod(l, 1)
|
|
346
|
+
if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
|
|
347
|
+
else: l = float_to_str(ld[0])
|
|
348
|
+
|
|
349
|
+
text = self.candleformat.format(
|
|
350
|
+
dt,
|
|
351
|
+
f'{c:>{self._length_text}}{self.unit_price}',
|
|
352
|
+
f'{r:>{self._length_text}}%',
|
|
353
|
+
f'{com:>{self._length_text}}{self.unit_price}',
|
|
354
|
+
f'{o:>{self._length_text}}{self.unit_price}', f'{Or:+06,.2f}%',
|
|
355
|
+
f'{h:>{self._length_text}}{self.unit_price}', f'{hr:+06,.2f}%',
|
|
356
|
+
f'{l:>{self._length_text}}{self.unit_price}', f'{lr:+06,.2f}%',
|
|
357
|
+
f'{v:>{self._length_text}}{self.unit_volume}', f'{vr:+06,.2f}%',
|
|
358
|
+
)
|
|
359
|
+
else:
|
|
360
|
+
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))
|
|
361
|
+
com = float_to_str(compare, self.digit_price)
|
|
362
|
+
|
|
363
|
+
text = self.candleformat.format(
|
|
364
|
+
dt,
|
|
365
|
+
f'{c:>{self._length_text}}{self.unit_price}',
|
|
366
|
+
f'{r:>{self._length_text}}%',
|
|
367
|
+
f'{com:>{self._length_text}}{self.unit_price}',
|
|
368
|
+
f'{o:>{self._length_text}}{self.unit_price}', f'{Or:+06,.2f}%',
|
|
369
|
+
f'{h:>{self._length_text}}{self.unit_price}', f'{hr:+06,.2f}%',
|
|
370
|
+
f'{l:>{self._length_text}}{self.unit_price}', f'{lr:+06,.2f}%',
|
|
371
|
+
f'{v:>{self._length_text}}{self.unit_volume}', f'{vr:+06,.2f}%',
|
|
372
|
+
)
|
|
373
|
+
else:
|
|
374
|
+
vrate = f'{vr:+06,.2f}'
|
|
375
|
+
text = self.volumeformat.format(
|
|
376
|
+
dt,
|
|
377
|
+
f'{v:>{self._length_text}}{self.unit_volume}',
|
|
378
|
+
f'{vrate:>{self._length_text}}%',
|
|
379
|
+
)
|
|
380
|
+
return text
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class CursorMixin(InfoMixin):
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class Chart(CursorMixin, CM, Mixin):
|
|
388
|
+
def _generate_data(self, df):
|
|
389
|
+
super()._generate_data(df)
|
|
390
|
+
return self.generate_data(df)
|
|
391
|
+
|
|
392
|
+
def _on_draw(self, e):
|
|
393
|
+
super()._on_draw(e)
|
|
394
|
+
return self.on_draw(e)
|
|
395
|
+
|
|
396
|
+
def _on_pick(self, e):
|
|
397
|
+
self.on_pick(e)
|
|
398
|
+
return super()._on_pick(e)
|
|
399
|
+
|
|
400
|
+
def _draw_artist(self):
|
|
401
|
+
super()._draw_artist()
|
|
402
|
+
return self.create_background()
|
|
403
|
+
|
|
404
|
+
def _blit(self):
|
|
405
|
+
super()._blit()
|
|
406
|
+
return self.on_blit()
|
|
407
|
+
|
|
408
|
+
def _on_move(self, e):
|
|
409
|
+
super()._on_move(e)
|
|
410
|
+
return self.on_move(e)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
if __name__ == '__main__':
|
|
414
|
+
import json
|
|
415
|
+
from pathlib import Path
|
|
416
|
+
import matplotlib.pyplot as plt
|
|
417
|
+
file = Path(__file__).parent / 'data/samsung.txt'
|
|
418
|
+
file = Path(__file__).parent / 'data/apple.txt'
|
|
419
|
+
with open(file, 'r', encoding='utf-8') as txt:
|
|
420
|
+
data = json.load(txt)
|
|
421
|
+
data = data[:100]
|
|
422
|
+
df = pd.DataFrame(data)
|
|
423
|
+
|
|
424
|
+
t = time()
|
|
425
|
+
c = CursorMixin()
|
|
426
|
+
c.unit_price = '$'
|
|
427
|
+
# c.fraction = True
|
|
428
|
+
c.set_data(df=df)
|
|
429
|
+
t2 = time() - t
|
|
430
|
+
print(f'{t2=}')
|
|
431
|
+
plt.show()
|
|
432
|
+
|
|
433
|
+
|