seolpyo-mplchart 1.4.1__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. seolpyo_mplchart/__init__.py +53 -333
  2. seolpyo_mplchart/_chart/__init__.py +145 -0
  3. seolpyo_mplchart/_chart/_base.py +217 -0
  4. seolpyo_mplchart/_chart/_cursor/__init__.py +2 -0
  5. seolpyo_mplchart/_chart/_cursor/_artist.py +217 -0
  6. seolpyo_mplchart/_chart/_cursor/_cursor.py +165 -0
  7. seolpyo_mplchart/_chart/_cursor/_info.py +187 -0
  8. seolpyo_mplchart/_chart/_draw/__init__.py +2 -0
  9. seolpyo_mplchart/_chart/_draw/_artist.py +50 -0
  10. seolpyo_mplchart/_chart/_draw/_data.py +314 -0
  11. seolpyo_mplchart/_chart/_draw/_draw.py +103 -0
  12. seolpyo_mplchart/_chart/_draw/_lim.py +265 -0
  13. seolpyo_mplchart/_chart/_slider/__init__.py +1 -0
  14. seolpyo_mplchart/_chart/_slider/_base.py +268 -0
  15. seolpyo_mplchart/_chart/_slider/_data.py +105 -0
  16. seolpyo_mplchart/_chart/_slider/_mouse.py +176 -0
  17. seolpyo_mplchart/_chart/_slider/_nav.py +204 -0
  18. seolpyo_mplchart/_chart/base/__init__.py +111 -0
  19. seolpyo_mplchart/_chart/base/a_canvas.py +250 -0
  20. seolpyo_mplchart/_chart/base/b_artist.py +143 -0
  21. seolpyo_mplchart/_chart/base/c_draw.py +100 -0
  22. seolpyo_mplchart/_chart/base/d_segment.py +262 -0
  23. seolpyo_mplchart/_chart/base/e_axis.py +267 -0
  24. seolpyo_mplchart/_chart/base/f_background.py +62 -0
  25. seolpyo_mplchart/_chart/base/g_event.py +66 -0
  26. seolpyo_mplchart/_chart/base/h_data.py +138 -0
  27. seolpyo_mplchart/_chart/base/test.py +58 -0
  28. seolpyo_mplchart/_chart/cursor/__init__.py +125 -0
  29. seolpyo_mplchart/_chart/cursor/b_artist.py +130 -0
  30. seolpyo_mplchart/_chart/cursor/c_draw.py +96 -0
  31. seolpyo_mplchart/_chart/cursor/d_segment.py +359 -0
  32. seolpyo_mplchart/_chart/cursor/e_axis.py +65 -0
  33. seolpyo_mplchart/_chart/cursor/g_event.py +233 -0
  34. seolpyo_mplchart/_chart/cursor/h_data.py +61 -0
  35. seolpyo_mplchart/_chart/cursor/test.py +69 -0
  36. seolpyo_mplchart/_chart/slider/__init__.py +169 -0
  37. seolpyo_mplchart/_chart/slider/a_canvas.py +260 -0
  38. seolpyo_mplchart/_chart/slider/b_artist.py +91 -0
  39. seolpyo_mplchart/_chart/slider/c_draw.py +54 -0
  40. seolpyo_mplchart/_chart/slider/d_segment.py +166 -0
  41. seolpyo_mplchart/_chart/slider/e_axis.py +70 -0
  42. seolpyo_mplchart/_chart/slider/f_background.py +37 -0
  43. seolpyo_mplchart/_chart/slider/g_event.py +353 -0
  44. seolpyo_mplchart/_chart/slider/h_data.py +102 -0
  45. seolpyo_mplchart/_chart/slider/test.py +71 -0
  46. seolpyo_mplchart/_chart/test.py +121 -0
  47. seolpyo_mplchart/_config/__init__.py +3 -0
  48. seolpyo_mplchart/_config/ax.py +28 -0
  49. seolpyo_mplchart/_config/candle.py +31 -0
  50. seolpyo_mplchart/_config/config.py +21 -0
  51. seolpyo_mplchart/_config/cursor.py +49 -0
  52. seolpyo_mplchart/_config/figure.py +40 -0
  53. seolpyo_mplchart/_config/format.py +51 -0
  54. seolpyo_mplchart/_config/ma.py +17 -0
  55. seolpyo_mplchart/_config/slider/__init__.py +2 -0
  56. seolpyo_mplchart/_config/slider/config.py +24 -0
  57. seolpyo_mplchart/_config/slider/figure.py +19 -0
  58. seolpyo_mplchart/_config/slider/nav.py +10 -0
  59. seolpyo_mplchart/_config/unit.py +19 -0
  60. seolpyo_mplchart/_config/utils.py +67 -0
  61. seolpyo_mplchart/_config/volume.py +27 -0
  62. seolpyo_mplchart/_cursor.py +27 -25
  63. seolpyo_mplchart/_draw.py +7 -18
  64. seolpyo_mplchart/_slider.py +26 -20
  65. seolpyo_mplchart/_utils/__init__.py +10 -0
  66. seolpyo_mplchart/_utils/nums.py +67 -0
  67. seolpyo_mplchart/_utils/theme/__init__.py +15 -0
  68. seolpyo_mplchart/_utils/theme/dark.py +57 -0
  69. seolpyo_mplchart/_utils/theme/light.py +56 -0
  70. seolpyo_mplchart/_utils/utils.py +28 -0
  71. seolpyo_mplchart/_utils/xl/__init__.py +15 -0
  72. seolpyo_mplchart/_utils/xl/csv.py +46 -0
  73. seolpyo_mplchart/_utils/xl/xlsx.py +49 -0
  74. seolpyo_mplchart/sample/apple.txt +6058 -0
  75. seolpyo_mplchart/sample/samsung.txt +5938 -0
  76. seolpyo_mplchart/test.py +172 -56
  77. seolpyo_mplchart/xl_to_dict.py +47 -0
  78. seolpyo_mplchart-2.1.0.dist-info/METADATA +718 -0
  79. seolpyo_mplchart-2.1.0.dist-info/RECORD +89 -0
  80. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/WHEEL +1 -1
  81. seolpyo_mplchart-1.4.1.dist-info/METADATA +0 -57
  82. seolpyo_mplchart-1.4.1.dist-info/RECORD +0 -17
  83. {seolpyo_mplchart-1.4.1.dist-info → seolpyo_mplchart-2.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,27 @@
1
+
2
+
3
+ class VolumeFaceColorData:
4
+ def __init__(self):
5
+ self.rise: str|tuple[float, float, float, float] = '#F27663'
6
+ self.fall: str|tuple[float, float, float, float] = '#70B5F2'
7
+ self.doji: str|tuple[float, float, float, float] = '#BEBEBE'
8
+
9
+ VOLUMEFACECOLOR = VolumeFaceColorData()
10
+
11
+ class VolumeEdgeColorData:
12
+ def __init__(self):
13
+ self.rise: str|tuple[float, float, float, float] = '#F27663'
14
+ self.fall: str|tuple[float, float, float, float] = '#70B5F2'
15
+ self.doji: str|tuple[float, float, float, float] = '#BEBEBE'
16
+
17
+ VOLUMEEDGECOLOR = VolumeEdgeColorData()
18
+
19
+ class VolumeData:
20
+ def __init__(self):
21
+ self.half_width = 0.36
22
+ self.linewidth = 0.7
23
+ self.FACECOLOR = VOLUMEFACECOLOR
24
+ self.EDGECOLOR = VOLUMEEDGECOLOR
25
+
26
+ VOLUME = VolumeData()
27
+
@@ -105,7 +105,7 @@ class CollectionMixin(BM):
105
105
  _set_key = {
106
106
  'compare', 'rate',
107
107
  'rate_open', 'rate_high', 'rate_low',
108
- 'compare_volume', 'rate_volume',
108
+ 'volume_pre', 'compare_volume', 'rate_volume',
109
109
  'space_box_candle',
110
110
  'bottom_box_candle', 'top_box_candle',
111
111
  'max_box_volume',
@@ -136,13 +136,14 @@ class DataMixin(CollectionMixin):
136
136
  raise Exception(f'"{i}" column not in DataFrame.\nadd column or set calc_info=True.')
137
137
  else:
138
138
  self.df['compare'] = (self.df[self.close] - self.df['close_pre']).fillna(0)
139
- self.df['rate'] = (self.df['compare'] / self.df[self.close] * 100).__round__(2).fillna(0)
140
- self.df['rate_open'] = ((self.df[self.Open] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
141
- self.df['rate_high'] = ((self.df[self.high] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
142
- self.df['rate_low'] = ((self.df[self.low] - self.df['close_pre']) / self.df[self.close] * 100).__round__(2).fillna(0)
139
+ self.df['rate'] = (self.df['compare'] * 100 / self.df['close_pre']).__round__(2).fillna(0)
140
+ self.df['rate_open'] = ((self.df[self.Open] - self.df['close_pre']) * 100 / self.df['close_pre']).__round__(2).fillna(0)
141
+ self.df['rate_high'] = ((self.df[self.high] - self.df['close_pre']) * 100 / self.df['close_pre']).__round__(2).fillna(0)
142
+ self.df['rate_low'] = ((self.df[self.low] - self.df['close_pre']) * 100 / self.df['close_pre']).__round__(2).fillna(0)
143
143
  if self.volume:
144
- self.df['compare_volume'] = (self.df[self.volume] - self.df[self.volume].shift(1)).fillna(0)
145
- self.df['rate_volume'] = (self.df['compare_volume'] / self.df[self.volume].shift(1) * 100).__round__(2).fillna(0)
144
+ self.df['volume_pre'] = self.df[self.volume].shift(1)
145
+ self.df['compare_volume'] = (self.df[self.volume] - self.df['volume_pre']).fillna(0)
146
+ self.df['rate_volume'] = (self.df['compare_volume'] * 100 / self.df['volume_pre']).__round__(2).fillna(0)
146
147
 
147
148
  self.df['space_box_candle'] = (self.df[self.high] - self.df[self.low]) / 5
148
149
  self.df['bottom_box_candle'] = self.df[self.low] - self.df['space_box_candle']
@@ -233,7 +234,7 @@ class CrossLineMixin(EventMixin):
233
234
 
234
235
  if self.in_price_chart or self.in_volume_chart:
235
236
  self._restore_region()
236
- self._draw_crossline(e, self.in_price_chart)
237
+ self._draw_crossline(e)
237
238
  self.figure.canvas.blit()
238
239
  else:
239
240
  if self._erase_crossline():
@@ -248,10 +249,10 @@ class CrossLineMixin(EventMixin):
248
249
  return True
249
250
  return False
250
251
 
251
- def _draw_crossline(self, e: MouseEvent, in_price_chart):
252
+ def _draw_crossline(self, e: MouseEvent):
252
253
  x, y = (e.xdata, e.ydata)
253
254
 
254
- if in_price_chart:
255
+ if self.in_price_chart:
255
256
  self.collection_price_crossline.set_segments([((x, self.price_ymin), (x, self.price_ymax)), ((self.vxmin, y), (self.vxmax, y))])
256
257
  self.collection_volume_crossline.set_segments([((x, 0), (x, self.volume_ymax))])
257
258
  else:
@@ -262,14 +263,14 @@ class CrossLineMixin(EventMixin):
262
263
  self.collection_price_crossline.draw(renderer)
263
264
  self.collection_volume_crossline.draw(renderer)
264
265
 
265
- self._draw_text_artist(e, in_price_chart)
266
+ self._draw_text_artist(e)
266
267
  return
267
268
 
268
- def _draw_text_artist(self, e: MouseEvent, in_price_chart):
269
+ def _draw_text_artist(self, e: MouseEvent):
269
270
  x, y = (e.xdata, e.ydata)
270
271
 
271
272
  renderer = self.figure.canvas.renderer
272
- if in_price_chart:
273
+ if self.in_price_chart:
273
274
  # 가격
274
275
  self.artist_text_price.set_text(f'{float_to_str(y, self.digit_price)}{self.unit_price}')
275
276
  self.artist_text_price.set_x(self.v0 if self.veighth < x else self.vsixth)
@@ -297,17 +298,20 @@ class CrossLineMixin(EventMixin):
297
298
 
298
299
 
299
300
  class BoxMixin(CrossLineMixin):
300
- def _draw_crossline(self, e, in_price_chart):
301
- super()._draw_crossline(e, in_price_chart)
302
- self._draw_box_artist(e, in_price_chart)
301
+ def _draw_crossline(self, e):
302
+ super()._draw_crossline(e)
303
+ self._draw_box_artist(e)
303
304
  return
304
305
 
305
- def _draw_box_artist(self, e: MouseEvent, in_price_chart):
306
+ def _draw_box_artist(self, e: MouseEvent):
306
307
  y = e.ydata
307
308
 
308
309
  renderer = self.figure.canvas.renderer
310
+
311
+ self.in_candle = False
312
+ self.in_volumebar = False
309
313
  if self.intx is not None:
310
- if in_price_chart:
314
+ if self.in_price_chart:
311
315
  # 박스 크기
312
316
  high = self.df['top_box_candle'][self.intx]
313
317
  low = self.df['bottom_box_candle'][self.intx]
@@ -317,21 +321,19 @@ class BoxMixin(CrossLineMixin):
317
321
  high, low = (high+sub, low-sub)
318
322
 
319
323
  # 커서가 캔들 사이에 있는지 확인
320
- if high < y or y < low: self.in_candle = False
321
- else:
324
+ if low <= y and y <= high:
322
325
  # 캔들 강조
323
326
  self.in_candle = True
324
327
  x1, x2 = (self.intx-0.3, self.intx+1.3)
325
328
  self.collection_price_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
326
329
  self.collection_price_box.draw(renderer)
327
- elif self.volume:
330
+ elif self.in_volume_chart and self.volume:
328
331
  # 거래량 강조
329
332
  high = self.df['max_box_volume'][self.intx]
330
333
  low = 0
331
334
  if high < self.min_height_box_volume: high = self.min_height_box_volume
332
335
 
333
- if high < y or y < low: self.in_volumebar = False
334
- else:
336
+ if low <= y and y <= high:
335
337
  self.in_volumebar = True
336
338
  x1, x2 = (self.intx-0.3, self.intx+1.3)
337
339
  self.collection_volume_box.set_segments([((x1, high), (x2, high), (x2, low), (x1, low), (x1, high))])
@@ -394,8 +396,8 @@ class InfoMixin(BoxMixin):
394
396
  self._length_text = lenth_high if lenth_volume < lenth_high else lenth_volume
395
397
  return
396
398
 
397
- def _draw_box_artist(self, e, in_price_chart):
398
- super()._draw_box_artist(e, in_price_chart)
399
+ def _draw_box_artist(self, e):
400
+ super()._draw_box_artist(e)
399
401
 
400
402
  if self.intx is not None:
401
403
  if self.in_candle: self._draw_candle_info_artist(e)
seolpyo_mplchart/_draw.py CHANGED
@@ -251,18 +251,11 @@ class CandleSegmentMixin(DataMixin):
251
251
  tuple[tuple[float, float]]: candle segment
252
252
  """
253
253
  return (
254
- (x, high),
255
- (x, top),
256
- (left, top),
257
- (left, bottom),
258
- (x, bottom),
259
- (x, low),
260
- (x, bottom),
261
- (right, bottom),
262
- (right, top),
263
- (x, top),
264
- (x, high),
265
254
  (x, top),
255
+ (left, top), (left, bottom),
256
+ (x, bottom), (x, low), (x, bottom),
257
+ (right, bottom), (right, top),
258
+ (x, top), (x, high)
266
259
  )
267
260
 
268
261
  def _create_candle_segments(self):
@@ -278,8 +271,7 @@ class CandleSegmentMixin(DataMixin):
278
271
  segment_candle.append(
279
272
  self.get_candle_segment(
280
273
  is_up=is_up,
281
- x=x,
282
- left=left, right=right,
274
+ x=x, left=left, right=right,
283
275
  top=top, bottom=bottom,
284
276
  high=high, low=low,
285
277
  )
@@ -419,11 +411,8 @@ class VolumeSegmentMixin(MaSegmentMixin):
419
411
  tuple[tuple[float, float]]: volume bar segment
420
412
  """
421
413
  return (
422
- (left, top),
423
- (left, 0),
424
- (right, 0),
425
- (right, top),
426
- (left, top),
414
+ (left, 0), (left, top),
415
+ (right, top), (right, 0),
427
416
  )
428
417
 
429
418
  def _create_volume_segments(self):
@@ -131,7 +131,9 @@ class CollectionMixin(PlotMixin):
131
131
  keys.append('x')
132
132
  keys.append(f'ma{i}')
133
133
 
134
- segment_slider = self.df[keys + ['x', self.close] ].values
134
+ series = self.df[keys + ['x', self.close]]
135
+ series['x'] = series['x'] - 0.5
136
+ segment_slider = series.values
135
137
  segment_slider = segment_slider.reshape(segment_slider.shape[0], len(self.list_ma)+1, 2).swapaxes(0, 1)
136
138
  self.collection_slider.set_segments(segment_slider)
137
139
  linewidth = [1 for _ in self.list_ma]
@@ -300,7 +302,7 @@ class MouseMoveMixin(BackgroundMixin):
300
302
  self.figure.canvas.blit()
301
303
  elif self.in_price_chart or self.in_volume_chart:
302
304
  self._restore_region()
303
- self._draw_crossline(e, self.in_price_chart)
305
+ self._draw_crossline(e)
304
306
  self.figure.canvas.blit()
305
307
  else:
306
308
  if self._erase_crossline():
@@ -327,13 +329,16 @@ class MouseMoveMixin(BackgroundMixin):
327
329
  navleft, navright = self.navcoordinate
328
330
  if navleft == navright: return
329
331
 
330
- x = e.xdata
331
- leftmin, leftmax = (navleft-self._navLineWidth, navleft+self._navLineWidth_half)
332
- rightmin, rightmax = (navright-self._navLineWidth_half, navright+self._navLineWidth)
332
+ x = e.xdata.__round__()
333
+
334
+ leftmin = navleft - self._navLineWidth
335
+ leftmax = navleft + self._navLineWidth_half
336
+ rightmin = navright - self._navLineWidth_half
337
+ rightmax = navright + self._navLineWidth
333
338
  if x < leftmin: self.figure.canvas.set_cursor(cursors.POINTER)
334
- elif x < leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
339
+ elif x <= leftmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
335
340
  elif x < rightmin: self.figure.canvas.set_cursor(cursors.MOVE)
336
- elif x < rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
341
+ elif x <= rightmax: self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
337
342
  else: self.figure.canvas.set_cursor(cursors.POINTER)
338
343
  return
339
344
 
@@ -373,10 +378,10 @@ class MouseMoveMixin(BackgroundMixin):
373
378
  self.artist_text_slider.draw(renderer)
374
379
  return
375
380
 
376
- def _draw_crossline(self, e: MouseEvent, in_price_chart):
381
+ def _draw_crossline(self, e: MouseEvent):
377
382
  self.collection_slider_vline.set_segments([((e.xdata, self.slider_ymin), (e.xdata, self.slider_ymax))])
378
383
  self.collection_slider_vline.draw(self.figure.canvas.renderer)
379
- return super()._draw_crossline(e, in_price_chart)
384
+ return super()._draw_crossline(e)
380
385
 
381
386
 
382
387
  class ClickMixin(MouseMoveMixin):
@@ -400,25 +405,26 @@ class ClickMixin(MouseMoveMixin):
400
405
  self.is_click_slider = True
401
406
  self.figure.canvas.set_cursor(cursors.RESIZE_HORIZONTAL)
402
407
 
403
- x = e.xdata.__int__()
404
- navmin, navmax = self.navcoordinate
405
-
406
- leftmax = navmin+self._navLineWidth_half
407
- rightmin = navmax-self._navLineWidth_half
408
+ navleft, navright = self.navcoordinate
409
+ x = e.xdata.__round__()
410
+
411
+ leftmax = navleft + self._navLineWidth_half
412
+ rightmin = navright - self._navLineWidth_half
408
413
 
409
- grater_than_left, less_then_right = (leftmax < x, x < rightmin)
414
+ grater_than_left = leftmax < x
415
+ less_then_right = x < rightmin
410
416
  if grater_than_left and less_then_right:
411
417
  self.is_move = True
412
418
  self.x_click = x
413
419
  else:
414
- leftmin = navmin - self._navLineWidth
415
- rightmax = navmax+self._navLineWidth
420
+ leftmin = navleft - self._navLineWidth
421
+ rightmax = navright + self._navLineWidth
416
422
  if not grater_than_left and leftmin <= x:
417
423
  self.click_navleft = True
418
- self.x_click = navmax
424
+ self.x_click = navright
419
425
  elif not less_then_right and x <= rightmax:
420
426
  self.click_navright = True
421
- self.x_click = navmin
427
+ self.x_click = navleft
422
428
  else:
423
429
  self.x_click = x
424
430
  return
@@ -561,7 +567,7 @@ class ChartClickMixin(ReleaseMixin):
561
567
  elif self.in_price_chart or self.in_volume_chart:
562
568
  self._restore_region(self.is_click_chart)
563
569
  if not self.is_click_chart:
564
- self._draw_crossline(e, self.in_price_chart)
570
+ self._draw_crossline(e)
565
571
  else: self._move_chart(e)
566
572
  self.figure.canvas.blit()
567
573
  else:
@@ -0,0 +1,10 @@
1
+ from .theme import set_theme
2
+ from .nums import (
3
+ convert_num,
4
+ float_to_str,
5
+ convert_unit, convert_unit_en,
6
+ data_unit_ko, data_unit_en
7
+ )
8
+ from .xl import xl_to_dataList
9
+ from .utils import switch_backend, show, close, list_to_DataFrame
10
+
@@ -0,0 +1,67 @@
1
+
2
+
3
+ def convert_num(num):
4
+ if isinstance(num, float) and num % 1:
5
+ return num
6
+ return int(num)
7
+
8
+
9
+ def float_to_str(num: float, *, digit=0, plus=False):
10
+ if 0 < digit:
11
+ num.__round__(digit)
12
+ text = f'{num:+,.{digit}f}' if plus else f'{num:,.{digit}f}'
13
+ else:
14
+ num = round(num, digit).__int__()
15
+ text = f'{num:+,}' if plus else f'{num:,}'
16
+ return text
17
+
18
+
19
+ data_unit_ko = {
20
+ '경': 10_000_000_000_000_000,
21
+ '조': 1_000_000_000_000,
22
+ '억': 100_000_000,
23
+ '만': 10_000,
24
+ }
25
+ def convert_unit(value, *, digit=0, word='원', unit_data: dict[str, int]=None):
26
+ if not unit_data:
27
+ unit_data = data_unit_ko
28
+ # print('ko')
29
+ # print(f'{value=:,}')
30
+ v = abs(value)
31
+ for unit, n in unit_data.items():
32
+ if n <= v:
33
+ # print(f'{n=:,}')
34
+ # print(f'{unit=}')
35
+ num = value / n
36
+ if word.startswith(' '):
37
+ return f'{float_to_str(num, digit=digit)}{unit}{word}'
38
+ return f'{float_to_str(num, digit=digit)}{unit} {word}'
39
+
40
+ if not value % 1:
41
+ value = int(value)
42
+ text = f'{float_to_str(value, digit=digit)}{word}'
43
+ # print(f'{text=}')
44
+ return text
45
+
46
+ data_unit_en = {
47
+ 'Qd': 1_000_000_000_000_000,
48
+ 'T': 1_000_000_000_000,
49
+ 'B': 1_000_000_000,
50
+ 'M': 1_000_000,
51
+ 'K': 1_000,
52
+ }
53
+ def convert_unit_en(value, *, digit=0, word='$', unit_data: dict[str, int]=None):
54
+ if not unit_data:
55
+ unit_data = data_unit_en
56
+ # print('en')
57
+ # print(f'{value=:,}')
58
+ return convert_unit(value, digit=digit, word=word, unit_data=unit_data)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ a = 456.123
63
+ print(float_to_str(a))
64
+ print(float_to_str(a, 2))
65
+ print(float_to_str(a, 6))
66
+
67
+
@@ -0,0 +1,15 @@
1
+ from typing import Literal
2
+
3
+ from ..._config import ConfigData, SliderConfigData
4
+
5
+ from . import light, dark
6
+
7
+
8
+ def set_theme(config: ConfigData|SliderConfigData, theme: Literal['light', 'dark']='dark'):
9
+ if theme == 'light':
10
+ config = light.set_theme(config)
11
+ elif theme == 'dark':
12
+ config = dark.set_theme(config)
13
+
14
+ return config
15
+
@@ -0,0 +1,57 @@
1
+ from ..._config import ConfigData, SliderConfigData
2
+
3
+
4
+ def set_theme(config: ConfigData|SliderConfigData):
5
+ # axes
6
+ config.FIGURE.facecolor = '#0f0f0f'
7
+ config.AX.facecolor = '#0f0f0f'
8
+ config.AX.TICK.edgecolor = '#dbdbdb'
9
+ config.AX.TICK.fontcolor = '#dbdbdb'
10
+ config.AX.GRID.color = '#1c1c1c'
11
+
12
+ # candle
13
+ config.CANDLE.line_color = 'w'
14
+ config.CANDLE.FACECOLOR.bull_rise = '#089981'
15
+ config.CANDLE.FACECOLOR.bull_fall = '#0f0f0f'
16
+ config.CANDLE.FACECOLOR.bear_fall = '#f23645'
17
+ config.CANDLE.FACECOLOR.bear_rise = '#0f0f0f'
18
+
19
+ config.CANDLE.EDGECOLOR.bull_rise = '#089981'
20
+ config.CANDLE.EDGECOLOR.bull_fall = '#089981'
21
+ config.CANDLE.EDGECOLOR.bear_fall = '#f23645'
22
+ config.CANDLE.EDGECOLOR.bear_rise = '#f23645'
23
+ config.CANDLE.EDGECOLOR.doji = 'w'
24
+
25
+ # volume
26
+ config.VOLUME.FACECOLOR.rise = '#2A8076'
27
+ config.VOLUME.FACECOLOR.fall = '#BE4F58'
28
+ config.VOLUME.FACECOLOR.doji = '#82828A'
29
+
30
+ config.VOLUME.EDGECOLOR.rise = '#2A8076'
31
+ config.VOLUME.EDGECOLOR.fall = '#BE4F58'
32
+ config.VOLUME.EDGECOLOR.doji = '#82828A'
33
+
34
+ # ma
35
+ config.MA.color_default = 'w'
36
+ config.MA.color_list = ['#FFB300', '#03A9F4', '#AB47BC', '#8BC34A', '#EF5350']
37
+
38
+ # text
39
+ config.CURSOR.TEXT.BBOX.facecolor = '#3d3d3d'
40
+ config.CURSOR.TEXT.BBOX.edgecolor = '#ffffff'
41
+ config.CURSOR.TEXT.color = '#ffffff'
42
+
43
+ # box
44
+ config.CURSOR.BOX.edgecolor = 'w'
45
+
46
+ # line
47
+ config.CURSOR.CROSSLINE.edgecolor = '#9c9c9c'
48
+
49
+ # wartermark
50
+ config.FIGURE.WATERMARK.color = 'w'
51
+
52
+ if getattr(config, 'SLIDER', None):
53
+ config.SLIDER.NAV.edgecolor = "#00A6FF"
54
+ config.SLIDER.NAV.facecolor = '#FFFFFF4D'
55
+
56
+ return config
57
+
@@ -0,0 +1,56 @@
1
+ from ..._config import ConfigData, SliderConfigData
2
+
3
+
4
+ def set_theme(config: ConfigData|SliderConfigData):
5
+ # axes
6
+ config.FIGURE.facecolor = '#fafafa'
7
+ config.AX.facecolor = '#fafafa'
8
+ config.AX.TICK.edgecolor = 'k'
9
+ config.AX.TICK.fontcolor = 'k'
10
+ config.AX.GRID.color = '#d0d0d0'
11
+
12
+ # candle
13
+ config.CANDLE.line_color = 'k'
14
+ config.CANDLE.FACECOLOR.bull_rise = '#FF2400'
15
+ config.CANDLE.FACECOLOR.bull_fall = 'w'
16
+ config.CANDLE.FACECOLOR.bear_fall = '#1E90FF'
17
+ config.CANDLE.FACECOLOR.bear_rise = 'w'
18
+
19
+ config.CANDLE.EDGECOLOR.bull_rise = '#FF2400'
20
+ config.CANDLE.EDGECOLOR.bull_fall = '#FF2400'
21
+ config.CANDLE.EDGECOLOR.bear_fall = '#1E90FF'
22
+ config.CANDLE.EDGECOLOR.bear_rise = '#1E90FF'
23
+ config.CANDLE.EDGECOLOR.doji = 'k'
24
+
25
+ # volume
26
+ config.VOLUME.FACECOLOR.rise = '#F27663'
27
+ config.VOLUME.FACECOLOR.fall = '#70B5F2'
28
+ config.VOLUME.FACECOLOR.doji = '#BEBEBE'
29
+
30
+ config.VOLUME.EDGECOLOR.rise = '#F27663'
31
+ config.VOLUME.EDGECOLOR.fall = '#70B5F2'
32
+ config.VOLUME.EDGECOLOR.doji = '#BEBEBE'
33
+
34
+ # ma
35
+ config.MA.color_default = 'k'
36
+ config.MA.color_list = ['#8B00FF', '#008000', '#A0522D', '#008B8B', '#FF0080']
37
+ # text
38
+ config.CURSOR.TEXT.BBOX.facecolor = 'w'
39
+ config.CURSOR.TEXT.BBOX.edgecolor = 'k'
40
+ config.CURSOR.TEXT.color = 'k'
41
+
42
+ # box
43
+ config.CURSOR.BOX.edgecolor = 'k'
44
+
45
+ # line
46
+ config.CURSOR.CROSSLINE.edgecolor = 'k'
47
+
48
+ # wartermark
49
+ config.FIGURE.WATERMARK.color = 'k'
50
+
51
+ if getattr(config, 'SLIDER', None):
52
+ config.SLIDER.NAV.edgecolor = '#2962FF'
53
+ config.SLIDER.NAV.facecolor = '#0000002E'
54
+
55
+ return config
56
+
@@ -0,0 +1,28 @@
1
+ import matplotlib.pyplot as plt
2
+ import pandas as pd
3
+
4
+
5
+ def switch_backend(newbackend='TkAgg'):
6
+ "call matplotlib.pyplot.switch_backend(newbackend)"
7
+ return plt.switch_backend(newbackend)
8
+
9
+
10
+ def close(fig='all'):
11
+ "call matplotlib.pyplot.close(fig)"
12
+ return plt.close(fig)
13
+
14
+ def show(Close=True):
15
+ """
16
+ call matplotlib.pyplot.show()
17
+ ```if Close``` if True, run matplotlib.pyplot.close('all') after window closee.
18
+ """
19
+ plt.show()
20
+ if Close:
21
+ close()
22
+ return
23
+
24
+
25
+ def list_to_DataFrame(item_list):
26
+ "return pd.DataFrame(item_list)"
27
+ return pd.DataFrame(item_list)
28
+
@@ -0,0 +1,15 @@
1
+ from . import xlsx, csv
2
+
3
+
4
+ def xl_to_dataList(path_file):
5
+ pf = str(path_file)
6
+ if pf.endswith('.xlsx'):
7
+ data = xlsx.convert(path_file)
8
+ elif pf.endswith('.csv'):
9
+ data = csv.convert(path_file)
10
+ else:
11
+ msg = '다음 형식의 파일의 데이터만 변환할 수 있습니다.'
12
+ msg += f' [.xlsx, .csv]'
13
+ raise ValueError(msg)
14
+ return data
15
+
@@ -0,0 +1,46 @@
1
+ from datetime import datetime
2
+ import json
3
+
4
+
5
+ base_date = datetime(1899, 12, 30).date()
6
+
7
+
8
+ def convert(path_file: str, row=1, index_date=0, index_open=1, index_high=2, index_low=3, index_close=4, index_volume=5):
9
+ "csv 파일에서 주가 정보를 추출합니다."
10
+ item_list: list[dict[str, str|float]] = []
11
+
12
+ with open(path_file, 'r', encoding='utf-8') as txt:
13
+ j = json.load(txt)
14
+
15
+ if len(j[1]) < 6:
16
+ index_volume = None
17
+ for i in j[row:]:
18
+ if index_volume:
19
+ dt = i[index_date]
20
+ o = i[index_open]
21
+ h = i[index_high]
22
+ l = i[index_low]
23
+ c = i[index_close]
24
+ v = None
25
+ else:
26
+ dt = i[index_date]
27
+ o = i[index_open]
28
+ h = i[index_high]
29
+ l = i[index_low]
30
+ c = i[index_close]
31
+ v = i[index_volume]
32
+
33
+ o = float(o)
34
+ h = float(h)
35
+ l = float(l)
36
+ c = float(c)
37
+ if v:
38
+ v = float(v)
39
+ item = {'기준일': dt, '시가': o, '고가': h, '저가': l, '종가': c, '거래량': v}
40
+ else:
41
+ item = {'기준일': dt, '시가': o, '고가': h, '저가': l, '종가': c}
42
+
43
+ item_list.append(item)
44
+
45
+ return sorted(item_list, key=lambda x: x['기준일'])
46
+
@@ -0,0 +1,49 @@
1
+ from datetime import datetime, timedelta
2
+ from re import findall
3
+ from zipfile import ZipFile
4
+
5
+
6
+ base_date = datetime(1899, 12, 30).date()
7
+
8
+
9
+ def convert(path_file: str, sheet=1, row=4, date='A', Open='B', high='C', low='D', close='E', volume='F'):
10
+ "xlsx 파일에서 주가 정보를 추출합니다."
11
+ list_price: list[dict[str, str|float]] = []
12
+
13
+ zipfile = ZipFile(path_file)
14
+ # print(f'{zipfile.filelist=}')
15
+ a = zipfile.read(f'xl/worksheets/sheet{sheet}.xml')
16
+ # print(f'{a=}')
17
+ # print(f'{type(a)=}')
18
+ b = a.decode('utf-8')
19
+ for i in findall('<row.+?</row>', b)[row:]:
20
+ # print()
21
+ # print(f'{i=}')
22
+ dt = findall(f'<c r="{date}[0-9].+?<v>([0-9]+)</v>', i)
23
+ c = findall(f'<c r="{close}[0-9].+?<v>([0-9\.]+)</v>', i)
24
+ o = findall(f'<c r="{Open}[0-9].+?<v>([0-9\.]+)</v>', i)
25
+ h = findall(f'<c r="{high}[0-9].+?<v>([0-9\.]+)</v>', i)
26
+ l = findall(f'<c r="{low}[0-9].+?<v>([0-9\.]+)</v>', i)
27
+ v = findall(f'<c r="{volume}[0-9].+?<v>([0-9]+)</v>', i)
28
+ # print(f'{(dt, c, o, h, l, v)=}')
29
+ if not all([dt, o, h, l, c,]):
30
+ continue
31
+ try:
32
+ dt = base_date + timedelta(int(dt[0]))
33
+ c = float(c[0])
34
+ o = float(o[0])
35
+ h = float(h[0])
36
+ l = float(l[0])
37
+ except:
38
+ continue
39
+ try:
40
+ v = float(v[0])
41
+ except:
42
+ v = 0
43
+ # print(f'{(dt, c, o, h, l, v)=}')
44
+ # if 2020 < dt.year and dt.year < 2024: list_price.append({'기준일': f'{dt}', '종가': c, '시가': o, '고가': h, '저가': l, '거래량': v,})
45
+ list_price.append({'기준일': str(dt), '종가': c, '시가': o, '고가': h, '저가': l, '거래량': v,})
46
+ # for i in enumerate(list_price, 1): print(f' {i}')
47
+
48
+ return sorted(list_price, key=lambda x: x['기준일'])
49
+