seolpyo-mplchart 0.0.41__tar.gz → 0.1.1__tar.gz
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-0.0.41/seolpyo_mplchart.egg-info → seolpyo_mplchart-0.1.1}/PKG-INFO +3 -3
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/pyproject.toml +2 -2
- seolpyo_mplchart-0.1.1/seolpyo_mplchart/__init__.py +120 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/cursor.py +85 -75
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/draw.py +81 -75
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/slider.py +80 -127
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1/seolpyo_mplchart.egg-info}/PKG-INFO +3 -3
- seolpyo_mplchart-0.0.41/seolpyo_mplchart/__init__.py +0 -49
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/MANIFEST.in +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/README.md +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/base.py +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/data/apple.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/data/samsung.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/test.py +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/utils.py +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/SOURCES.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/dependency_links.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/requires.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/top_level.txt +0 -0
- {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/setup.cfg +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: seolpyo-mplchart
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Fast candlestick chart using Python.
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
|
|
5
5
|
Author-email: white-seolpyo <white-seolpyo@naver.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
Project-URL: Homepage, https://white.seolpyo.com/
|
|
@@ -8,13 +8,13 @@ build-backend = "setuptools.build_meta"
|
|
|
8
8
|
|
|
9
9
|
[project]
|
|
10
10
|
name = "seolpyo-mplchart"
|
|
11
|
-
version = "0.
|
|
11
|
+
version = "0.1.1"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"matplotlib >= 3.7.0",
|
|
14
14
|
"pandas >= 2.0.0",
|
|
15
15
|
]
|
|
16
16
|
license = {text = "MIT License"}
|
|
17
|
-
description = "Fast candlestick chart using Python
|
|
17
|
+
description = "Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions"
|
|
18
18
|
readme = "README.md"
|
|
19
19
|
requires-python = ">= 3.11"
|
|
20
20
|
authors = [
|
|
@@ -0,0 +1,120 @@
|
|
|
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
|
+
from matplotlib.figure import Figure
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from .slider import Chart as CM
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
'pd',
|
|
22
|
+
'plt',
|
|
23
|
+
|
|
24
|
+
'Chart',
|
|
25
|
+
|
|
26
|
+
'sample',
|
|
27
|
+
'show',
|
|
28
|
+
'close',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Chart(CM):
|
|
33
|
+
r"""
|
|
34
|
+
You can see the guidance document:
|
|
35
|
+
Korean: https://white.seolpyo.com/entry/147/
|
|
36
|
+
English: https://white.seolpyo.com/entry/148/
|
|
37
|
+
|
|
38
|
+
Variables:
|
|
39
|
+
unit_price, unit_volume: unit for price and volume. default ('원', '주').
|
|
40
|
+
|
|
41
|
+
figsize: figure size if you use plt.show(). default (12, 6).
|
|
42
|
+
ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume: Axes ratio. default (3, 2, 18, 5).
|
|
43
|
+
adjust: figure adjust. default dict(top=0.95, bottom=0.05, left=0.01, right=0.93, wspace=0, hspace=0).
|
|
44
|
+
slider_top: ax_slider is located at the top or bottom. default True.
|
|
45
|
+
color_background: color of background. default '#fafafa'.
|
|
46
|
+
color_grid: color of grid. default '#d0d0d0'.
|
|
47
|
+
|
|
48
|
+
df: stock data.
|
|
49
|
+
date: date column key. default 'date'
|
|
50
|
+
Open, high, low, close: price column key. default ('open', 'high', 'low', 'close')
|
|
51
|
+
volume: volume column key. default 'volume'
|
|
52
|
+
|
|
53
|
+
label_ma: moving average legend label format. default '{}일선'
|
|
54
|
+
list_ma: Decide how many days to draw the moving average line. default (5, 20, 60, 120, 240)
|
|
55
|
+
list_macolor: Color the moving average line. If the number of colors is greater than the moving average line, black is applied. default ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
|
|
56
|
+
|
|
57
|
+
candle_on_ma: Decide whether to draw candles on the moving average line. default True
|
|
58
|
+
color_sliderline: Color of closing price line in ax_slider. default 'k'
|
|
59
|
+
color_navigatorline: Color of left and right dividing lines in selected area. default '#1e78ff'
|
|
60
|
+
color_navigator: Color of unselected area. default 'k'
|
|
61
|
+
|
|
62
|
+
color_up: The color of the candle. When the closing price is greater than the opening price. default '#fe3032'
|
|
63
|
+
color_down: The color of the candle. When the opening price is greater than the opening price. default '#0095ff'
|
|
64
|
+
color_flat: The color of the candle. WWhen the closing price is the same as the opening price. default 'k'
|
|
65
|
+
color_up_down: The color of the candle. If the closing price is greater than the opening price, but is lower than the previous day's closing price. default 'w'
|
|
66
|
+
color_down_up: The color of the candle. If the opening price is greater than the closing price, but is higher than the closing price of the previous day. default 'w'
|
|
67
|
+
colors_volume: The color of the volume bar. default '#1f77b4'
|
|
68
|
+
|
|
69
|
+
lineKwargs: Options applied to horizontal and vertical lines drawn along the mouse position. default dict(edgecolor='k', linewidth=1, linestyle='-')
|
|
70
|
+
textboxKwargs: Options that apply to the information text box. dufault dict(boxstyle='round', facecolor='w')
|
|
71
|
+
|
|
72
|
+
fraction: Decide whether to express information as a fraction. default False
|
|
73
|
+
candleformat: Candle information text format. default '{}\n\n종가: {}\n등락률: {}\n대비: {}\n시가: {}({})\n고가: {}({})\n저가: {}({})\n거래량: {}({})'
|
|
74
|
+
volumeformat: Volume information text format. default '{}\n\n거래량 : {}\n거래량증가율: {}'
|
|
75
|
+
digit_price, digit_volume: Number of decimal places expressed in informational text. default (0, 0)
|
|
76
|
+
|
|
77
|
+
min_distance: Minimum number of candles that can be selected with the slider. default 30
|
|
78
|
+
simpler: Decide whether to display candles simply when moving the chart. default False
|
|
79
|
+
limit_volume: Maximum number of volume bars drawn when moving the chart. default 2_000
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
_name = {'samsung', 'apple'}
|
|
85
|
+
def sample(name: Literal['samsung', 'apple']='samsung'):
|
|
86
|
+
if name not in _name:
|
|
87
|
+
print('name should be either samsung or apple.')
|
|
88
|
+
return
|
|
89
|
+
file = Path(__file__).parent / f'data/{name}.txt'
|
|
90
|
+
with open(file, 'r', encoding='utf-8') as txt:
|
|
91
|
+
data = json.load(txt)
|
|
92
|
+
data = data
|
|
93
|
+
df = pd.DataFrame(data)
|
|
94
|
+
|
|
95
|
+
c = Chart()
|
|
96
|
+
if name == 'apple':
|
|
97
|
+
c.unit_price = '$'
|
|
98
|
+
c.unit_volume = ' vol'
|
|
99
|
+
c.digit_price = 3
|
|
100
|
+
c.label_ma = 'ma{}'
|
|
101
|
+
c.candleformat = '{}\n\nend: {}\nrate: {}\ncompare: {}\nopen: {}({})\nhigh: {}({})\nlow: {}({})\nvolume: {}({})'
|
|
102
|
+
c.volumeformat = '{}\n\nvolume: {}\nvolume rate: {}'
|
|
103
|
+
c.set_data(df)
|
|
104
|
+
show()
|
|
105
|
+
close()
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def show():
|
|
110
|
+
return plt.show()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def close(fig: int|str|Figure|None='all'):
|
|
114
|
+
return plt.close(fig)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == '__main__':
|
|
118
|
+
sample('apple')
|
|
119
|
+
|
|
120
|
+
|
|
@@ -11,9 +11,6 @@ from .utils import float_to_str
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Mixin:
|
|
14
|
-
def create_background(self):
|
|
15
|
-
"This function works befor canvas.copy_from_bbox()."
|
|
16
|
-
return
|
|
17
14
|
def on_draw(self, e):
|
|
18
15
|
"This function works if draw event active."
|
|
19
16
|
return
|
|
@@ -64,19 +61,57 @@ class CollectionMixin(DrawMixin):
|
|
|
64
61
|
_set_key = {'rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume',}
|
|
65
62
|
|
|
66
63
|
class DataMixin(CollectionMixin):
|
|
67
|
-
def _generate_data(self, df
|
|
68
|
-
for i in
|
|
64
|
+
def _generate_data(self, df, sort_df=True, calc_ma=True, calc_info=True):
|
|
65
|
+
for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
|
|
69
66
|
v = getattr(self, i)
|
|
70
|
-
if v in _set_key:
|
|
67
|
+
if v in _set_key:
|
|
68
|
+
raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={v!r}')
|
|
69
|
+
|
|
70
|
+
super()._generate_data(df, sort_df, calc_ma)
|
|
71
|
+
df = self.df
|
|
72
|
+
|
|
73
|
+
if not calc_info:
|
|
74
|
+
keys = set(df.keys())
|
|
75
|
+
for i in ('rate', 'compare', 'rate_open', 'rate_high', 'rate_low', 'rate_volume'):
|
|
76
|
+
if i not in keys:
|
|
77
|
+
raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
|
|
78
|
+
else:
|
|
79
|
+
df['rate'] = ((df[self.close] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
80
|
+
df['compare'] = (df[self.close] - df[self.close].shift(1)).fillna(0)
|
|
81
|
+
df['rate_open'] = ((df[self.Open] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
82
|
+
df['rate_high'] = ((df[self.high] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
83
|
+
df['rate_low'] = ((df[self.low] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
84
|
+
df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
85
|
+
|
|
86
|
+
self.df = df
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
|
|
90
|
+
# 주가, 거래량 텍스트 x 위치
|
|
91
|
+
x_distance = (vmax - vmin) / 30
|
|
92
|
+
self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
|
|
93
|
+
self.text_price.set_x(self.v0)
|
|
94
|
+
self.text_volume.set_x(self.v0)
|
|
95
|
+
|
|
96
|
+
self.vmin, self.vmax = (vmin, vmax)
|
|
97
|
+
self.vmiddle = vmax - int((vmax - vmin) / 2)
|
|
71
98
|
|
|
72
|
-
|
|
99
|
+
psub = pmax - pmin
|
|
100
|
+
self.min_psub = psub / 12
|
|
101
|
+
|
|
102
|
+
# 주가 날짜 텍스트 y 위치
|
|
103
|
+
y = (psub) / 20 + pmin
|
|
104
|
+
self.text_date_price.set_y(y)
|
|
105
|
+
# 주가 정보 y 위치
|
|
106
|
+
y = pmax - (psub) / 20
|
|
107
|
+
self.text_price_info.set_y(y)
|
|
108
|
+
|
|
109
|
+
# 거래량 날짜 텍스트 y 위치
|
|
110
|
+
y = volmax * 0.85
|
|
111
|
+
self.text_date_volume.set_y(y)
|
|
112
|
+
# 거래량 정보 y 위치
|
|
113
|
+
self.text_volume_info.set_y(y)
|
|
73
114
|
|
|
74
|
-
df['rate'] = ((df[self.close] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
75
|
-
df['compare'] = (df[self.close] - df[self.close].shift(1)).fillna(0)
|
|
76
|
-
df['rate_open'] = ((df[self.Open] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
77
|
-
df['rate_high'] = ((df[self.high] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
78
|
-
df['rate_low'] = ((df[self.low] - df[self.close].shift(1)) / df[self.close] * 100).__round__(2).fillna(0)
|
|
79
|
-
df['rate_volume'] = ((df[self.volume] - df[self.volume].shift(1)) / df[self.volume].shift(1) * 100).__round__(2).fillna(0)
|
|
80
115
|
return
|
|
81
116
|
|
|
82
117
|
|
|
@@ -95,8 +130,10 @@ class LineMixin(DataMixin):
|
|
|
95
130
|
self.canvas.blit()
|
|
96
131
|
return
|
|
97
132
|
|
|
98
|
-
def set_data(self, df):
|
|
99
|
-
super().set_data(df)
|
|
133
|
+
def set_data(self, df, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
|
|
134
|
+
return super().set_data(df, sort_df, calc_ma, change_lim, calc_info=calc_info, *args, **kwargs)
|
|
135
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
|
|
136
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info=calc_info, *args, **kwargs)
|
|
100
137
|
|
|
101
138
|
self.vmin, self.vmax = (self.xmin, self.xmax)
|
|
102
139
|
return
|
|
@@ -172,8 +209,12 @@ class LineMixin(DataMixin):
|
|
|
172
209
|
if self.in_index:
|
|
173
210
|
intx = self.intx
|
|
174
211
|
|
|
175
|
-
|
|
176
|
-
|
|
212
|
+
h = self.df[self.high][intx]
|
|
213
|
+
l = self.df[self.low][intx]
|
|
214
|
+
sub = (h - l) / 2
|
|
215
|
+
if sub < self.min_psub: sub = self.min_psub
|
|
216
|
+
high = h + sub
|
|
217
|
+
low = l - sub
|
|
177
218
|
if high < y or y < low: self._in_candle = False
|
|
178
219
|
else:
|
|
179
220
|
self._in_candle = True
|
|
@@ -210,12 +251,12 @@ class LineMixin(DataMixin):
|
|
|
210
251
|
|
|
211
252
|
class InfoMixin(LineMixin):
|
|
212
253
|
fraction = False
|
|
213
|
-
candleformat = '{}\n\n종가: {}\n등락률: {}\n대비: {}\n시가: {}({})\n고가: {}({})\n저가: {}({})\n거래량: {}({})'
|
|
214
|
-
volumeformat = '{}\n\n거래량 : {}\n거래량증가율: {}'
|
|
254
|
+
candleformat = '{dt}\n\n종가: {close}\n등락률: {rate}\n대비: {compare}\n시가: {open}({rate_open})\n고가: {high}({rate_high})\n저가: {low}({rate_low})\n거래량: {volume}({rate_volume})'
|
|
255
|
+
volumeformat = '{dt}\n\n거래량 : {volume}\n거래량증가율: {rate_volume}'
|
|
215
256
|
digit_price, digit_volume = (0, 0)
|
|
216
257
|
|
|
217
|
-
def
|
|
218
|
-
super().
|
|
258
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True, *args, **kwargs):
|
|
259
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info, *args, **kwargs)
|
|
219
260
|
|
|
220
261
|
# 슬라이더 날짜 텍스트 y 위치
|
|
221
262
|
y = self._slider_ymax - (self._slider_ymax - self._slider_ymin) / 6
|
|
@@ -227,31 +268,6 @@ class InfoMixin(LineMixin):
|
|
|
227
268
|
|
|
228
269
|
return
|
|
229
270
|
|
|
230
|
-
def set_text_coordante(self, vmin, vmax, pmin, pmax, volmax):
|
|
231
|
-
# 주가, 거래량 텍스트 x 위치
|
|
232
|
-
x_distance = (vmax - vmin) / 30
|
|
233
|
-
self.v0, self.v1 = (vmin + x_distance, vmax - x_distance)
|
|
234
|
-
self.text_price.set_x(self.v0)
|
|
235
|
-
self.text_volume.set_x(self.v0)
|
|
236
|
-
|
|
237
|
-
self.vmin, self.vmax = (vmin, vmax)
|
|
238
|
-
self.vmiddle = vmax - int((vmax - vmin) / 2)
|
|
239
|
-
|
|
240
|
-
# 주가 날짜 텍스트 y 위치
|
|
241
|
-
y = (pmax - pmin) / 20 + pmin
|
|
242
|
-
self.text_date_price.set_y(y)
|
|
243
|
-
# 주가 정보 y 위치
|
|
244
|
-
y = pmax - (pmax - pmin) / 20
|
|
245
|
-
self.text_price_info.set_y(y)
|
|
246
|
-
|
|
247
|
-
# 거래량 날짜 텍스트 y 위치
|
|
248
|
-
y = volmax * 0.85
|
|
249
|
-
self.text_date_volume.set_y(y)
|
|
250
|
-
# 거래량 정보 y 위치
|
|
251
|
-
self.text_volume_info.set_y(y)
|
|
252
|
-
|
|
253
|
-
return
|
|
254
|
-
|
|
255
271
|
def _slider_move_action(self, e):
|
|
256
272
|
super()._slider_move_action(e)
|
|
257
273
|
|
|
@@ -350,35 +366,35 @@ class InfoMixin(LineMixin):
|
|
|
350
366
|
else: l = float_to_str(ld[0])
|
|
351
367
|
|
|
352
368
|
text = self.candleformat.format(
|
|
353
|
-
dt,
|
|
354
|
-
f'{c:>{self._length_text}}{self.unit_price}',
|
|
355
|
-
f'{r:>{self._length_text}}%',
|
|
356
|
-
f'{com:>{self._length_text}}{self.unit_price}',
|
|
357
|
-
f'{o:>{self._length_text}}{self.unit_price}', f'{Or:+06,.2f}%',
|
|
358
|
-
f'{h:>{self._length_text}}{self.unit_price}', f'{hr:+06,.2f}%',
|
|
359
|
-
f'{l:>{self._length_text}}{self.unit_price}', f'{lr:+06,.2f}%',
|
|
360
|
-
f'{v:>{self._length_text}}{self.unit_volume}', f'{vr:+06,.2f}%',
|
|
369
|
+
dt=dt,
|
|
370
|
+
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
371
|
+
rate=f'{r:>{self._length_text}}%',
|
|
372
|
+
compare=f'{com:>{self._length_text}}{self.unit_price}',
|
|
373
|
+
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
374
|
+
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
375
|
+
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
376
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr:+06,.2f}%',
|
|
361
377
|
)
|
|
362
378
|
else:
|
|
363
379
|
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))
|
|
364
380
|
com = float_to_str(compare, self.digit_price, plus=True)
|
|
365
381
|
|
|
366
382
|
text = self.candleformat.format(
|
|
367
|
-
dt,
|
|
368
|
-
f'{c:>{self._length_text}}{self.unit_price}',
|
|
369
|
-
f'{r:>{self._length_text}}%',
|
|
370
|
-
f'{com:>{self._length_text}}{self.unit_price}',
|
|
371
|
-
f'{o:>{self._length_text}}{self.unit_price}', f'{Or:+06,.2f}%',
|
|
372
|
-
f'{h:>{self._length_text}}{self.unit_price}', f'{hr:+06,.2f}%',
|
|
373
|
-
f'{l:>{self._length_text}}{self.unit_price}', f'{lr:+06,.2f}%',
|
|
374
|
-
f'{v:>{self._length_text}}{self.unit_volume}', f'{vr:+06,.2f}%',
|
|
383
|
+
dt=dt,
|
|
384
|
+
close=f'{c:>{self._length_text}}{self.unit_price}',
|
|
385
|
+
rate=f'{r:>{self._length_text}}%',
|
|
386
|
+
compare=f'{com:>{self._length_text}}{self.unit_price}',
|
|
387
|
+
open=f'{o:>{self._length_text}}{self.unit_price}', rate_open=f'{Or:+06,.2f}%',
|
|
388
|
+
high=f'{h:>{self._length_text}}{self.unit_price}', rate_high=f'{hr:+06,.2f}%',
|
|
389
|
+
low=f'{l:>{self._length_text}}{self.unit_price}', rate_low=f'{lr:+06,.2f}%',
|
|
390
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}', rate_volume=f'{vr:+06,.2f}%',
|
|
375
391
|
)
|
|
376
392
|
else:
|
|
377
393
|
vrate = f'{vr:+06,.2f}'
|
|
378
394
|
text = self.volumeformat.format(
|
|
379
|
-
dt,
|
|
380
|
-
f'{v:>{self._length_text}}{self.unit_volume}',
|
|
381
|
-
f'{vrate:>{self._length_text}}%',
|
|
395
|
+
dt=dt,
|
|
396
|
+
volume=f'{v:>{self._length_text}}{self.unit_volume}',
|
|
397
|
+
rate_volume=f'{vrate:>{self._length_text}}%',
|
|
382
398
|
)
|
|
383
399
|
return text
|
|
384
400
|
|
|
@@ -388,10 +404,6 @@ class CursorMixin(InfoMixin):
|
|
|
388
404
|
|
|
389
405
|
|
|
390
406
|
class Chart(CursorMixin, CM, Mixin):
|
|
391
|
-
def _generate_data(self, df):
|
|
392
|
-
super()._generate_data(df)
|
|
393
|
-
return self.generate_data(df)
|
|
394
|
-
|
|
395
407
|
def _on_draw(self, e):
|
|
396
408
|
super()._on_draw(e)
|
|
397
409
|
return self.on_draw(e)
|
|
@@ -400,10 +412,6 @@ class Chart(CursorMixin, CM, Mixin):
|
|
|
400
412
|
self.on_pick(e)
|
|
401
413
|
return super()._on_pick(e)
|
|
402
414
|
|
|
403
|
-
def _draw_artist(self):
|
|
404
|
-
super()._draw_artist()
|
|
405
|
-
return self.create_background()
|
|
406
|
-
|
|
407
415
|
def _blit(self):
|
|
408
416
|
super()._blit()
|
|
409
417
|
return self.on_blit()
|
|
@@ -424,14 +432,16 @@ if __name__ == '__main__':
|
|
|
424
432
|
file = Path(__file__).parent / 'data/apple.txt'
|
|
425
433
|
with open(file, 'r', encoding='utf-8') as txt:
|
|
426
434
|
data = json.load(txt)
|
|
427
|
-
|
|
435
|
+
n = 2600
|
|
436
|
+
data = data[n:n+100]
|
|
428
437
|
df = pd.DataFrame(data)
|
|
438
|
+
print(f'{df.keys()=}')
|
|
429
439
|
|
|
430
440
|
t = time()
|
|
431
441
|
c = CursorMixin()
|
|
432
442
|
c.unit_price = '$'
|
|
433
443
|
# c.fraction = True
|
|
434
|
-
c.set_data(df
|
|
444
|
+
c.set_data(df[['date', 'open', 'high', 'low', 'close', 'volume']])
|
|
435
445
|
t2 = time() - t
|
|
436
446
|
print(f'{t2=}')
|
|
437
447
|
plt.show()
|
|
@@ -9,10 +9,6 @@ from .base import Base
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class Mixin:
|
|
12
|
-
def generate_data(self, df):
|
|
13
|
-
"This function works after data generate process is done."
|
|
14
|
-
return
|
|
15
|
-
|
|
16
12
|
def on_blit(self):
|
|
17
13
|
"This function works after cavas.blit()."
|
|
18
14
|
return
|
|
@@ -27,7 +23,7 @@ class Mixin:
|
|
|
27
23
|
return
|
|
28
24
|
|
|
29
25
|
|
|
30
|
-
_set_key = {'x', 'left', 'right', 'top', 'bottom',}
|
|
26
|
+
_set_key = {'zero', 'x', 'left', 'right', 'top', 'bottom',}
|
|
31
27
|
|
|
32
28
|
class DataMixin(Base):
|
|
33
29
|
df: pd.DataFrame
|
|
@@ -41,30 +37,40 @@ class DataMixin(Base):
|
|
|
41
37
|
# https://matplotlib.org/stable/gallery/color/named_colors.html
|
|
42
38
|
list_macolor = ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
|
|
43
39
|
|
|
44
|
-
color_up = '#fe3032'
|
|
45
|
-
color_down = '#0095ff'
|
|
40
|
+
color_up, color_down = ('#fe3032', '#0095ff')
|
|
46
41
|
color_flat = 'k'
|
|
47
|
-
color_up_down = 'w'
|
|
48
|
-
color_down_up = 'w'
|
|
42
|
+
color_up_down, color_down_up = ('w', 'w')
|
|
49
43
|
colors_volume = '#1f77b4'
|
|
50
44
|
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
candlewidth_half, volumewidth_half = (0.3, 0.36)
|
|
46
|
+
|
|
47
|
+
def _generate_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, **_):
|
|
48
|
+
for i in ('date', 'Open', 'high', 'low', 'close', 'volume'):
|
|
53
49
|
k: str = getattr(self, i)
|
|
54
50
|
if k in _set_key: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={k!r}')
|
|
55
51
|
if i != 'date':
|
|
56
52
|
dtype = df[k].dtype
|
|
57
|
-
if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)):
|
|
53
|
+
if not isinstance(dtype, (np.dtypes.Float64DType, np.dtypes.Int64DType, np.dtypes.Float32DType, np.dtypes.Int32DType)):
|
|
54
|
+
raise TypeError(f'column dtype must be one of "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
# DataFrame 정렬
|
|
57
|
+
if sort_df:
|
|
58
|
+
df = df.sort_values([self.date]).reset_index()
|
|
59
|
+
|
|
60
|
+
if not self.list_ma: self.list_ma = tuple()
|
|
61
|
+
if calc_ma:
|
|
62
|
+
for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
|
|
63
|
+
else:
|
|
64
|
+
keys = set(df.keys())
|
|
65
|
+
for i in self.list_ma:
|
|
66
|
+
if f'ma{i}' not in keys:
|
|
67
|
+
raise Exception(f'"ma{i}" column not in DataFrame.\nadd column or set calc_ma=True.')
|
|
60
68
|
|
|
61
|
-
candlewidth_half = 0.3
|
|
62
|
-
volumewidth_half = 0.36
|
|
63
69
|
df['x'] = df.index + 0.5
|
|
64
|
-
df['left'] = df['x'] - candlewidth_half
|
|
65
|
-
df['right'] = df['x'] + candlewidth_half
|
|
66
|
-
df['vleft'] = df['x'] - volumewidth_half
|
|
67
|
-
df['vright'] = df['x'] + volumewidth_half
|
|
70
|
+
df['left'] = df['x'] - self.candlewidth_half
|
|
71
|
+
df['right'] = df['x'] + self.candlewidth_half
|
|
72
|
+
df['vleft'] = df['x'] - self.volumewidth_half
|
|
73
|
+
df['vright'] = df['x'] + self.volumewidth_half
|
|
68
74
|
|
|
69
75
|
df['top'] = np.where(df[self.Open] <= df[self.close], df[self.close], df[self.Open])
|
|
70
76
|
df['top'] = np.where(df[self.close] < df[self.Open], df[self.Open], df[self.close])
|
|
@@ -72,7 +78,7 @@ class DataMixin(Base):
|
|
|
72
78
|
df['bottom'] = np.where(df[self.close] < df[self.Open], df[self.close], df[self.Open])
|
|
73
79
|
|
|
74
80
|
# 양봉
|
|
75
|
-
df.loc[:, ['facecolor', 'edgecolor']] = (self.color_up, self.color_up)
|
|
81
|
+
df.loc[:, ['zero', 'facecolor', 'edgecolor']] = (0, self.color_up, self.color_up)
|
|
76
82
|
if self.color_up != self.color_down:
|
|
77
83
|
# 음봉
|
|
78
84
|
df.loc[df[self.close] < df[self.Open], ['facecolor', 'edgecolor']] = (self.color_down, self.color_down)
|
|
@@ -89,7 +95,6 @@ class DataMixin(Base):
|
|
|
89
95
|
self.df = df
|
|
90
96
|
return
|
|
91
97
|
|
|
92
|
-
|
|
93
98
|
class CollectionMixin(DataMixin):
|
|
94
99
|
color_sliderline = 'k'
|
|
95
100
|
|
|
@@ -123,57 +128,55 @@ class CollectionMixin(DataMixin):
|
|
|
123
128
|
|
|
124
129
|
return
|
|
125
130
|
|
|
126
|
-
def _get_candlesegment(self, s: pd.Series):
|
|
127
|
-
v = s.values
|
|
128
|
-
segment = (
|
|
129
|
-
(v[0], v[3]), # 심지 상단
|
|
130
|
-
(v[0], v[5]), # 몸통 상단
|
|
131
|
-
(v[1], v[5]), # 몸통 상단 좌측
|
|
132
|
-
(v[1], v[6]), # 몸통 하단 좌측
|
|
133
|
-
(v[0], v[6]), # 몸통 하단
|
|
134
|
-
(v[0], v[4]), # 심지 하단
|
|
135
|
-
(v[0], v[6]), # 몸통 하단
|
|
136
|
-
(v[2], v[6]), # 몸통 하단 우측
|
|
137
|
-
(v[2], v[5]), # 몸통 상단 우측
|
|
138
|
-
(v[0], v[5]), # 몸통 상단
|
|
139
|
-
)
|
|
140
|
-
return segment
|
|
141
|
-
|
|
142
|
-
def _get_volumesegment(self, s: pd.Series):
|
|
143
|
-
v = s.values
|
|
144
|
-
segment = (
|
|
145
|
-
(v[0], 0), # 몸통 하단 좌측
|
|
146
|
-
(v[0], v[2]), # 몸통 상단 좌측
|
|
147
|
-
(v[1], v[2]), # 몸통 상단 우측
|
|
148
|
-
(v[1], 0), # 몸통 하단 우측
|
|
149
|
-
)
|
|
150
|
-
return segment
|
|
151
|
-
|
|
152
131
|
def _set_collection(self):
|
|
153
|
-
|
|
154
|
-
|
|
132
|
+
candleseg = self.df[[
|
|
133
|
+
'x', self.high,
|
|
134
|
+
'x', 'top',
|
|
135
|
+
'left', 'top',
|
|
136
|
+
'left', 'bottom',
|
|
137
|
+
'x', 'bottom',
|
|
138
|
+
'x', self.low,
|
|
139
|
+
'x', 'bottom',
|
|
140
|
+
'right', 'bottom',
|
|
141
|
+
'right', 'top',
|
|
142
|
+
'x', 'top',
|
|
143
|
+
]].values
|
|
144
|
+
candleseg = candleseg.reshape(candleseg.shape[0], 10, 2)
|
|
145
|
+
|
|
146
|
+
self.candlecollection.set_segments(candleseg)
|
|
147
|
+
self.candlecollection.set_facecolor(self.df['facecolor'].values)
|
|
148
|
+
self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
|
|
149
|
+
|
|
150
|
+
volseg = self.df[[
|
|
151
|
+
'left', 'zero',
|
|
152
|
+
'left', self.volume,
|
|
153
|
+
'right', self.volume,
|
|
154
|
+
'right', 'zero',
|
|
155
|
+
]].values
|
|
156
|
+
volseg = volseg.reshape(volseg.shape[0], 4, 2)
|
|
157
|
+
|
|
158
|
+
self.volumecollection.set_segments(volseg)
|
|
155
159
|
|
|
156
160
|
self._set_macollection()
|
|
157
161
|
|
|
158
162
|
# 가격이동평균선
|
|
159
|
-
|
|
163
|
+
maseg = reversed(self._masegment.values())
|
|
160
164
|
colors, widths = ([], [])
|
|
161
165
|
for i in reversed(self._macolors.values()): (colors.append(i), widths.append(1))
|
|
162
|
-
self.macollection.set_segments(
|
|
166
|
+
self.macollection.set_segments(maseg)
|
|
163
167
|
self.macollection.set_edgecolor(colors)
|
|
164
168
|
|
|
165
169
|
# 슬라이더 선형차트
|
|
166
|
-
|
|
170
|
+
keys = []
|
|
171
|
+
for i in reversed(self.list_ma):
|
|
172
|
+
keys.append('x')
|
|
173
|
+
keys.append(f'ma{i}')
|
|
174
|
+
sliderseg = self.df[keys + ['x', self.close]].values
|
|
175
|
+
sliderseg = sliderseg.reshape(sliderseg.shape[0], self.list_ma.__len__()+1, 2).swapaxes(0, 1)
|
|
167
176
|
(colors.append(self.color_sliderline), widths.append(1.8))
|
|
168
|
-
self.slidercollection.set_segments(
|
|
177
|
+
self.slidercollection.set_segments(sliderseg)
|
|
169
178
|
self.slidercollection.set_edgecolor(colors)
|
|
170
179
|
self.slidercollection.set_linewidth(widths)
|
|
171
|
-
|
|
172
|
-
self.candlecollection.set_segments(self.df['candlesegment'])
|
|
173
|
-
self.candlecollection.set_facecolor(self.df['facecolor'].values)
|
|
174
|
-
self.candlecollection.set_edgecolor(self.df['edgecolor'].values)
|
|
175
|
-
|
|
176
|
-
self.volumecollection.set_segments(self.df['volumesegment'])
|
|
177
180
|
return
|
|
178
181
|
|
|
179
182
|
def _set_macollection(self):
|
|
@@ -188,7 +191,10 @@ class CollectionMixin(DataMixin):
|
|
|
188
191
|
try: c = self.list_macolor[n]
|
|
189
192
|
except: c = self.color_sliderline
|
|
190
193
|
self._macolors[i] = c
|
|
191
|
-
|
|
194
|
+
# seg = self.df['x', f'ma{i}'].values
|
|
195
|
+
seg = self.df.loc[self.df[f'ma{i}'] != np.nan, ['x', f'ma{i}']].values
|
|
196
|
+
# print(f'{seg[:5]=}')
|
|
197
|
+
self._masegment[i] = seg
|
|
192
198
|
|
|
193
199
|
handles.append(Line2D([0, 1], [0, 1], color=c, linewidth=5, label=i))
|
|
194
200
|
labels.append(self.label_ma.format(i))
|
|
@@ -300,25 +306,29 @@ class BackgroundMixin(CollectionMixin):
|
|
|
300
306
|
self.canvas.renderer.restore_region(self.background)
|
|
301
307
|
return
|
|
302
308
|
|
|
303
|
-
|
|
304
309
|
class DrawMixin(BackgroundMixin):
|
|
305
|
-
def set_data(self, df: pd.DataFrame):
|
|
306
|
-
self.
|
|
310
|
+
def set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
|
|
311
|
+
self._set_data(df, sort_df, calc_ma, change_lim, **kwargs)
|
|
312
|
+
return self.df
|
|
313
|
+
|
|
314
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, *_, **kwargs):
|
|
315
|
+
self._generate_data(df, sort_df, calc_ma, **kwargs)
|
|
307
316
|
self._set_collection()
|
|
308
|
-
self._draw_collection()
|
|
317
|
+
self._draw_collection(change_lim)
|
|
309
318
|
return
|
|
310
319
|
|
|
311
|
-
def _draw_collection(self):
|
|
320
|
+
def _draw_collection(self, change_lim=True):
|
|
312
321
|
xmax = self.df['x'].values[-1] + 1
|
|
313
322
|
|
|
314
323
|
xspace = xmax / 40
|
|
315
324
|
self.xmin, self.xmax = (-xspace, xmax+xspace)
|
|
316
325
|
# 슬라이더 xlim
|
|
317
326
|
self.ax_slider.set_xlim(self.xmin, self.xmax)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
327
|
+
if change_lim:
|
|
328
|
+
# 주가 xlim
|
|
329
|
+
self.ax_price.set_xlim(self.xmin, self.xmax)
|
|
330
|
+
# 거래량 xlim
|
|
331
|
+
self.ax_volume.set_xlim(self.xmin, self.xmax)
|
|
322
332
|
|
|
323
333
|
ymin, ymax = (self.df[self.low].min(), self.df[self.high].max())
|
|
324
334
|
ysub = (ymax - ymin) / 15
|
|
@@ -329,19 +339,15 @@ class DrawMixin(BackgroundMixin):
|
|
|
329
339
|
|
|
330
340
|
# 주가 ylim
|
|
331
341
|
self._price_ymin, self._price_ymax = (ymin-ysub, ymax+ysub)
|
|
332
|
-
self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
|
|
342
|
+
if change_lim: self.ax_price.set_ylim(self._price_ymin, self._price_ymax)
|
|
333
343
|
|
|
334
344
|
# 거래량 ylim
|
|
335
345
|
self._vol_ymax = self.df[self.volume].max() * 1.2
|
|
336
|
-
self.ax_volume.set_ylim(0, self._vol_ymax)
|
|
346
|
+
if change_lim: self.ax_volume.set_ylim(0, self._vol_ymax)
|
|
337
347
|
return
|
|
338
348
|
|
|
339
349
|
|
|
340
350
|
class Chart(DrawMixin, Mixin):
|
|
341
|
-
def _generate_data(self, df):
|
|
342
|
-
super()._generate_data(df)
|
|
343
|
-
return self.generate_data(self.df)
|
|
344
|
-
|
|
345
351
|
def _on_draw(self, e):
|
|
346
352
|
super()._on_draw(e)
|
|
347
353
|
return self.on_draw(e)
|
|
@@ -21,6 +21,10 @@ class Mixin(CursorMixin):
|
|
|
21
21
|
def on_release(self, e):
|
|
22
22
|
"This function works if mouse button release event active."
|
|
23
23
|
return
|
|
24
|
+
def draw_artist(self):
|
|
25
|
+
"This function works before canvas.blit()."
|
|
26
|
+
return
|
|
27
|
+
|
|
24
28
|
|
|
25
29
|
class NavgatorMixin(Mixin):
|
|
26
30
|
min_distance = 30
|
|
@@ -39,8 +43,8 @@ class NavgatorMixin(Mixin):
|
|
|
39
43
|
self.ax_slider.add_artist(self.navigator)
|
|
40
44
|
return
|
|
41
45
|
|
|
42
|
-
def
|
|
43
|
-
super().
|
|
46
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
47
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
|
|
44
48
|
|
|
45
49
|
# 네비게이터 라인 선택 영역
|
|
46
50
|
xsub = self.xmax - self.xmin
|
|
@@ -96,9 +100,6 @@ class NavgatorMixin(Mixin):
|
|
|
96
100
|
|
|
97
101
|
if self._navcoordinate[0] == self._navcoordinate[1]:
|
|
98
102
|
self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
|
|
99
|
-
|
|
100
|
-
self.background = None
|
|
101
|
-
self._draw()
|
|
102
103
|
return
|
|
103
104
|
|
|
104
105
|
|
|
@@ -108,19 +109,27 @@ class BackgroundMixin(NavgatorMixin):
|
|
|
108
109
|
self._restore_region()
|
|
109
110
|
return
|
|
110
111
|
|
|
111
|
-
def _restore_region(self, with_nav=True):
|
|
112
|
+
def _restore_region(self, with_nav=True, empty=False, empty_with_nav=False):
|
|
112
113
|
if not self.background: self._create_background()
|
|
113
114
|
|
|
114
|
-
if
|
|
115
|
+
if empty: self.canvas.restore_region(self.background_empty)
|
|
116
|
+
elif empty_with_nav: self.canvas.restore_region(self.background_empty_with_nav)
|
|
117
|
+
elif with_nav: self.canvas.restore_region(self.background_with_nav)
|
|
115
118
|
else: self.canvas.renderer.restore_region(self.background)
|
|
116
119
|
return
|
|
117
120
|
|
|
118
121
|
def _copy_bbox(self):
|
|
119
|
-
self.
|
|
120
|
-
self.ax_slider.yaxis.draw(self.canvas.renderer)
|
|
121
|
-
self.slidercollection.draw(self.canvas.renderer)
|
|
122
|
+
renderer = self.canvas.renderer
|
|
122
123
|
|
|
123
|
-
|
|
124
|
+
self.background_empty = renderer.copy_from_bbox(self.fig.bbox)
|
|
125
|
+
|
|
126
|
+
self.ax_slider.xaxis.draw(renderer)
|
|
127
|
+
self.ax_slider.yaxis.draw(renderer)
|
|
128
|
+
self.slidercollection.draw(renderer)
|
|
129
|
+
self.background_empty_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
130
|
+
|
|
131
|
+
self._draw_artist()
|
|
132
|
+
self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
124
133
|
|
|
125
134
|
self.navigator.draw(self.canvas.renderer)
|
|
126
135
|
self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
@@ -147,26 +156,24 @@ class BackgroundMixin(NavgatorMixin):
|
|
|
147
156
|
|
|
148
157
|
|
|
149
158
|
class DrawMixin(BackgroundMixin):
|
|
150
|
-
def
|
|
151
|
-
super().
|
|
159
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
160
|
+
super()._set_data(df, sort_df, calc_ma, change_lim, calc_info)
|
|
152
161
|
|
|
153
162
|
# 네비게이터 높이 설정
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
self._ymiddle = ysub / 2
|
|
163
|
+
ysub = self._slider_ymax - self._slider_ymin
|
|
164
|
+
self._ymiddle = self._slider_ymax - ysub / 2
|
|
157
165
|
self.navigator.set_linewidth((ysub, 5))
|
|
158
166
|
return
|
|
159
167
|
|
|
160
168
|
def _on_release(self, e: MouseEvent):
|
|
161
|
-
|
|
162
|
-
self.is_click, self.is_move = (False, False)
|
|
163
|
-
|
|
164
|
-
if self._navcoordinate[0] == self._navcoordinate[1]:
|
|
165
|
-
self._navcoordinate = (self._navcoordinate[0], self._navcoordinate[1]+self.min_distance)
|
|
169
|
+
super()._on_release(e)
|
|
166
170
|
self._set_navigator(*self._navcoordinate)
|
|
167
171
|
|
|
168
|
-
self.
|
|
169
|
-
self.
|
|
172
|
+
self._restore_region(empty=True)
|
|
173
|
+
self._creating_background = False
|
|
174
|
+
self._create_background()
|
|
175
|
+
self._restore_region()
|
|
176
|
+
self._blit()
|
|
170
177
|
return
|
|
171
178
|
|
|
172
179
|
def _on_move(self, e: MouseEvent):
|
|
@@ -238,27 +245,6 @@ class DrawMixin(BackgroundMixin):
|
|
|
238
245
|
|
|
239
246
|
|
|
240
247
|
class LimMixin(DrawMixin):
|
|
241
|
-
def _restore_region(self, with_nav=True, empty=False):
|
|
242
|
-
if not self.background: self._create_background()
|
|
243
|
-
|
|
244
|
-
if empty: self.canvas.restore_region(self.background_empty)
|
|
245
|
-
elif with_nav: self.canvas.restore_region(self.background_with_nav)
|
|
246
|
-
else: self.canvas.renderer.restore_region(self.background)
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
def _copy_bbox(self):
|
|
250
|
-
self.ax_slider.xaxis.draw(self.canvas.renderer)
|
|
251
|
-
self.ax_slider.yaxis.draw(self.canvas.renderer)
|
|
252
|
-
self.slidercollection.draw(self.canvas.renderer)
|
|
253
|
-
self.background_empty = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
254
|
-
|
|
255
|
-
self._draw_artist()
|
|
256
|
-
self.background = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
257
|
-
|
|
258
|
-
self.navigator.draw(self.canvas.renderer)
|
|
259
|
-
self.background_with_nav = self.canvas.renderer.copy_from_bbox(self.fig.bbox)
|
|
260
|
-
return
|
|
261
|
-
|
|
262
248
|
def _on_release(self, e: MouseEvent):
|
|
263
249
|
if e.inaxes is not self.ax_slider: return
|
|
264
250
|
self.is_click, self.is_move = (False, False)
|
|
@@ -268,12 +254,14 @@ class LimMixin(DrawMixin):
|
|
|
268
254
|
self._set_navigator(*self._navcoordinate)
|
|
269
255
|
self._lim()
|
|
270
256
|
|
|
271
|
-
self.
|
|
272
|
-
self.
|
|
257
|
+
self._restore_region(empty=True)
|
|
258
|
+
self._copy_bbox()
|
|
259
|
+
self._restore_region()
|
|
260
|
+
self._blit()
|
|
273
261
|
return
|
|
274
262
|
|
|
275
263
|
def _on_move(self, e):
|
|
276
|
-
self._restore_region(with_nav=(not self.is_click),
|
|
264
|
+
self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
|
|
277
265
|
|
|
278
266
|
self._on_move_action(e)
|
|
279
267
|
|
|
@@ -329,7 +317,8 @@ class LimMixin(DrawMixin):
|
|
|
329
317
|
|
|
330
318
|
class SimpleMixin(LimMixin):
|
|
331
319
|
simpler = False
|
|
332
|
-
limit_volume =
|
|
320
|
+
limit_volume = 1_500
|
|
321
|
+
default_left, default_right = (180, 10)
|
|
333
322
|
|
|
334
323
|
def __init__(self, *args, **kwargs):
|
|
335
324
|
super().__init__(*args, **kwargs)
|
|
@@ -345,35 +334,34 @@ class SimpleMixin(LimMixin):
|
|
|
345
334
|
self.ax_volume.add_collection(self.blitvolume)
|
|
346
335
|
return
|
|
347
336
|
|
|
348
|
-
def
|
|
349
|
-
super().
|
|
337
|
+
def _set_data(self, df: pd.DataFrame, sort_df=True, calc_ma=True, change_lim=True, calc_info=True):
|
|
338
|
+
super()._set_data(df, sort_df, calc_ma, False, calc_info)
|
|
350
339
|
|
|
351
|
-
seg = self.df[['x', self.high, self.low]].
|
|
340
|
+
seg = self.df[['x', self.high, 'x', self.low]].values
|
|
341
|
+
seg = seg.reshape(seg.shape[0], 2, 2)
|
|
352
342
|
self.blitcandle.set_segments(seg)
|
|
353
343
|
self.blitcandle.set_edgecolor(self.df['edgecolor'])
|
|
354
|
-
self.priceline.set_verts([self.df[['x', self.close]].apply(tuple, axis=1).tolist()])
|
|
355
344
|
|
|
356
|
-
|
|
345
|
+
pseg = self.df[['x', self.close]].values
|
|
346
|
+
self.priceline.set_verts(pseg.reshape(1, *pseg.shape))
|
|
347
|
+
|
|
357
348
|
l = self.df.__len__()
|
|
358
349
|
if l < self.limit_volume:
|
|
359
|
-
volseg = self.df.loc[:, ['x', self.volume]].
|
|
350
|
+
volseg = self.df.loc[:, ['x', 'zero', 'x', self.volume]].values
|
|
360
351
|
else:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
index = self.df.index[-1]
|
|
372
|
-
if index < 120: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
|
|
373
|
-
else: self._navcoordinate = (index-80, index+10)
|
|
352
|
+
v = self.df[['x', 'zero', 'x', self.volume]].sort_values([self.volume], axis=0, ascending=False)
|
|
353
|
+
volseg = v[:self.limit_volume].values
|
|
354
|
+
|
|
355
|
+
self.blitvolume.set_segments(volseg.reshape(volseg.shape[0], 2, 2))
|
|
356
|
+
|
|
357
|
+
if change_lim:
|
|
358
|
+
index = self.df.index[-1]
|
|
359
|
+
if index < self.default_left + self.default_right: self._navcoordinate = (int(self.xmin)-1, int(self.xmax)+1)
|
|
360
|
+
else: self._navcoordinate = (index-self.default_left, index+self.default_right)
|
|
361
|
+
|
|
374
362
|
self._set_navigator(*self._navcoordinate)
|
|
375
363
|
self._lim()
|
|
376
|
-
return
|
|
364
|
+
return
|
|
377
365
|
|
|
378
366
|
def _draw_blit_artist(self):
|
|
379
367
|
renderer = self.canvas.renderer
|
|
@@ -381,12 +369,20 @@ class SimpleMixin(LimMixin):
|
|
|
381
369
|
self.ax_price.xaxis.draw(renderer)
|
|
382
370
|
self.ax_price.yaxis.draw(renderer)
|
|
383
371
|
|
|
384
|
-
|
|
372
|
+
left, right = self._navcoordinate
|
|
373
|
+
Range = right - left
|
|
374
|
+
if self.simpler:
|
|
375
|
+
if Range < 1_000: self.blitcandle.draw(renderer)
|
|
376
|
+
else: self.priceline.draw(renderer)
|
|
385
377
|
elif self.candle_on_ma:
|
|
386
378
|
self.macollection.draw(renderer)
|
|
387
|
-
self.
|
|
379
|
+
if 2_500 < Range: self.priceline.draw(renderer)
|
|
380
|
+
elif 800 < Range or 9_999 < self.xmax: self.blitcandle.draw(renderer)
|
|
381
|
+
else: self.candlecollection.draw(renderer)
|
|
388
382
|
else:
|
|
389
|
-
self.
|
|
383
|
+
if 2_500 < Range: self.priceline.draw(renderer)
|
|
384
|
+
elif 800 < Range or 9_999 < self.xmax: self.blitcandle.draw(renderer)
|
|
385
|
+
else: self.candlecollection.draw(renderer)
|
|
390
386
|
self.macollection.draw(renderer)
|
|
391
387
|
|
|
392
388
|
self.ax_volume.xaxis.draw(renderer)
|
|
@@ -436,12 +432,16 @@ class ClickMixin(SimpleMixin):
|
|
|
436
432
|
if not self.is_click: return
|
|
437
433
|
elif e.inaxes is self.ax_slider: return super()._on_release(e)
|
|
438
434
|
elif not self.in_price and not self.in_volume and not self.is_click_chart: return
|
|
435
|
+
# 차트 click release action
|
|
439
436
|
self.canvas.set_cursor(cursors.POINTER)
|
|
440
437
|
self.is_click, self.is_move = (False, False)
|
|
441
438
|
self.is_click_chart = False
|
|
442
439
|
|
|
443
|
-
self.
|
|
444
|
-
|
|
440
|
+
self._restore_region(empty=True)
|
|
441
|
+
self._copy_bbox()
|
|
442
|
+
self._restore_region()
|
|
443
|
+
self._blit()
|
|
444
|
+
return
|
|
445
445
|
|
|
446
446
|
def _on_chart_click(self, e: MouseEvent):
|
|
447
447
|
self.is_click = True
|
|
@@ -469,7 +469,7 @@ class ClickMixin(SimpleMixin):
|
|
|
469
469
|
return
|
|
470
470
|
|
|
471
471
|
def _on_move(self, e):
|
|
472
|
-
self._restore_region(with_nav=(not self.is_click),
|
|
472
|
+
self._restore_region(with_nav=(not self.is_click), empty_with_nav=self.is_click)
|
|
473
473
|
|
|
474
474
|
self._on_move_action(e)
|
|
475
475
|
|
|
@@ -506,58 +506,6 @@ class SliderMixin(ClickMixin):
|
|
|
506
506
|
|
|
507
507
|
|
|
508
508
|
class Chart(SliderMixin, CM, Mixin):
|
|
509
|
-
r"""
|
|
510
|
-
You can see the guidance document:
|
|
511
|
-
Korean: https://white.seolpyo.com/entry/147/
|
|
512
|
-
English: https://white.seolpyo.com/entry/148/
|
|
513
|
-
|
|
514
|
-
Variables:
|
|
515
|
-
unit_price, unit_volume: unit for price and volume. default ('원', '주').
|
|
516
|
-
|
|
517
|
-
figsize: figure size if you use plt.show(). default (12, 6).
|
|
518
|
-
ratio_ax_slider, ratio_ax_legend, ratio_ax_price, ratio_ax_volume: Axes ratio. default (3, 2, 18, 5).
|
|
519
|
-
adjust: figure adjust. default dict(top=0.95, bottom=0.05, left=0.01, right=0.93, wspace=0, hspace=0).
|
|
520
|
-
slider_top: ax_slider is located at the top or bottom. default True.
|
|
521
|
-
color_background: color of background. default '#fafafa'.
|
|
522
|
-
color_grid: color of grid. default '#d0d0d0'.
|
|
523
|
-
|
|
524
|
-
df: stock data.
|
|
525
|
-
date: date column key. default 'date'
|
|
526
|
-
Open, high, low, close: price column key. default ('open', 'high', 'low', 'close')
|
|
527
|
-
volume: volume column key. default 'volume'
|
|
528
|
-
|
|
529
|
-
label_ma: moving average legend label format. default '{}일선'
|
|
530
|
-
list_ma: Decide how many days to draw the moving average line. default (5, 20, 60, 120, 240)
|
|
531
|
-
list_macolor: Color the moving average line. If the number of colors is greater than the moving average line, black is applied. default ('darkred', 'fuchsia', 'olive', 'orange', 'navy', 'darkmagenta', 'limegreen', 'darkcyan',)
|
|
532
|
-
|
|
533
|
-
candle_on_ma: Decide whether to draw candles on the moving average line. default True
|
|
534
|
-
color_sliderline: Color of closing price line in ax_slider. default 'k'
|
|
535
|
-
color_navigatorline: Color of left and right dividing lines in selected area. default '#1e78ff'
|
|
536
|
-
color_navigator: Color of unselected area. default 'k'
|
|
537
|
-
|
|
538
|
-
color_up: The color of the candle. When the closing price is greater than the opening price. default '#fe3032'
|
|
539
|
-
color_down: The color of the candle. When the opening price is greater than the opening price. default '#0095ff'
|
|
540
|
-
color_flat: The color of the candle. WWhen the closing price is the same as the opening price. default 'k'
|
|
541
|
-
color_up_down: The color of the candle. If the closing price is greater than the opening price, but is lower than the previous day's closing price. default 'w'
|
|
542
|
-
color_down_up: The color of the candle. If the opening price is greater than the closing price, but is higher than the closing price of the previous day. default 'w'
|
|
543
|
-
colors_volume: The color of the volume bar. default '#1f77b4'
|
|
544
|
-
|
|
545
|
-
lineKwargs: Options applied to horizontal and vertical lines drawn along the mouse position. default dict(edgecolor='k', linewidth=1, linestyle='-')
|
|
546
|
-
textboxKwargs: Options that apply to the information text box. dufault dict(boxstyle='round', facecolor='w')
|
|
547
|
-
|
|
548
|
-
fraction: Decide whether to express information as a fraction. default False
|
|
549
|
-
candleformat: Candle information text format. default '{}\n\n종가: {}\n등락률: {}\n대비: {}\n시가: {}({})\n고가: {}({})\n저가: {}({})\n거래량: {}({})'
|
|
550
|
-
volumeformat: Volume information text format. default '{}\n\n거래량 : {}\n거래량증가율: {}'
|
|
551
|
-
digit_price, digit_volume: Number of decimal places expressed in informational text. default (0, 0)
|
|
552
|
-
|
|
553
|
-
min_distance: Minimum number of candles that can be selected with the slider. default 30
|
|
554
|
-
simpler: Decide whether to display candles simply when moving the chart. default False
|
|
555
|
-
limit_volume: Maximum number of volume bars drawn when moving the chart. default 2_000
|
|
556
|
-
"""
|
|
557
|
-
def _generate_data(self, df):
|
|
558
|
-
super()._generate_data(df)
|
|
559
|
-
return self.generate_data(self.df)
|
|
560
|
-
|
|
561
509
|
def _on_draw(self, e):
|
|
562
510
|
super()._on_draw(e)
|
|
563
511
|
return self.on_draw(e)
|
|
@@ -568,7 +516,10 @@ class Chart(SliderMixin, CM, Mixin):
|
|
|
568
516
|
|
|
569
517
|
def _draw_artist(self):
|
|
570
518
|
super()._draw_artist()
|
|
571
|
-
return self.
|
|
519
|
+
return self.draw_artist()
|
|
520
|
+
def _draw_blit_artist(self):
|
|
521
|
+
super()._draw_blit_artist()
|
|
522
|
+
return self.draw_artist()
|
|
572
523
|
|
|
573
524
|
def _blit(self):
|
|
574
525
|
super()._blit()
|
|
@@ -594,8 +545,10 @@ if __name__ == '__main__':
|
|
|
594
545
|
df = pd.DataFrame(data)
|
|
595
546
|
|
|
596
547
|
t = time()
|
|
548
|
+
# c = SimpleMixin()
|
|
597
549
|
c = SliderMixin()
|
|
598
550
|
c.set_data(df)
|
|
599
551
|
t2 = time() - t
|
|
600
552
|
print(f'{t2=}')
|
|
601
553
|
plt.show()
|
|
554
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: seolpyo-mplchart
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Fast candlestick chart using Python.
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Fast candlestick chart using Python. Includes navigator, slider, navigation, and text information display functions
|
|
5
5
|
Author-email: white-seolpyo <white-seolpyo@naver.com>
|
|
6
6
|
License: MIT License
|
|
7
7
|
Project-URL: Homepage, https://white.seolpyo.com/
|
|
@@ -1,49 +0,0 @@
|
|
|
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')
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|