seolpyo-mplchart 2.0.0.3__py3-none-any.whl → 2.1.0.4__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 +17 -133
- seolpyo_mplchart/_chart/__init__.py +39 -31
- seolpyo_mplchart/_chart/base/__init__.py +111 -0
- seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
- seolpyo_mplchart/_chart/base/b_artist.py +143 -0
- seolpyo_mplchart/_chart/base/c_draw.py +100 -0
- seolpyo_mplchart/_chart/base/d_segment.py +262 -0
- seolpyo_mplchart/_chart/base/e_axis.py +268 -0
- seolpyo_mplchart/_chart/base/f_background.py +62 -0
- seolpyo_mplchart/_chart/base/g_event.py +66 -0
- seolpyo_mplchart/_chart/base/h_data.py +138 -0
- seolpyo_mplchart/_chart/base/test.py +58 -0
- seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
- seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
- seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
- seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
- seolpyo_mplchart/_chart/cursor/e_axis.py +64 -0
- seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
- seolpyo_mplchart/_chart/cursor/h_data.py +62 -0
- seolpyo_mplchart/_chart/cursor/test.py +69 -0
- seolpyo_mplchart/_chart/slider/__init__.py +169 -0
- seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
- seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
- seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
- seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
- seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
- seolpyo_mplchart/_chart/slider/f_background.py +37 -0
- seolpyo_mplchart/_chart/slider/g_event.py +353 -0
- seolpyo_mplchart/_chart/slider/h_data.py +102 -0
- seolpyo_mplchart/_chart/slider/test.py +71 -0
- seolpyo_mplchart/_config/ax.py +2 -0
- seolpyo_mplchart/_config/candle.py +1 -0
- seolpyo_mplchart/_config/cursor.py +4 -0
- seolpyo_mplchart/_config/figure.py +4 -4
- seolpyo_mplchart/_config/ma.py +2 -0
- seolpyo_mplchart/_config/nums.py +67 -0
- seolpyo_mplchart/_config/slider/config.py +2 -2
- seolpyo_mplchart/_config/slider/figure.py +3 -4
- seolpyo_mplchart/_config/slider/nav.py +3 -2
- seolpyo_mplchart/_config/unit.py +1 -1
- seolpyo_mplchart/_config/volume.py +1 -0
- seolpyo_mplchart/_utils/__init__.py +10 -0
- seolpyo_mplchart/_utils/nums.py +1 -0
- seolpyo_mplchart/_utils/theme/__init__.py +15 -0
- seolpyo_mplchart/_utils/theme/dark.py +57 -0
- seolpyo_mplchart/_utils/theme/light.py +56 -0
- seolpyo_mplchart/_utils/utils.py +28 -0
- seolpyo_mplchart/_utils/xl/__init__.py +15 -0
- seolpyo_mplchart/_utils/xl/csv.py +46 -0
- seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
- seolpyo_mplchart/sample/apple.txt +6058 -0
- seolpyo_mplchart/sample/samsung.txt +5938 -0
- seolpyo_mplchart/test.py +5 -5
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/METADATA +22 -14
- seolpyo_mplchart-2.1.0.4.dist-info/RECORD +90 -0
- seolpyo_mplchart-2.0.0.3.dist-info/RECORD +0 -50
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from ..base import Chart as BaseChart, Figure, ConfigData
|
|
9
|
+
|
|
10
|
+
from .b_artist import ArtistMixin
|
|
11
|
+
from .c_draw import DrawMixin
|
|
12
|
+
from .d_segment import SegmentMixin
|
|
13
|
+
from .e_axis import AxisMixin
|
|
14
|
+
from .g_event import EventMixin
|
|
15
|
+
from .h_data import DataMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CursorMixin(
|
|
19
|
+
ArtistMixin,
|
|
20
|
+
DrawMixin,
|
|
21
|
+
SegmentMixin,
|
|
22
|
+
AxisMixin,
|
|
23
|
+
EventMixin,
|
|
24
|
+
DataMixin
|
|
25
|
+
):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Chart(CursorMixin, BaseChart):
|
|
30
|
+
limit_candle = 400
|
|
31
|
+
limit_wick = 2_000
|
|
32
|
+
candle_on_ma = True
|
|
33
|
+
fraction = False
|
|
34
|
+
|
|
35
|
+
key_date = 'date'
|
|
36
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
37
|
+
key_volume = 'volume'
|
|
38
|
+
|
|
39
|
+
index_list: list[int] = []
|
|
40
|
+
|
|
41
|
+
df: pd.DataFrame
|
|
42
|
+
|
|
43
|
+
CONFIG: ConfigData
|
|
44
|
+
|
|
45
|
+
figure: Figure
|
|
46
|
+
ax_legend: Axes
|
|
47
|
+
ax_price: Axes
|
|
48
|
+
ax_volume: Axes
|
|
49
|
+
|
|
50
|
+
artist_watermark: Text
|
|
51
|
+
collection_candle: LineCollection
|
|
52
|
+
collection_volume: LineCollection
|
|
53
|
+
collection_ma: LineCollection
|
|
54
|
+
|
|
55
|
+
in_chart = False
|
|
56
|
+
in_chart_price = False
|
|
57
|
+
in_chart_volume = False
|
|
58
|
+
|
|
59
|
+
in_candle = False
|
|
60
|
+
in_volume = False
|
|
61
|
+
|
|
62
|
+
collection_price_crossline: LineCollection
|
|
63
|
+
collection_volume_crossline: LineCollection
|
|
64
|
+
|
|
65
|
+
artist_label_x: Text = None
|
|
66
|
+
artist_label_y: Text = None
|
|
67
|
+
|
|
68
|
+
collection_box_price: LineCollection
|
|
69
|
+
collection_box_volume: LineCollection
|
|
70
|
+
|
|
71
|
+
artist_info_candle: Text
|
|
72
|
+
artist_info_volume: Text
|
|
73
|
+
|
|
74
|
+
###
|
|
75
|
+
|
|
76
|
+
segment_volume: np.ndarray
|
|
77
|
+
segment_volume_wick: np.ndarray
|
|
78
|
+
facecolor_volume: np.ndarray
|
|
79
|
+
edgecolor_volume: np.ndarray
|
|
80
|
+
|
|
81
|
+
segment_candle: np.ndarray
|
|
82
|
+
segment_candle_wick: np.ndarray
|
|
83
|
+
segment_priceline: np.ndarray
|
|
84
|
+
facecolor_candle: np.ndarray
|
|
85
|
+
edgecolor_candle: np.ndarray
|
|
86
|
+
|
|
87
|
+
segment_ma: np.ndarray
|
|
88
|
+
edgecolor_ma: np.ndarray
|
|
89
|
+
|
|
90
|
+
price_ymin: int
|
|
91
|
+
price_ymax: int
|
|
92
|
+
volume_ymax: int
|
|
93
|
+
|
|
94
|
+
chart_price_ymax: float
|
|
95
|
+
chart_volume_ymax: float
|
|
96
|
+
|
|
97
|
+
vxmin: int
|
|
98
|
+
vxmax: int
|
|
99
|
+
|
|
100
|
+
v0: int
|
|
101
|
+
v1: int
|
|
102
|
+
vmiddle: int
|
|
103
|
+
|
|
104
|
+
min_height_box_candle: float
|
|
105
|
+
min_height_box_volume: float
|
|
106
|
+
|
|
107
|
+
###
|
|
108
|
+
|
|
109
|
+
_visible_ma: set[int] = set()
|
|
110
|
+
_edgecolor_ma = []
|
|
111
|
+
|
|
112
|
+
_background = None
|
|
113
|
+
_background_background = None
|
|
114
|
+
_creating_background = False
|
|
115
|
+
|
|
116
|
+
_length_text: int
|
|
117
|
+
|
|
118
|
+
_in_mouse_move = False
|
|
119
|
+
|
|
120
|
+
def refresh(self):
|
|
121
|
+
super().refresh()
|
|
122
|
+
|
|
123
|
+
self._set_length_text()
|
|
124
|
+
return
|
|
125
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
|
|
5
|
+
from ..._config import ConfigData
|
|
6
|
+
from ..base.a_canvas import Figure
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Base:
|
|
10
|
+
CONFIG: ConfigData
|
|
11
|
+
|
|
12
|
+
figure: Figure
|
|
13
|
+
ax_price: Axes
|
|
14
|
+
ax_volume: Axes
|
|
15
|
+
|
|
16
|
+
add_artists: callable
|
|
17
|
+
set_artists: callable
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CrossLineMixin(Base):
|
|
21
|
+
def _add_crosslines(self):
|
|
22
|
+
kwargs = {'segments': [], 'animated': True}
|
|
23
|
+
|
|
24
|
+
self.collection_price_crossline = LineCollection(**kwargs)
|
|
25
|
+
self.ax_price.add_artist(self.collection_price_crossline)
|
|
26
|
+
|
|
27
|
+
self.collection_volume_crossline = LineCollection(**kwargs)
|
|
28
|
+
self.ax_volume.add_artist(self.collection_volume_crossline)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
def _set_crosslines(self):
|
|
32
|
+
kwargs = self.CONFIG.CURSOR.CROSSLINE.__dict__
|
|
33
|
+
kwargs.update({'segments': [], 'animated': True})
|
|
34
|
+
|
|
35
|
+
self.collection_price_crossline.set(**kwargs)
|
|
36
|
+
self.collection_volume_crossline.set(**kwargs)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LabelMixin(Base):
|
|
41
|
+
def _add_artist_labels(self):
|
|
42
|
+
kwargs = {'text': '', 'animated': True,}
|
|
43
|
+
|
|
44
|
+
self.artist_label_x = Text(**kwargs)
|
|
45
|
+
self.figure.add_artist(self.artist_label_x)
|
|
46
|
+
|
|
47
|
+
self.artist_label_y = Text(**kwargs)
|
|
48
|
+
self.figure.add_artist(self.artist_label_y)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
def _set_artist_labels(self):
|
|
52
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
53
|
+
kwargs.update({'text': ' ', 'animated': True, 'horizontalalignment': 'center', 'verticalalignment': 'center', 'clip_on':True})
|
|
54
|
+
|
|
55
|
+
self.artist_label_x.set(**kwargs)
|
|
56
|
+
self.artist_label_y.set(**kwargs)
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BoxMixin(Base):
|
|
61
|
+
def _add_box_collections(self):
|
|
62
|
+
kwargs = {'segments': [], 'animated': True,}
|
|
63
|
+
|
|
64
|
+
self.collection_box_price = LineCollection(**kwargs)
|
|
65
|
+
self.ax_price.add_artist(self.collection_box_price)
|
|
66
|
+
self.collection_box_volume = LineCollection(**kwargs)
|
|
67
|
+
self.ax_volume.add_artist(self.collection_box_volume)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
def _set_box_collections(self):
|
|
71
|
+
kwargs = self.CONFIG.CURSOR.BOX.__dict__
|
|
72
|
+
kwargs.update({'segments': [], 'animated': True,})
|
|
73
|
+
|
|
74
|
+
self.collection_box_price.set(**kwargs)
|
|
75
|
+
self.collection_box_volume.set(**kwargs)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class InfoMixin(Base):
|
|
80
|
+
def _add_info_texts(self):
|
|
81
|
+
kwargs = {'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',}
|
|
82
|
+
|
|
83
|
+
self.artist_info_candle = Text(**kwargs)
|
|
84
|
+
self.ax_price.add_artist(self.artist_info_candle)
|
|
85
|
+
self.artist_info_volume = Text(**kwargs)
|
|
86
|
+
self.ax_volume.add_artist(self.artist_info_volume)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def _set_info_texts(self):
|
|
90
|
+
kwargs = self.CONFIG.CURSOR.TEXT.to_dict()
|
|
91
|
+
kwargs.update({'text': '', 'animated': True, 'horizontalalignment': 'left', 'verticalalignment': 'top',})
|
|
92
|
+
|
|
93
|
+
self.artist_info_candle.set(**kwargs)
|
|
94
|
+
self.artist_info_volume.set(**kwargs)
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ArtistMixin(CrossLineMixin, LabelMixin, BoxMixin, InfoMixin):
|
|
99
|
+
collection_price_crossline: LineCollection
|
|
100
|
+
collection_volume_crossline: LineCollection
|
|
101
|
+
|
|
102
|
+
artist_label_x: Text = None
|
|
103
|
+
artist_label_y: Text = None
|
|
104
|
+
|
|
105
|
+
collection_box_price: LineCollection
|
|
106
|
+
collection_box_volume: LineCollection
|
|
107
|
+
|
|
108
|
+
artist_info_candle: Text
|
|
109
|
+
artist_info_volume: Text
|
|
110
|
+
|
|
111
|
+
def add_artists(self):
|
|
112
|
+
super().add_artists()
|
|
113
|
+
|
|
114
|
+
self._add_crosslines()
|
|
115
|
+
self._add_artist_labels()
|
|
116
|
+
self._add_box_collections()
|
|
117
|
+
self._add_info_texts()
|
|
118
|
+
|
|
119
|
+
self.set_artists()
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
def set_artists(self):
|
|
123
|
+
super().set_artists()
|
|
124
|
+
|
|
125
|
+
self._set_crosslines()
|
|
126
|
+
self._set_artist_labels()
|
|
127
|
+
self._set_box_collections()
|
|
128
|
+
self._set_info_texts()
|
|
129
|
+
return
|
|
130
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from matplotlib.axes import Axes
|
|
2
|
+
from matplotlib.collections import LineCollection
|
|
3
|
+
from matplotlib.text import Text
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from ..._config import ConfigData
|
|
7
|
+
from ..._chart.base.a_canvas import Figure
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Base:
|
|
11
|
+
CONFIG: ConfigData
|
|
12
|
+
|
|
13
|
+
df: pd.DataFrame
|
|
14
|
+
|
|
15
|
+
watermark: str
|
|
16
|
+
|
|
17
|
+
figure: Figure
|
|
18
|
+
ax_legend: Axes
|
|
19
|
+
ax_price: Axes
|
|
20
|
+
ax_volume: Axes
|
|
21
|
+
artist_watermark: Text
|
|
22
|
+
collection_candle: LineCollection
|
|
23
|
+
collection_volume: LineCollection
|
|
24
|
+
collection_ma: LineCollection
|
|
25
|
+
|
|
26
|
+
collection_price_crossline: LineCollection
|
|
27
|
+
collection_volume_crossline: LineCollection
|
|
28
|
+
|
|
29
|
+
artist_label_x: Text
|
|
30
|
+
artist_label_y: Text
|
|
31
|
+
|
|
32
|
+
collection_box_price: LineCollection
|
|
33
|
+
collection_box_volume: LineCollection
|
|
34
|
+
|
|
35
|
+
artist_info_candle: Text
|
|
36
|
+
artist_info_volume: Text
|
|
37
|
+
|
|
38
|
+
in_chart_price: bool
|
|
39
|
+
in_chart_volume: bool
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CrosslineMixin(Base):
|
|
43
|
+
def _draw_crossline(self):
|
|
44
|
+
renderer = self.figure.canvas.renderer
|
|
45
|
+
for artist in [
|
|
46
|
+
self.collection_price_crossline,
|
|
47
|
+
self.collection_volume_crossline,
|
|
48
|
+
]:
|
|
49
|
+
artist.draw(renderer)
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LabelMixin(Base):
|
|
54
|
+
def _draw_label_x(self):
|
|
55
|
+
artist = self.artist_label_x
|
|
56
|
+
renderer = self.figure.canvas.renderer
|
|
57
|
+
|
|
58
|
+
artist.draw(renderer)
|
|
59
|
+
# print(f'{artist.get_position()=}')
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
def _draw_label_y(self):
|
|
63
|
+
artist = self.artist_label_y
|
|
64
|
+
renderer = self.figure.canvas.renderer
|
|
65
|
+
|
|
66
|
+
artist.draw(renderer)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class BoxMixin(Base):
|
|
71
|
+
def _draw_box_candle(self):
|
|
72
|
+
renderer = self.figure.canvas.renderer
|
|
73
|
+
self.collection_box_price.draw(renderer)
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
def _draw_box_volume(self):
|
|
77
|
+
renderer = self.figure.canvas.renderer
|
|
78
|
+
self.collection_box_volume.draw(renderer)
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InfoMixin(Base):
|
|
83
|
+
def _draw_info_candle(self):
|
|
84
|
+
renderer = self.figure.canvas.renderer
|
|
85
|
+
self.artist_info_candle.draw(renderer)
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
def _draw_info_volume(self):
|
|
89
|
+
renderer = self.figure.canvas.renderer
|
|
90
|
+
self.artist_info_volume.draw(renderer)
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DrawMixin(CrosslineMixin, LabelMixin, BoxMixin, InfoMixin):
|
|
95
|
+
pass
|
|
96
|
+
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
from fractions import Fraction
|
|
2
|
+
|
|
3
|
+
from matplotlib.axes import Axes
|
|
4
|
+
from matplotlib.collections import LineCollection
|
|
5
|
+
from matplotlib.text import Text
|
|
6
|
+
from matplotlib.backend_bases import MouseEvent
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ..._config import ConfigData
|
|
10
|
+
from ..._utils.nums import float_to_str
|
|
11
|
+
from ..base.a_canvas import Figure
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Base:
|
|
15
|
+
CONFIG: ConfigData
|
|
16
|
+
|
|
17
|
+
key_volume: str
|
|
18
|
+
_length_text: int
|
|
19
|
+
df: pd.DataFrame
|
|
20
|
+
|
|
21
|
+
watermark: str
|
|
22
|
+
|
|
23
|
+
v0: int
|
|
24
|
+
v1: int
|
|
25
|
+
vmiddle: int
|
|
26
|
+
vxmin: int
|
|
27
|
+
vxmax: int
|
|
28
|
+
price_ymin: int
|
|
29
|
+
price_ymax: int
|
|
30
|
+
volume_ymax: int
|
|
31
|
+
|
|
32
|
+
figure: Figure
|
|
33
|
+
ax_legend: Axes
|
|
34
|
+
ax_price: Axes
|
|
35
|
+
ax_volume: Axes
|
|
36
|
+
artist_watermark: Text
|
|
37
|
+
collection_candle: LineCollection
|
|
38
|
+
collection_volume: LineCollection
|
|
39
|
+
collection_ma: LineCollection
|
|
40
|
+
|
|
41
|
+
collection_price_crossline: LineCollection
|
|
42
|
+
collection_volume_crossline: LineCollection
|
|
43
|
+
|
|
44
|
+
artist_label_x: Text
|
|
45
|
+
artist_label_y: Text
|
|
46
|
+
|
|
47
|
+
collection_box_price: LineCollection
|
|
48
|
+
collection_box_volume: LineCollection
|
|
49
|
+
|
|
50
|
+
artist_info_candle: Text
|
|
51
|
+
artist_info_volume: Text
|
|
52
|
+
|
|
53
|
+
in_chart_price: bool
|
|
54
|
+
in_chart_volume: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CrosslineMixin(Base):
|
|
58
|
+
def _set_crossline(self, e: MouseEvent,):
|
|
59
|
+
x, y = (e.xdata, e.ydata)
|
|
60
|
+
|
|
61
|
+
if self.in_chart_price:
|
|
62
|
+
seg = [
|
|
63
|
+
[
|
|
64
|
+
(x, self.price_ymin),
|
|
65
|
+
(x, self.price_ymax),
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
(self.vxmin, y),
|
|
69
|
+
(self.vxmax, y),
|
|
70
|
+
]
|
|
71
|
+
]
|
|
72
|
+
self.collection_price_crossline.set_segments(seg)
|
|
73
|
+
seg = [
|
|
74
|
+
[
|
|
75
|
+
(x, 0),
|
|
76
|
+
(x, self.volume_ymax),
|
|
77
|
+
]
|
|
78
|
+
]
|
|
79
|
+
self.collection_volume_crossline.set_segments(seg)
|
|
80
|
+
elif self.in_chart_volume:
|
|
81
|
+
seg = [
|
|
82
|
+
[
|
|
83
|
+
(x, self.price_ymin),
|
|
84
|
+
(x, self.price_ymax),
|
|
85
|
+
]
|
|
86
|
+
]
|
|
87
|
+
self.collection_price_crossline.set_segments(seg)
|
|
88
|
+
seg = [
|
|
89
|
+
[
|
|
90
|
+
(x, 0),
|
|
91
|
+
(x, self.volume_ymax),
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
(self.vxmin, y),
|
|
95
|
+
(self.vxmax, y)
|
|
96
|
+
]
|
|
97
|
+
]
|
|
98
|
+
self.collection_volume_crossline.set_segments(seg)
|
|
99
|
+
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class LabelMixin(Base):
|
|
104
|
+
def _set_label_x(self, e: MouseEvent):
|
|
105
|
+
xdata, ydata = (e.xdata, e.xdata)
|
|
106
|
+
# print(f'{(x, xdata)=}')
|
|
107
|
+
|
|
108
|
+
if xdata < 0 or xdata is None:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
text = self.df.iloc[int(xdata)]['date']
|
|
113
|
+
except:
|
|
114
|
+
return
|
|
115
|
+
# print(f'{text=}')
|
|
116
|
+
|
|
117
|
+
display_coords = e.inaxes.transData.transform((xdata, ydata))
|
|
118
|
+
figure_coords = self.figure.transFigure.inverted()\
|
|
119
|
+
.transform(display_coords)
|
|
120
|
+
# print(f'{figure_coords=}')
|
|
121
|
+
|
|
122
|
+
artist = self.artist_label_x
|
|
123
|
+
|
|
124
|
+
artist.set_text(text)
|
|
125
|
+
artist.set_x(figure_coords[0])
|
|
126
|
+
self._set_label_x_position()
|
|
127
|
+
return 1
|
|
128
|
+
|
|
129
|
+
def _set_label_x_position(self):
|
|
130
|
+
artist = self.artist_label_x
|
|
131
|
+
renderer = self.figure.canvas.renderer
|
|
132
|
+
|
|
133
|
+
# Axes 하단 경계 좌표
|
|
134
|
+
boundary = self.ax_volume.get_position()\
|
|
135
|
+
.y0
|
|
136
|
+
# print(f'{y0=}')
|
|
137
|
+
|
|
138
|
+
if not artist.get_text():
|
|
139
|
+
artist.set_text(' ')
|
|
140
|
+
|
|
141
|
+
# Text bbox 너비
|
|
142
|
+
bbox = artist.get_bbox_patch()\
|
|
143
|
+
.get_window_extent(renderer)
|
|
144
|
+
bbox_size = bbox.height
|
|
145
|
+
# 밀어야 하는 값
|
|
146
|
+
fig_size = self.figure.bbox.height
|
|
147
|
+
offset = (bbox_size + 10) / fig_size
|
|
148
|
+
# print(f'{box_width_fig=}')
|
|
149
|
+
|
|
150
|
+
# x축 값(가격 또는 거래량)
|
|
151
|
+
# self.artist_label_y.set_x(x1)
|
|
152
|
+
y = boundary - (offset / 2)
|
|
153
|
+
# print(f'{(x1, x)=}')
|
|
154
|
+
artist.set_y(y)
|
|
155
|
+
return
|
|
156
|
+
|
|
157
|
+
def _set_label_y(self, e: MouseEvent, *, is_price_chart):
|
|
158
|
+
xdata, ydata = (e.xdata, e.ydata)
|
|
159
|
+
artist = self.artist_label_y
|
|
160
|
+
|
|
161
|
+
if is_price_chart:
|
|
162
|
+
text = float_to_str(ydata, digit=self.CONFIG.UNIT.digit) + self.CONFIG.UNIT.price
|
|
163
|
+
else:
|
|
164
|
+
text = float_to_str(ydata, digit=self.CONFIG.UNIT.digit_volume) + self.CONFIG.UNIT.volume
|
|
165
|
+
|
|
166
|
+
display_coords = e.inaxes.transData.transform((xdata, ydata))
|
|
167
|
+
figure_coords = self.figure.transFigure.inverted().transform(display_coords)
|
|
168
|
+
|
|
169
|
+
artist.set_text(text)
|
|
170
|
+
artist.set_y(figure_coords[1])
|
|
171
|
+
self._set_label_y_position()
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
def _set_label_y_position(self):
|
|
175
|
+
artist = self.artist_label_y
|
|
176
|
+
renderer = self.figure.canvas.renderer
|
|
177
|
+
|
|
178
|
+
# Axes 우측 경계 좌표
|
|
179
|
+
boundary = self.ax_volume.get_position()\
|
|
180
|
+
.x1
|
|
181
|
+
# print(f'{boundary=}')
|
|
182
|
+
|
|
183
|
+
if not artist.get_text():
|
|
184
|
+
artist.set_text(' ')
|
|
185
|
+
|
|
186
|
+
# Text bbox 너비
|
|
187
|
+
bbox = artist.get_bbox_patch()\
|
|
188
|
+
.get_window_extent(renderer)
|
|
189
|
+
bbox_size = bbox.width
|
|
190
|
+
# 밀어야 하는 값
|
|
191
|
+
fig_size = self.figure.bbox.width
|
|
192
|
+
offset = (bbox_size + 8) / fig_size
|
|
193
|
+
# print(f'{box_width_fig=}')
|
|
194
|
+
|
|
195
|
+
# x축 값(가격 또는 거래량)
|
|
196
|
+
# self.artist_label_y.set_x(x1)
|
|
197
|
+
x = boundary + (offset / 2)
|
|
198
|
+
# print(f'{(x1, x)=}')
|
|
199
|
+
artist.set_x(x)
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class BoxMixin(Base):
|
|
204
|
+
def _set_box_candle(self, segment):
|
|
205
|
+
self.collection_box_price.set_segments(segment)
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
def _set_box_volume(self, segment):
|
|
209
|
+
self.collection_box_volume.set_segments(segment)
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class InfoMixin(Base):
|
|
214
|
+
fraction = False
|
|
215
|
+
|
|
216
|
+
def _set_info_candle(self, ind):
|
|
217
|
+
text = self._get_info(ind, is_price=True)
|
|
218
|
+
self.artist_info_candle.set_text(text)
|
|
219
|
+
|
|
220
|
+
# 정보 텍스트를 중앙에 몰리게 설정할 수도 있지만,
|
|
221
|
+
# 그런 경우 차트를 가리므로 좌우 끝단에 위치하도록 설정
|
|
222
|
+
if self.vmiddle < ind:
|
|
223
|
+
self.artist_info_candle.set_x(self.v0)
|
|
224
|
+
else:
|
|
225
|
+
# self.artist_info_candle.set_x(self.vmax - self.x_distance)
|
|
226
|
+
# self.artist_info_candle.set_horizontalalignment('right')
|
|
227
|
+
# 텍스트박스 크기 가져오기
|
|
228
|
+
bbox = self.artist_info_candle.get_window_extent()\
|
|
229
|
+
.transformed(self.ax_price.transData.inverted())
|
|
230
|
+
width = bbox.x1 - bbox.x0
|
|
231
|
+
self.artist_info_candle.set_x(self.v1 - width)
|
|
232
|
+
|
|
233
|
+
self.artist_info_candle.draw(self.figure.canvas.renderer)
|
|
234
|
+
return
|
|
235
|
+
|
|
236
|
+
def _set_info_volume(self, ind):
|
|
237
|
+
text = self._get_info(ind, is_price=False)
|
|
238
|
+
self.artist_info_volume.set_text(text)
|
|
239
|
+
|
|
240
|
+
if self.vmiddle < ind:
|
|
241
|
+
self.artist_info_volume.set_x(self.v0)
|
|
242
|
+
else:
|
|
243
|
+
# self.artist_info_volume.set_x(self.vmax - self.x_distance)
|
|
244
|
+
# self.artist_info_volume.set_horizontalalignment('right')
|
|
245
|
+
# 텍스트박스 크기 가져오기
|
|
246
|
+
bbox = self.artist_info_volume.get_window_extent()\
|
|
247
|
+
.transformed(self.ax_price.transData.inverted())
|
|
248
|
+
width = bbox.x1 - bbox.x0
|
|
249
|
+
self.artist_info_volume.set_x(self.v1 - width)
|
|
250
|
+
|
|
251
|
+
self.artist_info_volume.draw(self.figure.canvas.renderer)
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
def get_info_kwargs(self, is_price: bool, **kwargs)-> dict:
|
|
255
|
+
"""
|
|
256
|
+
get text info kwargs
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
is_price (bool): is price chart info or not
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
dict[str, any]: text info kwargs
|
|
263
|
+
"""
|
|
264
|
+
return kwargs
|
|
265
|
+
|
|
266
|
+
def _get_info(self, ind: int, is_price=True):
|
|
267
|
+
# print(f'{self._length_text=}')
|
|
268
|
+
series = self.df.iloc[ind]
|
|
269
|
+
|
|
270
|
+
dt = series['date']
|
|
271
|
+
if not self.key_volume:
|
|
272
|
+
v, vr = ('-', '-')
|
|
273
|
+
else:
|
|
274
|
+
v, vr = series.loc[['volume', 'rate_volume']]
|
|
275
|
+
# print(f'{self.CONFIG.UNIT.digit_volume=}')
|
|
276
|
+
v = float_to_str(v, digit=self.CONFIG.UNIT.digit_volume)
|
|
277
|
+
# if not v % 1:
|
|
278
|
+
# v = int(v)
|
|
279
|
+
vr = f'{vr:+06,.2f}'
|
|
280
|
+
|
|
281
|
+
if is_price:
|
|
282
|
+
o, h, l, c = (series['open'], series['high'], series['low'], series['close'])
|
|
283
|
+
rate, compare = (series['rate'], series['compare'])
|
|
284
|
+
r = f'{rate:+06,.2f}'
|
|
285
|
+
Or, hr, lr = (series['rate_open'], series['rate_high'], series['rate_low'])
|
|
286
|
+
|
|
287
|
+
if self.fraction:
|
|
288
|
+
data = {}
|
|
289
|
+
c = round(c, self.CONFIG.UNIT.digit)
|
|
290
|
+
for value, key in [
|
|
291
|
+
[c, 'close'],
|
|
292
|
+
[compare, 'compare'],
|
|
293
|
+
[o, 'open'],
|
|
294
|
+
[h, 'high'],
|
|
295
|
+
[l, 'low'],
|
|
296
|
+
]:
|
|
297
|
+
div = divmod(value, 1)
|
|
298
|
+
if div[1]:
|
|
299
|
+
if div[0]:
|
|
300
|
+
data[key] = f'{float_to_str(div[0])} {Fraction((div[1]))}'
|
|
301
|
+
else:
|
|
302
|
+
data[key] = f' {Fraction((div[1]))}'
|
|
303
|
+
else:
|
|
304
|
+
data[key] = float_to_str(div[0])
|
|
305
|
+
# print(f'{data=}')
|
|
306
|
+
|
|
307
|
+
kwargs = self.get_info_kwargs(
|
|
308
|
+
is_price=is_price,
|
|
309
|
+
dt=dt,
|
|
310
|
+
close=f'{data["close"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
311
|
+
rate=f'{r:>{self._length_text}}%',
|
|
312
|
+
compare=f'{data["compare"]:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
313
|
+
open=f'{data["open"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
|
|
314
|
+
high=f'{data["high"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
|
|
315
|
+
low=f'{data["low"]:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
|
|
316
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
|
|
317
|
+
)
|
|
318
|
+
text = self.CONFIG.FORMAT.candle.format(**kwargs)
|
|
319
|
+
else:
|
|
320
|
+
o, h, l, c = (
|
|
321
|
+
float_to_str(o, digit=self.CONFIG.UNIT.digit),
|
|
322
|
+
float_to_str(h, digit=self.CONFIG.UNIT.digit),
|
|
323
|
+
float_to_str(l, digit=self.CONFIG.UNIT.digit),
|
|
324
|
+
float_to_str(c, digit=self.CONFIG.UNIT.digit),
|
|
325
|
+
)
|
|
326
|
+
com = float_to_str(compare, digit=self.CONFIG.UNIT.digit, plus=True)
|
|
327
|
+
|
|
328
|
+
kwargs = self.get_info_kwargs(
|
|
329
|
+
is_price=is_price,
|
|
330
|
+
dt=dt,
|
|
331
|
+
close=f'{c:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
332
|
+
rate=f'{r:>{self._length_text}}%',
|
|
333
|
+
compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.price}',
|
|
334
|
+
open=f'{o:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_open=f'{Or:+06,.2f}%',
|
|
335
|
+
high=f'{h:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_high=f'{hr:+06,.2f}%',
|
|
336
|
+
low=f'{l:>{self._length_text}}{self.CONFIG.UNIT.price}', rate_low=f'{lr:+06,.2f}%',
|
|
337
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}', rate_volume=f'{vr}%',
|
|
338
|
+
)
|
|
339
|
+
text = self.CONFIG.FORMAT.candle.format(**kwargs)
|
|
340
|
+
elif self.key_volume:
|
|
341
|
+
compare = self.df.loc[ind, 'compare_volume']
|
|
342
|
+
com = float_to_str(compare, digit=self.CONFIG.UNIT.digit_volume, plus=True)
|
|
343
|
+
kwargs = self.get_info_kwargs(
|
|
344
|
+
is_price=is_price,
|
|
345
|
+
dt=dt,
|
|
346
|
+
volume=f'{v:>{self._length_text}}{self.CONFIG.UNIT.volume}',
|
|
347
|
+
rate_volume=f'{vr:>{self._length_text}}%',
|
|
348
|
+
compare=f'{com:>{self._length_text}}{self.CONFIG.UNIT.volume}',
|
|
349
|
+
)
|
|
350
|
+
text = self.CONFIG.FORMAT.volume.format(**kwargs)
|
|
351
|
+
else:
|
|
352
|
+
text = ''
|
|
353
|
+
|
|
354
|
+
return text
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class SegmentMixin(CrosslineMixin, LabelMixin, BoxMixin, InfoMixin):
|
|
358
|
+
fraction = False
|
|
359
|
+
|