seolpyo-mplchart 1.1.1__py3-none-any.whl → 1.2.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 +9 -9
- seolpyo_mplchart/_base.py +114 -0
- seolpyo_mplchart/_cursor.py +485 -0
- seolpyo_mplchart/_draw.py +591 -0
- seolpyo_mplchart/_slider.py +608 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/METADATA +1 -1
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/RECORD +9 -5
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-1.1.1.dist-info → seolpyo_mplchart-1.2.0.dist-info}/top_level.txt +0 -0
seolpyo_mplchart/__init__.py
CHANGED
|
@@ -5,9 +5,9 @@ from pathlib import Path
|
|
|
5
5
|
import matplotlib.pyplot as plt
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
|
-
from .
|
|
9
|
-
from .
|
|
10
|
-
from .
|
|
8
|
+
from ._draw import Chart as _BaseChart
|
|
9
|
+
from ._cursor import Chart as _BaseCursorChart, format_candleinfo_ko, format_volumeinfo_ko, format_candleinfo_en, format_volumeinfo_en
|
|
10
|
+
from ._slider import Chart as _BaseSliderChart
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
@@ -24,7 +24,7 @@ path_samsung = Path(__file__).parent / 'sample/samsung.txt'
|
|
|
24
24
|
path_apple = Path(__file__).parent / 'sample/apple.txt'
|
|
25
25
|
|
|
26
26
|
def sample(stock: Literal['samsung', 'apple']='samsung', chart: Literal['Chart', 'CursorChart', 'SliderChart']='SliderChart'):
|
|
27
|
-
C:
|
|
27
|
+
C: _BaseSliderChart = {'Chart': _BaseChart, 'CursorChart': _BaseCursorChart, 'SliderChart': _BaseSliderChart}[chart]()
|
|
28
28
|
path_file = path_samsung if stock == 'samsung' else path_apple
|
|
29
29
|
if stock == 'samsung':
|
|
30
30
|
C.format_candleinfo = format_candleinfo_ko
|
|
@@ -68,7 +68,7 @@ def close(fig='all'):
|
|
|
68
68
|
return plt.close(fig)
|
|
69
69
|
|
|
70
70
|
|
|
71
|
-
class OnlyChart(
|
|
71
|
+
class OnlyChart(_BaseChart):
|
|
72
72
|
r"""
|
|
73
73
|
You can see the guidance document:
|
|
74
74
|
Korean: https://white.seolpyo.com/entry/147/
|
|
@@ -129,7 +129,7 @@ class OnlyChart(BaseChart):
|
|
|
129
129
|
pass
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
class CursorChart(
|
|
132
|
+
class CursorChart(_BaseCursorChart):
|
|
133
133
|
r"""
|
|
134
134
|
You can see the guidance document:
|
|
135
135
|
Korean: https://white.seolpyo.com/entry/147/
|
|
@@ -201,7 +201,7 @@ class CursorChart(BaseCursorChart):
|
|
|
201
201
|
pass
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
class SliderChart(
|
|
204
|
+
class SliderChart(_BaseSliderChart):
|
|
205
205
|
r"""
|
|
206
206
|
You can see the guidance document:
|
|
207
207
|
Korean: https://white.seolpyo.com/entry/147/
|
|
@@ -272,7 +272,7 @@ class SliderChart(BaseSliderChart):
|
|
|
272
272
|
min_distance: Minimum number of candles that can be selected with the slider. default 30
|
|
273
273
|
limit_candle: Maximum number of candles to draw. default 800
|
|
274
274
|
limit_wick: Maximum number of candle wicks to draw. default 4,000
|
|
275
|
-
limit_volume: Maximum number of volume bars to draw. default
|
|
275
|
+
limit_volume: Maximum number of volume bars to draw. default 200. Applies only to drawing candle wicks or price line.
|
|
276
276
|
limit_ma: If the number of displayed data is more than this, the price moving average line is not drawn. default 8,000
|
|
277
277
|
|
|
278
278
|
color_navigator_line: Navigator divider color. default '#1e78ff'
|
|
@@ -303,7 +303,7 @@ def set_theme(chart: OnlyChart|CursorChart|SliderChart, theme: Literal['light',
|
|
|
303
303
|
chart.color_box = 'w'
|
|
304
304
|
chart.textboxKwargs = {'facecolor': 'k', 'edgecolor': 'w'}
|
|
305
305
|
chart.textKwargs = {'color': 'w'}
|
|
306
|
-
chart.color_navigator_cover, chart.color_navigator_line = ('w', '#
|
|
306
|
+
chart.color_navigator_cover, chart.color_navigator_line = ('w', '#FF2400')
|
|
307
307
|
|
|
308
308
|
if initialized:
|
|
309
309
|
chart.change_background_color('k')
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from re import search
|
|
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
|
+
from matplotlib.figure import Figure as Fig
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from .utils import dict_unit, dict_unit_en
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
try: plt.switch_backend('TkAgg')
|
|
14
|
+
except: pass
|
|
15
|
+
|
|
16
|
+
# 한글 깨짐 문제 방지
|
|
17
|
+
try: plt.rcParams['font.family'] ='Malgun Gothic'
|
|
18
|
+
except: pass
|
|
19
|
+
|
|
20
|
+
mplstyle.use('fast')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def convert_unit(value: float, digit=0, word='원'):
|
|
24
|
+
v = value.__abs__()
|
|
25
|
+
du = dict_unit if search('[가-힣]', word) else dict_unit_en
|
|
26
|
+
for unit, n in du.items():
|
|
27
|
+
if n <= v:
|
|
28
|
+
num = (value / n).__round__(digit)
|
|
29
|
+
if not num % 1: num = int(num)
|
|
30
|
+
return f'{num:,}{unit} {word}'
|
|
31
|
+
value = value.__round__(digit)
|
|
32
|
+
if not value % 1: value = int(value)
|
|
33
|
+
elif value < 10: digit = 2
|
|
34
|
+
text = f'{value:,}{word}'
|
|
35
|
+
return text
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Figure(Fig):
|
|
39
|
+
canvas: FigureCanvasAgg
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Base:
|
|
43
|
+
figure: Figure
|
|
44
|
+
|
|
45
|
+
figsize = (14, 7)
|
|
46
|
+
ratio_ax_legend, ratio_ax_price, ratio_ax_volume = (2, 18, 5)
|
|
47
|
+
adjust = dict(
|
|
48
|
+
top=0.95, bottom=0.05, left=0.01, right=0.93, # 여백
|
|
49
|
+
wspace=0, hspace=0 # 플롯간 간격
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
title = 'seolpyo mplchart'
|
|
53
|
+
color_background = '#fafafa'
|
|
54
|
+
gridKwargs = {}
|
|
55
|
+
color_tick, color_tick_label = ('k', 'k')
|
|
56
|
+
|
|
57
|
+
unit_price, unit_volume = ('원', '주')
|
|
58
|
+
|
|
59
|
+
def __init__(self, *args, **kwargs):
|
|
60
|
+
# 기본 툴바 비활성화
|
|
61
|
+
plt.rcParams['toolbar'] = 'None'
|
|
62
|
+
# plt.rcParams['figure.dpi'] = 600
|
|
63
|
+
|
|
64
|
+
self._get_plot()
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
def _get_plot(self):
|
|
68
|
+
self.figure, axes = plt.subplots(
|
|
69
|
+
3, # row 수
|
|
70
|
+
figsize=self.figsize, # 기본 크기
|
|
71
|
+
height_ratios=(self.ratio_ax_legend, self.ratio_ax_price, self.ratio_ax_volume) # row 크기 비율
|
|
72
|
+
)
|
|
73
|
+
axes: list[Axes]
|
|
74
|
+
self.ax_legend, self.ax_price, self.ax_volume = axes
|
|
75
|
+
self.ax_legend.set_label('legend ax')
|
|
76
|
+
self.ax_price.set_label('price ax')
|
|
77
|
+
self.ax_volume.set_label('volume ax')
|
|
78
|
+
|
|
79
|
+
self.figure.canvas.manager.set_window_title(f'{self.title}')
|
|
80
|
+
self.figure.set_facecolor(self.color_background)
|
|
81
|
+
|
|
82
|
+
# 플롯간 간격 제거(Configure subplots)
|
|
83
|
+
self.figure.subplots_adjust(**self.adjust)
|
|
84
|
+
|
|
85
|
+
self.ax_legend.set_axis_off()
|
|
86
|
+
|
|
87
|
+
# y ticklabel foramt 설정
|
|
88
|
+
self.ax_price.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_price, digit=2))
|
|
89
|
+
self.ax_volume.yaxis.set_major_formatter(lambda x, _: convert_unit(x, word=self.unit_volume, digit=2))
|
|
90
|
+
|
|
91
|
+
gridKwargs = {'visible': True, 'linewidth': 0.7, 'color': '#d0d0d0', 'linestyle': '-', 'dashes': (1, 0)}
|
|
92
|
+
gridKwargs.update(self.gridKwargs)
|
|
93
|
+
# 공통 설정
|
|
94
|
+
for ax in (self.ax_price, self.ax_volume):
|
|
95
|
+
ax.xaxis.set_animated(True)
|
|
96
|
+
ax.yaxis.set_animated(True)
|
|
97
|
+
|
|
98
|
+
# x tick 외부 눈금 표시하지 않기
|
|
99
|
+
ax.xaxis.set_ticks_position('none')
|
|
100
|
+
# x tick label 제거
|
|
101
|
+
ax.set_xticklabels([])
|
|
102
|
+
# y tick 우측으로 이동
|
|
103
|
+
ax.tick_params(left=False, right=True, labelleft=False, labelright=True, colors=self.color_tick_label)
|
|
104
|
+
# Axes 외곽선 색 변경
|
|
105
|
+
for i in ['top', 'bottom', 'left', 'right']: ax.spines[i].set_color(self.color_tick)
|
|
106
|
+
|
|
107
|
+
# 차트 영역 배경 색상
|
|
108
|
+
ax.set_facecolor(self.color_background)
|
|
109
|
+
|
|
110
|
+
# grid(구분선, 격자) 그리기
|
|
111
|
+
# 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
|
|
112
|
+
ax.grid(**gridKwargs)
|
|
113
|
+
return
|
|
114
|
+
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
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
|
+
from ._draw import BaseMixin as BM, Mixin as M
|
|
10
|
+
from .utils import float_to_str
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Mixin(M):
|
|
14
|
+
def on_move(self, e):
|
|
15
|
+
"If mouse move event active, This method work."
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CollectionMixin(BM):
|
|
20
|
+
lineKwargs = {}
|
|
21
|
+
textboxKwargs = {}
|
|
22
|
+
textKwargs = {}
|
|
23
|
+
color_box = 'k'
|
|
24
|
+
|
|
25
|
+
def _add_collection(self):
|
|
26
|
+
super()._add_collection()
|
|
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')
|
|
40
|
+
self.ax_price.add_artist(self.text_date_price)
|
|
41
|
+
self.text_price = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
|
|
42
|
+
self.ax_price.add_artist(self.text_price)
|
|
43
|
+
|
|
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')
|
|
47
|
+
self.ax_volume.add_artist(self.text_date_volume)
|
|
48
|
+
self.text_volume = Text(**textKwargs, horizontalalignment='left', verticalalignment='center')
|
|
49
|
+
self.ax_volume.add_artist(self.text_volume)
|
|
50
|
+
|
|
51
|
+
self.price_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
|
|
52
|
+
self.ax_price.add_artist(self.price_box)
|
|
53
|
+
self.text_price_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
|
|
54
|
+
self.ax_price.add_artist(self.text_price_info)
|
|
55
|
+
|
|
56
|
+
self.volume_box = LineCollection([], animated=True, linewidth=1.2, edgecolor=self.color_box)
|
|
57
|
+
self.ax_volume.add_artist(self.volume_box)
|
|
58
|
+
self.text_volume_info = Text(**textKwargs, horizontalalignment='left', verticalalignment='top')
|
|
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)
|
|
83
|
+
|
|
84
|
+
self.text_price_info.set_color(color)
|
|
85
|
+
self.text_volume_info.set_color(color)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def change_line_color(self, color):
|
|
89
|
+
self.price_crossline.set_edgecolor(color)
|
|
90
|
+
self.volume_crossline.set_edgecolor(color)
|
|
91
|
+
|
|
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',}
|
|
107
|
+
|
|
108
|
+
class DataMixin(CollectionMixin):
|
|
109
|
+
def _validate_column_key(self):
|
|
110
|
+
super()._validate_column_key()
|
|
111
|
+
for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
|
|
112
|
+
v = getattr(self, i)
|
|
113
|
+
if v in _set_key: raise Exception(f'you can not set "{i}" to column key.\nself.{i}={v!r}')
|
|
114
|
+
return
|
|
115
|
+
|
|
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, *_, **__)
|
|
118
|
+
|
|
119
|
+
if not calc_info:
|
|
120
|
+
keys = set(df.keys())
|
|
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:
|
|
124
|
+
if i not in keys:
|
|
125
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
|
|
126
|
+
else:
|
|
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
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
def _set_lim(self, xmin, xmax, simpler=False, set_ma=True):
|
|
143
|
+
super()._set_lim(xmin, xmax, simpler, set_ma)
|
|
144
|
+
|
|
145
|
+
psub = (self.price_ymax - self.price_ymin)
|
|
146
|
+
self.min_candleboxheight = psub / 8
|
|
147
|
+
|
|
148
|
+
pydistance = psub / 20
|
|
149
|
+
self.text_date_price.set_y(self.price_ymin + pydistance)
|
|
150
|
+
|
|
151
|
+
self.min_volumeboxheight = self.volume_ymax / 4
|
|
152
|
+
|
|
153
|
+
vxsub = self.vxmax - self.vxmin
|
|
154
|
+
self.vmiddle = self.vxmax - int((vxsub) / 2)
|
|
155
|
+
|
|
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)
|
|
163
|
+
|
|
164
|
+
# 정보 텍스트박스
|
|
165
|
+
self.text_price_info.set_y(self.price_ymax - pydistance)
|
|
166
|
+
self.text_volume_info.set_y(yvolume)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class EventMixin(DataMixin):
|
|
171
|
+
in_price_chart, in_volume_chart = (False, False)
|
|
172
|
+
intx = None
|
|
173
|
+
|
|
174
|
+
def _connect_event(self):
|
|
175
|
+
super()._connect_event()
|
|
176
|
+
self.figure.canvas.mpl_connect('motion_notify_event', lambda x: self._on_move(x))
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
def _on_move(self, e):
|
|
180
|
+
self._on_move_action(e)
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
def _on_move_action(self, e: MouseEvent):
|
|
184
|
+
self._check_ax(e)
|
|
185
|
+
|
|
186
|
+
self.intx = None
|
|
187
|
+
if self.in_price_chart or self.in_volume_chart: self._get_x(e)
|
|
188
|
+
return
|
|
189
|
+
|
|
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
|
|
197
|
+
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
def _get_x(self, e: MouseEvent):
|
|
201
|
+
self.intx = e.xdata.__int__()
|
|
202
|
+
if self.intx < 0: self.intx = None
|
|
203
|
+
else:
|
|
204
|
+
try: self.list_index[self.intx]
|
|
205
|
+
except: self.intx = None
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class LineMixin(EventMixin):
|
|
210
|
+
digit_price, digit_volume = (0, 0)
|
|
211
|
+
in_candle, in_volumebar = (False, False)
|
|
212
|
+
|
|
213
|
+
def _on_move(self, e):
|
|
214
|
+
super()._on_move(e)
|
|
215
|
+
|
|
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()
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
def _on_move_price_chart(self, e: MouseEvent):
|
|
225
|
+
x, y = (e.xdata, e.ydata)
|
|
226
|
+
|
|
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()
|
|
230
|
+
|
|
231
|
+
renderer = self.figure.canvas.renderer
|
|
232
|
+
|
|
233
|
+
# 가격
|
|
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)
|
|
236
|
+
self.text_price.set_y(y)
|
|
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
|
|
257
|
+
else:
|
|
258
|
+
self.in_candle = True
|
|
259
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
260
|
+
self.price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
261
|
+
self.price_box.draw(renderer)
|
|
262
|
+
return
|
|
263
|
+
|
|
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
|
|
280
|
+
|
|
281
|
+
# 거래량
|
|
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)
|
|
284
|
+
self.text_volume.set_y(y)
|
|
285
|
+
self.text_volume.draw(renderer)
|
|
286
|
+
|
|
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)
|
|
294
|
+
|
|
295
|
+
# 거래량 강조
|
|
296
|
+
high = self.df[self.volume][index] * 1.15
|
|
297
|
+
low = 0
|
|
298
|
+
if high < self.min_volumeboxheight: high = self.min_volumeboxheight
|
|
299
|
+
|
|
300
|
+
if high < y or y < low: self.in_volumebar = False
|
|
301
|
+
else:
|
|
302
|
+
self.in_volumebar = True
|
|
303
|
+
x1, x2 = (index-0.3, index+1.4)
|
|
304
|
+
self.volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
|
|
305
|
+
self.volume_box.draw(renderer)
|
|
306
|
+
return
|
|
307
|
+
|
|
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
|
+
|
|
314
|
+
class InfoMixin(LineMixin):
|
|
315
|
+
fraction = False
|
|
316
|
+
format_candleinfo = format_candleinfo_ko
|
|
317
|
+
format_volumeinfo = format_volumeinfo_ko
|
|
318
|
+
|
|
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)
|
|
321
|
+
|
|
322
|
+
self._length_text = self.df[(self.volume if self.volume else self.high)].apply(lambda x: len(f'{x:,}')).max()
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
def _on_move_price_chart(self, e):
|
|
326
|
+
super()._on_move_price_chart(e)
|
|
327
|
+
|
|
328
|
+
# 캔들 강조 확인
|
|
329
|
+
if not self.in_candle: return
|
|
330
|
+
|
|
331
|
+
# 캔들 정보
|
|
332
|
+
self.text_price_info.set_text(self._get_info(self.intx))
|
|
333
|
+
|
|
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)
|
|
344
|
+
return
|
|
345
|
+
|
|
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)
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
def _get_info(self, index, is_price=True):
|
|
368
|
+
dt = self.df[self.date][index]
|
|
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
|
+
|
|
378
|
+
if is_price:
|
|
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])
|
|
380
|
+
rate, compare = (self.df['rate'][index], self.df['compare'][index])
|
|
381
|
+
r = f'{rate:+06,.2f}'
|
|
382
|
+
Or, hr, lr = (self.df['rate_open'][index], self.df['rate_high'][index], self.df['rate_low'][index])
|
|
383
|
+
|
|
384
|
+
if self.fraction:
|
|
385
|
+
c = c.__round__(self.digit_price)
|
|
386
|
+
cd = divmod(c, 1)
|
|
387
|
+
if cd[1]: c = f'{float_to_str(cd[0])} {Fraction((cd[1]))}'
|
|
388
|
+
else: c = float_to_str(cd[0])
|
|
389
|
+
comd = divmod(compare, 1)
|
|
390
|
+
if comd[1]: com = f'{float_to_str(comd[0], plus=True)} {Fraction(comd[1])}'
|
|
391
|
+
else: com = float_to_str(comd[0], plus=True)
|
|
392
|
+
o = o.__round__(self.digit_price)
|
|
393
|
+
od = divmod(o, 1)
|
|
394
|
+
if od[1]: o = f'{float_to_str(od[0])} {Fraction(od[1])}'
|
|
395
|
+
else: o = float_to_str(od[0])
|
|
396
|
+
h = h.__round__(self.digit_price)
|
|
397
|
+
hd = divmod(h, 1)
|
|
398
|
+
if hd[1]: h = f'{float_to_str(hd[0])} {Fraction(hd[1])}'
|
|
399
|
+
else: h = float_to_str(hd[0])
|
|
400
|
+
l = l.__round__(self.digit_price)
|
|
401
|
+
ld = divmod(l, 1)
|
|
402
|
+
if ld[1]: l = f'{float_to_str(ld[0])} {Fraction(ld[1])}'
|
|
403
|
+
else: l = float_to_str(ld[0])
|
|
404
|
+
|
|
405
|
+
text = self.format_candleinfo.format(
|
|
406
|
+
dt=dt,
|
|
407
|
+
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
408
|
+
rate=f'{r:>{self._length_text}}%',
|
|
409
|
+
compare=f'{com:>{self._length_text}}{self.unit_price}',
|
|
410
|
+
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
411
|
+
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
412
|
+
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
413
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
414
|
+
)
|
|
415
|
+
else:
|
|
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))
|
|
417
|
+
com = float_to_str(compare, self.digit_price, plus=True)
|
|
418
|
+
|
|
419
|
+
text = self.format_candleinfo.format(
|
|
420
|
+
dt=dt,
|
|
421
|
+
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
422
|
+
rate=f'{r:>{self._length_text}}%',
|
|
423
|
+
compare=f'{com:>{self._length_text}}{self.unit_price}',
|
|
424
|
+
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
425
|
+
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
426
|
+
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
427
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=vr,
|
|
428
|
+
)
|
|
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(
|
|
433
|
+
dt=dt,
|
|
434
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}',
|
|
435
|
+
rate_volume=f'{vr:>{self._length_text}}%',
|
|
436
|
+
compare=f'{com:>{self._length_text}}{self.unit_volume}',
|
|
437
|
+
)
|
|
438
|
+
else: text = ''
|
|
439
|
+
return text
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
class BaseMixin(InfoMixin):
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
|
|
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
|
+
|
|
459
|
+
def _on_draw(self, e):
|
|
460
|
+
super()._on_draw(e)
|
|
461
|
+
return self.on_draw(e)
|
|
462
|
+
|
|
463
|
+
def _on_pick(self, e):
|
|
464
|
+
self.on_pick(e)
|
|
465
|
+
return super()._on_pick(e)
|
|
466
|
+
|
|
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
|
|
471
|
+
|
|
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
|
|
476
|
+
|
|
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
|
|
481
|
+
|
|
482
|
+
def _on_move(self, e):
|
|
483
|
+
super()._on_move(e)
|
|
484
|
+
return self.on_move(e)
|
|
485
|
+
|