seolpyo-mplchart 2.0.0.3__py3-none-any.whl → 2.1.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.
- 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 +267 -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 +65 -0
- seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
- seolpyo_mplchart/_chart/cursor/h_data.py +61 -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/candle.py +1 -0
- seolpyo_mplchart/_config/figure.py +3 -4
- seolpyo_mplchart/_config/ma.py +2 -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/volume.py +1 -0
- seolpyo_mplchart/_utils/__init__.py +10 -0
- seolpyo_mplchart/_utils/nums.py +67 -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.dist-info}/METADATA +21 -13
- seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -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.dist-info}/WHEEL +0 -0
- {seolpyo_mplchart-2.0.0.3.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
seolpyo_mplchart/__init__.py
CHANGED
|
@@ -2,11 +2,22 @@ import json
|
|
|
2
2
|
from typing import Literal
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
import matplotlib.pyplot as plt
|
|
6
|
-
import pandas as pd
|
|
7
|
-
|
|
8
5
|
from ._chart import OnlyChart, CursorChart, SliderChart
|
|
9
|
-
from ._config import
|
|
6
|
+
from ._config import (
|
|
7
|
+
ConfigData, DEFAULTCONFIG, DEFAULTCONFIG_EN,
|
|
8
|
+
SliderConfigData, SLIDERCONFIG, SLIDERCONFIG_EN,
|
|
9
|
+
)
|
|
10
|
+
from ._utils import (
|
|
11
|
+
float_to_str,
|
|
12
|
+
xl_to_dataList,
|
|
13
|
+
convert_num,
|
|
14
|
+
float_to_str,
|
|
15
|
+
convert_unit, convert_unit_en,
|
|
16
|
+
data_unit_ko, data_unit_en,
|
|
17
|
+
list_to_DataFrame,
|
|
18
|
+
show, close,
|
|
19
|
+
switch_backend,
|
|
20
|
+
)
|
|
10
21
|
|
|
11
22
|
|
|
12
23
|
# __all__ = [
|
|
@@ -49,137 +60,10 @@ def sample(stock: Literal['samsung', 'apple']='samsung', chart: Literal['Chart',
|
|
|
49
60
|
|
|
50
61
|
with open(path_file, 'r', encoding='utf-8') as txt:
|
|
51
62
|
data = json.load(txt)
|
|
52
|
-
df =
|
|
63
|
+
df = list_to_DataFrame(data)
|
|
53
64
|
|
|
54
65
|
CHART.set_data(df)
|
|
55
66
|
|
|
56
|
-
show()
|
|
57
|
-
close()
|
|
58
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def switch_backend(newbackend='TkAgg'):
|
|
62
|
-
"call matplotlib.pyplot.switch_backend(newbackend)"
|
|
63
|
-
return plt.switch_backend(newbackend)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def close(fig='all'):
|
|
67
|
-
"call matplotlib.pyplot.close(fig)"
|
|
68
|
-
return plt.close(fig)
|
|
69
|
-
|
|
70
|
-
def show(Close=True):
|
|
71
|
-
"""
|
|
72
|
-
call matplotlib.pyplot.show()
|
|
73
|
-
```if Close``` if True, run matplotlib.pyplot.close('all') after window closee.
|
|
74
|
-
"""
|
|
75
|
-
plt.show()
|
|
76
|
-
if Close:
|
|
77
|
-
close()
|
|
67
|
+
show(Close=True)
|
|
78
68
|
return
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
def set_theme(config: ConfigData|SliderConfigData, theme: Literal['light', 'dark']='dark'):
|
|
82
|
-
if theme == 'light':
|
|
83
|
-
# axes
|
|
84
|
-
config.FIGURE.facecolor = '#fafafa'
|
|
85
|
-
config.AX.facecolor = '#fafafa'
|
|
86
|
-
config.AX.TICK.edgecolor = 'k'
|
|
87
|
-
config.AX.TICK.fontcolor = 'k'
|
|
88
|
-
config.AX.GRID.color = '#d0d0d0'
|
|
89
|
-
|
|
90
|
-
# candle
|
|
91
|
-
config.CANDLE.line_color = 'k'
|
|
92
|
-
config.CANDLE.FACECOLOR.bull_rise = '#FF2400'
|
|
93
|
-
config.CANDLE.FACECOLOR.bull_fall = 'w'
|
|
94
|
-
config.CANDLE.FACECOLOR.bear_fall = '#1E90FF'
|
|
95
|
-
config.CANDLE.FACECOLOR.bear_rise = 'w'
|
|
96
|
-
|
|
97
|
-
config.CANDLE.EDGECOLOR.bull_rise = '#FF2400'
|
|
98
|
-
config.CANDLE.EDGECOLOR.bull_fall = '#FF2400'
|
|
99
|
-
config.CANDLE.EDGECOLOR.bear_fall = '#1E90FF'
|
|
100
|
-
config.CANDLE.EDGECOLOR.bear_rise = '#1E90FF'
|
|
101
|
-
config.CANDLE.EDGECOLOR.doji = 'k'
|
|
102
|
-
|
|
103
|
-
# volume
|
|
104
|
-
config.VOLUME.FACECOLOR.rise = '#F27663'
|
|
105
|
-
config.VOLUME.FACECOLOR.fall = '#70B5F2'
|
|
106
|
-
config.VOLUME.FACECOLOR.doji = '#BEBEBE'
|
|
107
|
-
|
|
108
|
-
config.VOLUME.EDGECOLOR.rise = '#F27663'
|
|
109
|
-
config.VOLUME.EDGECOLOR.fall = '#70B5F2'
|
|
110
|
-
config.VOLUME.EDGECOLOR.doji = '#BEBEBE'
|
|
111
|
-
|
|
112
|
-
# ma
|
|
113
|
-
config.MA.color_default = 'k'
|
|
114
|
-
config.MA.color_list = ['#8B00FF', '#008000', '#A0522D', '#008B8B', '#FF0080']
|
|
115
|
-
# text
|
|
116
|
-
config.CURSOR.TEXT.BBOX.facecolor = 'w'
|
|
117
|
-
config.CURSOR.TEXT.BBOX.edgecolor = 'k'
|
|
118
|
-
config.CURSOR.TEXT.color = 'k'
|
|
119
|
-
|
|
120
|
-
# box
|
|
121
|
-
config.CURSOR.BOX.edgecolor = 'k'
|
|
122
|
-
|
|
123
|
-
# line
|
|
124
|
-
config.CURSOR.CROSSLINE.edgecolor = 'k'
|
|
125
|
-
|
|
126
|
-
# wartermark
|
|
127
|
-
config.FIGURE.WATERMARK.color = 'k'
|
|
128
|
-
|
|
129
|
-
if getattr(config, 'SLIDER', None):
|
|
130
|
-
config.SLIDER.NAVIGATOR.edgecolor = '#2962FF'
|
|
131
|
-
config.SLIDER.NAVIGATOR.facecolor = '#0000002E'
|
|
132
|
-
elif theme == 'dark':
|
|
133
|
-
# axes
|
|
134
|
-
config.FIGURE.facecolor = '#0f0f0f'
|
|
135
|
-
config.AX.facecolor = '#0f0f0f'
|
|
136
|
-
config.AX.TICK.edgecolor = '#dbdbdb'
|
|
137
|
-
config.AX.TICK.fontcolor = '#dbdbdb'
|
|
138
|
-
config.AX.GRID.color = '#1c1c1c'
|
|
139
|
-
|
|
140
|
-
# candle
|
|
141
|
-
config.CANDLE.line_color = 'w'
|
|
142
|
-
config.CANDLE.FACECOLOR.bull_rise = '#089981'
|
|
143
|
-
config.CANDLE.FACECOLOR.bull_fall = '#0f0f0f'
|
|
144
|
-
config.CANDLE.FACECOLOR.bear_fall = '#f23645'
|
|
145
|
-
config.CANDLE.FACECOLOR.bear_rise = '#0f0f0f'
|
|
146
|
-
|
|
147
|
-
config.CANDLE.EDGECOLOR.bull_rise = '#089981'
|
|
148
|
-
config.CANDLE.EDGECOLOR.bull_fall = '#089981'
|
|
149
|
-
config.CANDLE.EDGECOLOR.bear_fall = '#f23645'
|
|
150
|
-
config.CANDLE.EDGECOLOR.bear_rise = '#f23645'
|
|
151
|
-
config.CANDLE.EDGECOLOR.doji = 'w'
|
|
152
|
-
|
|
153
|
-
# volume
|
|
154
|
-
config.VOLUME.FACECOLOR.rise = '#2A8076'
|
|
155
|
-
config.VOLUME.FACECOLOR.fall = '#BE4F58'
|
|
156
|
-
config.VOLUME.FACECOLOR.doji = '#82828A'
|
|
157
|
-
|
|
158
|
-
config.VOLUME.EDGECOLOR.rise = '#2A8076'
|
|
159
|
-
config.VOLUME.EDGECOLOR.fall = '#BE4F58'
|
|
160
|
-
config.VOLUME.EDGECOLOR.doji = '#82828A'
|
|
161
|
-
|
|
162
|
-
# ma
|
|
163
|
-
config.MA.color_default = 'w'
|
|
164
|
-
config.MA.color_list = ['#FFB300', '#03A9F4', '#AB47BC', '#8BC34A', '#EF5350']
|
|
165
|
-
|
|
166
|
-
# text
|
|
167
|
-
config.CURSOR.TEXT.BBOX.facecolor = '#3d3d3d'
|
|
168
|
-
config.CURSOR.TEXT.BBOX.edgecolor = '#ffffff'
|
|
169
|
-
config.CURSOR.TEXT.color = '#ffffff'
|
|
170
|
-
|
|
171
|
-
# box
|
|
172
|
-
config.CURSOR.BOX.edgecolor = 'w'
|
|
173
|
-
|
|
174
|
-
# line
|
|
175
|
-
config.CURSOR.CROSSLINE.edgecolor = '#9c9c9c'
|
|
176
|
-
|
|
177
|
-
# wartermark
|
|
178
|
-
config.FIGURE.WATERMARK.color = 'w'
|
|
179
|
-
|
|
180
|
-
if getattr(config, 'SLIDER', None):
|
|
181
|
-
config.SLIDER.NAVIGATOR.edgecolor = "#00A6FF"
|
|
182
|
-
config.SLIDER.NAVIGATOR.facecolor = '#FFFFFF4D'
|
|
183
|
-
|
|
184
|
-
return config
|
|
185
|
-
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
from ._cursor import Chart as _CC
|
|
3
|
-
from ._slider import Chart as _SC
|
|
1
|
+
import pandas as pd
|
|
4
2
|
|
|
3
|
+
from .cursor import Chart as _CC, BaseChart as _OC
|
|
4
|
+
from .slider import Chart as _SC
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class OnlyChart(_OC):
|
|
8
8
|
r"""
|
|
9
9
|
You can see the guidance document:
|
|
10
10
|
Korean: https://white.seolpyo.com/entry/147/?from=package
|
|
11
|
-
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
11
|
+
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
12
12
|
|
|
13
13
|
Quick Start:
|
|
14
14
|
```
|
|
@@ -20,37 +20,37 @@ class OnlyChart(_OC):
|
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Class Variables:
|
|
23
|
-
watermark: watermark text.
|
|
24
|
-
|
|
25
23
|
df: stock data DataFrame.
|
|
26
24
|
key_date: date column key. default 'date'
|
|
27
25
|
key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
|
|
28
26
|
key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
|
|
29
|
+
|
|
31
30
|
limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
|
|
32
31
|
limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
fraction: if True and number has a fractional part, display it as a fraction.
|
|
32
|
+
|
|
33
|
+
watermark: watermark text.
|
|
36
34
|
"""
|
|
35
|
+
df: pd.DataFrame
|
|
36
|
+
|
|
37
37
|
key_date = 'date'
|
|
38
38
|
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
39
39
|
key_volume = 'volume'
|
|
40
40
|
|
|
41
41
|
candle_on_ma = True
|
|
42
|
+
|
|
43
|
+
watermark = 'seolpyo mplchart'
|
|
44
|
+
|
|
42
45
|
limit_candle = 400
|
|
43
46
|
limit_wick = 2_000
|
|
44
|
-
limit_volume = 200
|
|
45
|
-
limit_ma = None
|
|
46
|
-
watermark = 'seolpyo mplchart'
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
class CursorChart(_CC):
|
|
50
50
|
r"""
|
|
51
51
|
You can see the guidance document:
|
|
52
52
|
Korean: https://white.seolpyo.com/entry/147/?from=package
|
|
53
|
-
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
53
|
+
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
54
54
|
|
|
55
55
|
Quick Start:
|
|
56
56
|
```
|
|
@@ -62,38 +62,39 @@ class CursorChart(_CC):
|
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
Class Variables:
|
|
65
|
-
watermark: watermark
|
|
66
|
-
|
|
67
65
|
df: stock data DataFrame.
|
|
68
66
|
key_date: date column key. default 'date'
|
|
69
67
|
key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
|
|
70
68
|
key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
|
|
71
|
+
fraction: if True and number has a fractional part, display it as a fraction.
|
|
72
|
+
|
|
73
73
|
limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
|
|
74
74
|
limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
fraction: if True and number has a fractional part, display it as a fraction.
|
|
75
|
+
|
|
76
|
+
watermark: watermark
|
|
78
77
|
"""
|
|
78
|
+
df: pd.DataFrame
|
|
79
|
+
|
|
79
80
|
key_date = 'date'
|
|
80
81
|
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
81
82
|
key_volume = 'volume'
|
|
82
83
|
|
|
83
84
|
candle_on_ma = True
|
|
85
|
+
fraction = False
|
|
86
|
+
|
|
87
|
+
watermark = 'seolpyo mplchart'
|
|
88
|
+
|
|
84
89
|
limit_candle = 400
|
|
85
90
|
limit_wick = 2_000
|
|
86
|
-
limit_volume = 200
|
|
87
|
-
limit_ma = None
|
|
88
|
-
watermark = 'seolpyo mplchart'
|
|
89
|
-
fraction = False
|
|
90
91
|
|
|
91
92
|
|
|
92
93
|
class SliderChart(_SC):
|
|
93
94
|
r"""
|
|
94
95
|
You can see the guidance document:
|
|
95
96
|
Korean: https://white.seolpyo.com/entry/147/?from=package
|
|
96
|
-
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
97
|
+
English: https://github.com/white-seolpyo/seolpyo-mplchart
|
|
97
98
|
|
|
98
99
|
Quick Start:
|
|
99
100
|
```
|
|
@@ -112,26 +113,33 @@ class SliderChart(_SC):
|
|
|
112
113
|
key_open, key_high, key_low, key_close: price column key. default ('open', 'high', 'low', 'close')
|
|
113
114
|
key_volume: volume column key. default 'volume'. If ```if config.VOLUME.EDGECOLOR.volume``` is ```False```, the volume chart is not drawn.
|
|
114
115
|
|
|
115
|
-
|
|
116
|
+
candle_on_ma: if True: draw candle above ma. else: draw candle below ma lines.
|
|
117
|
+
fraction: if True and number has a fractional part, display it as a fraction.
|
|
118
|
+
slider_top: set slider position. if True: slider top. else: bottom
|
|
119
|
+
|
|
116
120
|
limit_candle: If (`the number of candles to draw < limit_candle or not limit_candle`): draw candles, else: draw wicks.
|
|
117
121
|
limit_wick: If (`the number of candles to draw < limit_wick or not limit_wick`): draw wicks, else: draw line.
|
|
118
122
|
limit_volume: If (`the number of candles to draw < limit_volume or not limit_volume`): draw volumebar, else: draw wicks.
|
|
119
123
|
limit_ma: If (`the number of candles to draw < limit_ma or not limit_ma`): draw ma lines, else: don't draw ma lines.
|
|
120
|
-
|
|
121
|
-
slider_top: set slider position. if True: slider top. else: bottom
|
|
124
|
+
|
|
122
125
|
min_distance: the minimum number of candles displayed on the screen.
|
|
123
126
|
"""
|
|
127
|
+
df: pd.DataFrame
|
|
128
|
+
|
|
124
129
|
key_date = 'date'
|
|
125
130
|
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
126
131
|
key_volume = 'volume'
|
|
127
132
|
|
|
128
133
|
candle_on_ma = True
|
|
134
|
+
fraction = False
|
|
135
|
+
slider_top = True
|
|
136
|
+
|
|
137
|
+
watermark = 'seolpyo mplchart'
|
|
138
|
+
|
|
129
139
|
limit_candle = 400
|
|
130
140
|
limit_wick = 2_000
|
|
131
141
|
limit_volume = 200
|
|
132
142
|
limit_ma = 8_000
|
|
133
|
-
|
|
134
|
-
fraction = False
|
|
135
|
-
slider_top = True
|
|
143
|
+
|
|
136
144
|
min_distance = 5
|
|
137
145
|
|
|
@@ -0,0 +1,111 @@
|
|
|
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 ..._config import DEFAULTCONFIG, ConfigData
|
|
9
|
+
|
|
10
|
+
from .a_canvas import CanvasMixin, Figure
|
|
11
|
+
from .b_artist import ArtistMixin
|
|
12
|
+
from .c_draw import DrawMixin
|
|
13
|
+
from .d_segment import SegmentMixin
|
|
14
|
+
from .e_axis import AxisMixin
|
|
15
|
+
from .f_background import BackgroundMixin
|
|
16
|
+
from .g_event import EventMixin
|
|
17
|
+
from .h_data import DataMixin
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Chart(
|
|
21
|
+
CanvasMixin,
|
|
22
|
+
ArtistMixin,
|
|
23
|
+
DrawMixin,
|
|
24
|
+
SegmentMixin,
|
|
25
|
+
AxisMixin,
|
|
26
|
+
BackgroundMixin,
|
|
27
|
+
EventMixin,
|
|
28
|
+
DataMixin,
|
|
29
|
+
):
|
|
30
|
+
limit_candle = 400
|
|
31
|
+
limit_wick = 2_000
|
|
32
|
+
candle_on_ma = True
|
|
33
|
+
|
|
34
|
+
key_date = 'date'
|
|
35
|
+
key_open, key_high, key_low, key_close = ('open', 'high', 'low', 'close')
|
|
36
|
+
key_volume = 'volume'
|
|
37
|
+
|
|
38
|
+
index_list: list[int] = []
|
|
39
|
+
|
|
40
|
+
df: pd.DataFrame
|
|
41
|
+
|
|
42
|
+
CONFIG: ConfigData
|
|
43
|
+
|
|
44
|
+
figure: Figure
|
|
45
|
+
ax_legend: Axes
|
|
46
|
+
ax_price: Axes
|
|
47
|
+
ax_volume: Axes
|
|
48
|
+
|
|
49
|
+
artist_watermark: Text
|
|
50
|
+
collection_candle: LineCollection
|
|
51
|
+
collection_volume: LineCollection
|
|
52
|
+
collection_ma: LineCollection
|
|
53
|
+
|
|
54
|
+
###
|
|
55
|
+
|
|
56
|
+
segment_volume: np.ndarray
|
|
57
|
+
segment_volume_wick: np.ndarray
|
|
58
|
+
facecolor_volume: np.ndarray
|
|
59
|
+
edgecolor_volume: np.ndarray
|
|
60
|
+
|
|
61
|
+
segment_candle: np.ndarray
|
|
62
|
+
segment_candle_wick: np.ndarray
|
|
63
|
+
segment_priceline: np.ndarray
|
|
64
|
+
facecolor_candle: np.ndarray
|
|
65
|
+
edgecolor_candle: np.ndarray
|
|
66
|
+
|
|
67
|
+
segment_ma: np.ndarray
|
|
68
|
+
edgecolor_ma: np.ndarray
|
|
69
|
+
|
|
70
|
+
price_ymin: int
|
|
71
|
+
price_ymax: int
|
|
72
|
+
volume_ymax: int
|
|
73
|
+
|
|
74
|
+
chart_price_ymax: float
|
|
75
|
+
chart_volume_ymax: float
|
|
76
|
+
|
|
77
|
+
vxmin: int
|
|
78
|
+
vxmax: int
|
|
79
|
+
|
|
80
|
+
###
|
|
81
|
+
|
|
82
|
+
_visible_ma: set[int] = set()
|
|
83
|
+
|
|
84
|
+
_background = None
|
|
85
|
+
_background_background = None
|
|
86
|
+
_creating_background = False
|
|
87
|
+
|
|
88
|
+
def __init__(self, config=DEFAULTCONFIG):
|
|
89
|
+
self.CONFIG = config
|
|
90
|
+
super().__init__()
|
|
91
|
+
|
|
92
|
+
self.add_artists()
|
|
93
|
+
self.connect_events()
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
def refresh(self):
|
|
97
|
+
self.set_artists()
|
|
98
|
+
|
|
99
|
+
self.set_candle_segments()
|
|
100
|
+
self.set_color_segments()
|
|
101
|
+
ma_alpha = self.collection_ma.get_alpha()
|
|
102
|
+
# print(f'{ma_alpha=}')
|
|
103
|
+
if ma_alpha is not None:
|
|
104
|
+
self.collection_ma.set_alpha([1 for _ in ma_alpha])
|
|
105
|
+
|
|
106
|
+
self.axis(self.vxmin, xmax=self.vxmax-1)
|
|
107
|
+
|
|
108
|
+
self.set_canvas()
|
|
109
|
+
self.figure.canvas.draw()
|
|
110
|
+
return
|
|
111
|
+
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import matplotlib.style as mplstyle
|
|
3
|
+
from matplotlib.artist import Artist
|
|
4
|
+
from matplotlib.axes import Axes
|
|
5
|
+
from matplotlib.backends.backend_agg import FigureCanvasAgg, RendererAgg
|
|
6
|
+
from matplotlib.backend_bases import FigureManagerBase
|
|
7
|
+
from matplotlib.figure import Figure as Fig
|
|
8
|
+
from matplotlib.text import Text
|
|
9
|
+
|
|
10
|
+
try: plt.switch_backend('TkAgg')
|
|
11
|
+
except: pass
|
|
12
|
+
|
|
13
|
+
# 한글 깨짐 문제 방지
|
|
14
|
+
try: plt.rcParams['font.family'] ='Malgun Gothic'
|
|
15
|
+
except: pass
|
|
16
|
+
|
|
17
|
+
from ..._config import DEFAULTCONFIG, ConfigData
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
mplstyle.use('fast')
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Canvas(FigureCanvasAgg):
|
|
24
|
+
manager: FigureManagerBase
|
|
25
|
+
renderer = RendererAgg
|
|
26
|
+
|
|
27
|
+
class Figure(Fig):
|
|
28
|
+
canvas: Canvas
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Base:
|
|
32
|
+
CONFIG: ConfigData
|
|
33
|
+
figure: Figure = None
|
|
34
|
+
|
|
35
|
+
def add_axes(self):
|
|
36
|
+
if not self.figure:
|
|
37
|
+
self.figure, *_ = plt.subplots(
|
|
38
|
+
3, # row 수
|
|
39
|
+
figsize=self.CONFIG.FIGURE.figsize, # 기본 크기
|
|
40
|
+
height_ratios=(
|
|
41
|
+
1,
|
|
42
|
+
self.CONFIG.FIGURE.RATIO.price,
|
|
43
|
+
self.CONFIG.FIGURE.RATIO.volume,
|
|
44
|
+
) # row 크기 비율
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.ax_legend, self.ax_price, self.ax_volume = self.figure.axes
|
|
48
|
+
self.ax_legend.set_label('legend ax')
|
|
49
|
+
self.ax_price.set_label('price ax')
|
|
50
|
+
self.ax_volume.set_label('volume ax')
|
|
51
|
+
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class FigureMixin(Base):
|
|
56
|
+
key_volume: str
|
|
57
|
+
ax_legend: Axes
|
|
58
|
+
|
|
59
|
+
def _set_figure_ratios(self):
|
|
60
|
+
gs = self.figure.axes[0].get_subplotspec().get_gridspec()
|
|
61
|
+
|
|
62
|
+
ratio_volume = self.CONFIG.FIGURE.RATIO.volume
|
|
63
|
+
if not self.key_volume:
|
|
64
|
+
ratio_volume = 0
|
|
65
|
+
|
|
66
|
+
legend = self.ax_legend.get_legend()
|
|
67
|
+
if not legend:
|
|
68
|
+
ratios = [
|
|
69
|
+
0,
|
|
70
|
+
self.CONFIG.FIGURE.RATIO.price, ratio_volume
|
|
71
|
+
]
|
|
72
|
+
else:
|
|
73
|
+
fig_heihgt = self.figure.get_figheight()
|
|
74
|
+
fig_px = fig_heihgt * (1-self.CONFIG.FIGURE.ADJUST.hspace*2) * self.figure.dpi
|
|
75
|
+
# print(f'{(fig_heihgt, fig_px)=}')
|
|
76
|
+
|
|
77
|
+
# Legend에 Axes 높이 맞추기
|
|
78
|
+
bbox = legend.get_window_extent().transformed(self.figure.transFigure.inverted())
|
|
79
|
+
ax_pos = self.ax_legend.get_position()
|
|
80
|
+
self.ax_legend.set_position([ax_pos.x0, ax_pos.y0, ax_pos.width, bbox.height])
|
|
81
|
+
|
|
82
|
+
legend_height = bbox.height
|
|
83
|
+
legend_px = legend.get_window_extent().height
|
|
84
|
+
# print(f'{(legend_height, legend_px)=}')
|
|
85
|
+
|
|
86
|
+
chart_px = fig_px - legend_height
|
|
87
|
+
chart_ratio = self.CONFIG.FIGURE.RATIO.price + ratio_volume
|
|
88
|
+
div_chart = chart_px / chart_ratio
|
|
89
|
+
price_px = div_chart * self.CONFIG.FIGURE.RATIO.price
|
|
90
|
+
volume_px = div_chart * ratio_volume
|
|
91
|
+
|
|
92
|
+
# 차트 비율 변경
|
|
93
|
+
ratios = [
|
|
94
|
+
legend_px * 1.2,
|
|
95
|
+
price_px, volume_px
|
|
96
|
+
]
|
|
97
|
+
# print(f'{ratios=}')
|
|
98
|
+
gs.set_height_ratios(ratios)
|
|
99
|
+
|
|
100
|
+
self.figure.tight_layout()
|
|
101
|
+
|
|
102
|
+
# 플롯간 간격 설정(Configure subplots)
|
|
103
|
+
self.figure.subplots_adjust(**self.CONFIG.FIGURE.ADJUST.__dict__)
|
|
104
|
+
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
def _set_figure(self):
|
|
108
|
+
self.figure.canvas.manager.set_window_title('Seolpyo MPLChart')
|
|
109
|
+
|
|
110
|
+
# print(f'{self.CONFIG.FIGURE.RATIO.volume=}')
|
|
111
|
+
# print(f'{gs.get_height_ratios()=}')
|
|
112
|
+
|
|
113
|
+
self._set_figure_ratios()
|
|
114
|
+
|
|
115
|
+
self.figure.set_facecolor(self.CONFIG.FIGURE.facecolor)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class AxesMixin(FigureMixin):
|
|
120
|
+
def _set_axes(self):
|
|
121
|
+
# ax 요소 animated 처리
|
|
122
|
+
for ax in self.figure.axes:
|
|
123
|
+
ax.patch.set_animated(True)
|
|
124
|
+
|
|
125
|
+
# ax 경계선
|
|
126
|
+
for spine in ax.spines.values():
|
|
127
|
+
spine.set_animated(True)
|
|
128
|
+
|
|
129
|
+
for axis in (ax.xaxis, ax.yaxis):
|
|
130
|
+
axis.set_animated(True)
|
|
131
|
+
axis.label.set_animated(True)
|
|
132
|
+
for tick in axis.get_major_ticks() + axis.get_minor_ticks():
|
|
133
|
+
artists: list[Artist] = [
|
|
134
|
+
tick.tick1line, tick.tick2line,
|
|
135
|
+
tick.gridline,
|
|
136
|
+
tick.label1, tick.label2
|
|
137
|
+
]
|
|
138
|
+
for artist in artists:
|
|
139
|
+
if artist is not None:
|
|
140
|
+
artist.set_animated(True)
|
|
141
|
+
|
|
142
|
+
# y ticklabel foramt 설정
|
|
143
|
+
self.ax_price.yaxis.set_major_formatter(
|
|
144
|
+
lambda x, _: self.CONFIG.UNIT.func(
|
|
145
|
+
x,
|
|
146
|
+
word=self.CONFIG.UNIT.price,
|
|
147
|
+
digit=self.CONFIG.UNIT.digit+(0 if self.CONFIG.UNIT.digit % 1 else 2)
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
self.ax_volume.yaxis.set_major_formatter(
|
|
151
|
+
lambda x, _: self.CONFIG.UNIT.func(
|
|
152
|
+
x,
|
|
153
|
+
word=self.CONFIG.UNIT.volume,
|
|
154
|
+
digit=self.CONFIG.UNIT.digit_volume
|
|
155
|
+
)
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if not self.key_volume:
|
|
159
|
+
# tick 그리지 않기
|
|
160
|
+
self.ax_volume.set_yticklabels([])
|
|
161
|
+
self.ax_volume.set_yticks([])
|
|
162
|
+
|
|
163
|
+
# 공통 설정
|
|
164
|
+
for ax in (self.ax_price, self.ax_volume):
|
|
165
|
+
ax.xaxis.set_animated(True)
|
|
166
|
+
ax.yaxis.set_animated(True)
|
|
167
|
+
|
|
168
|
+
# x tick 외부 눈금 표시하지 않기
|
|
169
|
+
ax.xaxis.set_ticks_position('none')
|
|
170
|
+
# x tick label 제거
|
|
171
|
+
ax.set_xticklabels([])
|
|
172
|
+
# y tick 위치를 우측으로 이동
|
|
173
|
+
ax.tick_params(left=False, right=True, labelleft=False, labelright=True)
|
|
174
|
+
|
|
175
|
+
# 차트 영역 배경 색상
|
|
176
|
+
ax.set_facecolor(self.CONFIG.AX.facecolor)
|
|
177
|
+
|
|
178
|
+
# Axes 외곽선 색 변경(틱 색과 일치)
|
|
179
|
+
for i in ['top', 'bottom', 'left', 'right']:
|
|
180
|
+
ax.spines[i].set_color(self.CONFIG.AX.TICK.edgecolor)
|
|
181
|
+
# 틱 색상
|
|
182
|
+
ax.tick_params('both', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
183
|
+
# 틱 라벨 색상
|
|
184
|
+
ticklabels: list[Text] = ax.get_xticklabels() + ax.get_yticklabels()
|
|
185
|
+
for ticklabel in ticklabels:
|
|
186
|
+
ticklabel.set_color(self.CONFIG.AX.TICK.fontcolor)
|
|
187
|
+
|
|
188
|
+
# Axes grid(구분선, 격자) 그리기
|
|
189
|
+
# 어째서인지 grid의 zorder 값을 선언해도 1.6을 값으로 한다.
|
|
190
|
+
ax.grid(**self.CONFIG.AX.GRID.__dict__)
|
|
191
|
+
|
|
192
|
+
# 거래량 차트의 x tick 외부 눈금 표시하기
|
|
193
|
+
self.ax_volume.xaxis.set_ticks_position('bottom')
|
|
194
|
+
# major tick mark 길이를 0으로 만들어 튀어나오지 않게 하기
|
|
195
|
+
self.ax_volume.tick_params('x', which='major', length=0)
|
|
196
|
+
# minor tick mark 색상 변경
|
|
197
|
+
self.ax_volume.tick_params('x', which='minor', colors=self.CONFIG.AX.TICK.edgecolor)
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class LegendMixin(AxesMixin):
|
|
202
|
+
def _set_axes_legend(self):
|
|
203
|
+
# 이평선 라벨 axis 그리지 않기
|
|
204
|
+
self.ax_legend.set_axis_off()
|
|
205
|
+
self.ax_legend.xaxis.set_animated(True)
|
|
206
|
+
self.ax_legend.yaxis.set_animated(True)
|
|
207
|
+
self.ax_legend.set_animated(True)
|
|
208
|
+
|
|
209
|
+
# 이평선 라벨 Axes 배경색
|
|
210
|
+
legends = self.ax_legend.get_legend()
|
|
211
|
+
if legends:
|
|
212
|
+
legends.get_frame().set_facecolor(self.CONFIG.AX.facecolor)
|
|
213
|
+
|
|
214
|
+
# 이평선 라벨 Axes 테두리색
|
|
215
|
+
legends = self.ax_legend.get_legend()
|
|
216
|
+
if legends:
|
|
217
|
+
legends.get_frame().set_edgecolor(self.CONFIG.AX.TICK.edgecolor)
|
|
218
|
+
|
|
219
|
+
# 이평선 라벨 폰트 색상
|
|
220
|
+
fontcolor = self.CONFIG.AX.TICK.fontcolor
|
|
221
|
+
legends = self.ax_legend.get_legend()
|
|
222
|
+
if legends:
|
|
223
|
+
legend_labels: list[Text] = legends.texts
|
|
224
|
+
for i in legend_labels:
|
|
225
|
+
i.set_color(fontcolor)
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class CanvasMixin(LegendMixin):
|
|
230
|
+
figure: Figure
|
|
231
|
+
ax_legend: Axes
|
|
232
|
+
ax_price: Axes
|
|
233
|
+
ax_volume: Axes
|
|
234
|
+
|
|
235
|
+
def __init__(self, config=DEFAULTCONFIG):
|
|
236
|
+
# 기본 툴바 비활성화
|
|
237
|
+
plt.rcParams['toolbar'] = 'None'
|
|
238
|
+
# plt.rcParams['figure.dpi'] = 600
|
|
239
|
+
|
|
240
|
+
self.CONFIG = config
|
|
241
|
+
self.add_axes()
|
|
242
|
+
self.set_canvas()
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
def set_canvas(self):
|
|
246
|
+
self._set_axes()
|
|
247
|
+
self._set_axes_legend()
|
|
248
|
+
self._set_figure()
|
|
249
|
+
return
|
|
250
|
+
|