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.

Files changed (20) hide show
  1. {seolpyo_mplchart-0.0.41/seolpyo_mplchart.egg-info → seolpyo_mplchart-0.1.1}/PKG-INFO +3 -3
  2. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/pyproject.toml +2 -2
  3. seolpyo_mplchart-0.1.1/seolpyo_mplchart/__init__.py +120 -0
  4. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/cursor.py +85 -75
  5. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/draw.py +81 -75
  6. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/slider.py +80 -127
  7. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1/seolpyo_mplchart.egg-info}/PKG-INFO +3 -3
  8. seolpyo_mplchart-0.0.41/seolpyo_mplchart/__init__.py +0 -49
  9. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/MANIFEST.in +0 -0
  10. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/README.md +0 -0
  11. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/base.py +0 -0
  12. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/data/apple.txt +0 -0
  13. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/data/samsung.txt +0 -0
  14. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/test.py +0 -0
  15. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart/utils.py +0 -0
  16. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/SOURCES.txt +0 -0
  17. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/dependency_links.txt +0 -0
  18. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/requires.txt +0 -0
  19. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/seolpyo_mplchart.egg-info/top_level.txt +0 -0
  20. {seolpyo_mplchart-0.0.41 → seolpyo_mplchart-0.1.1}/setup.cfg +0 -0
@@ -1,7 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: seolpyo-mplchart
3
- Version: 0.0.41
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.0.41"
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.\nIncludes navigator, slider, navigation, and text information display functions"
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: pd.DataFrame):
68
- for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
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: raise Exception(f'you can not set "self.{i}" value in {_set_key}.\nself.{i}={v!r}')
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
- super()._generate_data(df)
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
- high = self.df[self.high][intx] * 1.02
176
- low = self.df[self.low][intx] * 0.98
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 set_data(self, df):
218
- super().set_data(df)
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
- data = data[:100]
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=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
- def _generate_data(self, df: pd.DataFrame):
52
- for i in ['date', 'Open', 'high', 'low', 'close', 'volume']:
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)): raise TypeError(f'Data column type must be "float64" or "int64" or "float32" or "int32".(excluding "date" column)\ndf[{k!r}].dtype={dtype!r}')
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
- for i in self.list_ma: df[f'ma{i}'] = df[self.close].rolling(i).mean()
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
- self.df.loc[:, ['candlesegment']] = self.df[['x', 'left', 'right', self.high, self.low, 'top', 'bottom']].agg(self._get_candlesegment, axis=1)
154
- self.df.loc[:, ['volumesegment']] = self.df[['vleft', 'vright', self.volume]].agg(self._get_volumesegment, axis=1)
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
- segments = list(reversed(self._masegment.values()))
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(segments)
166
+ self.macollection.set_segments(maseg)
163
167
  self.macollection.set_edgecolor(colors)
164
168
 
165
169
  # 슬라이더 선형차트
166
- segments.append(self.df[['x', self.close]].apply(tuple, axis=1).tolist())
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(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
- self._masegment[i] = self.df[['x', f'ma{i}']].apply(tuple, axis=1).tolist()
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._generate_data(df)
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
- # 주가 xlim
319
- self.ax_price.set_xlim(self.xmin, self.xmax)
320
- # 거래량 xlim
321
- self.ax_volume.set_xlim(self.xmin, self.xmax)
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 set_data(self, df):
43
- super().set_data(df)
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 with_nav: self.canvas.restore_region(self.background_with_nav)
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.ax_slider.xaxis.draw(self.canvas.renderer)
120
- self.ax_slider.yaxis.draw(self.canvas.renderer)
121
- self.slidercollection.draw(self.canvas.renderer)
122
+ renderer = self.canvas.renderer
122
123
 
123
- super()._copy_bbox()
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 set_data(self, df):
151
- super().set_data(df)
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
- if 0 < self._slider_ymin: ysub = self._slider_ymax
155
- else: ysub = self._slider_ymax - self._slider_ymin
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
- if e.inaxes is not self.ax_slider: return
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.background = None
169
- self._draw()
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.background = None
272
- self._draw()
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), empty=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 = 2_000
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 set_data(self, df):
349
- super().set_data(df)
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]].agg(get_wickline, axis=1)
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
- volmax = self.df[self.volume].max()
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]].agg(get_volumeline, axis=1)
350
+ volseg = self.df.loc[:, ['x', 'zero', 'x', self.volume]].values
360
351
  else:
361
- n, step = (1, 1 / self.limit_volume)
362
- for _ in range(self.limit_volume):
363
- n -= step
364
- volmin = volmax * n
365
- length = self.df.loc[volmin < self.df[self.volume]].__len__()
366
- if self.limit_volume < length: break
367
-
368
- volseg = self.df.loc[volmin < self.df[self.volume], ['x', self.volume]].agg(get_volumeline, axis=1)
369
- self.blitvolume.set_segments(volseg)
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 self._draw()
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
- if self.simpler: self.blitcandle.draw(renderer)
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.candlecollection.draw(renderer)
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.candlecollection.draw(renderer)
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._draw()
444
- return self._restore_region()
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), empty=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.create_background()
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
1
+ Metadata-Version: 2.2
2
2
  Name: seolpyo-mplchart
3
- Version: 0.0.41
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')