pyqqq 0.12.196__tar.gz → 0.12.197__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 pyqqq might be problematic. Click here for more details.

Files changed (59) hide show
  1. {pyqqq-0.12.196 → pyqqq-0.12.197}/PKG-INFO +1 -1
  2. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyproject.toml +1 -1
  3. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/domestic_stock.py +22 -3
  4. pyqqq-0.12.197/pyqqq/utils/indicators.py +1011 -0
  5. {pyqqq-0.12.196 → pyqqq-0.12.197}/README.md +0 -0
  6. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/__init__.py +0 -0
  7. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/__init__.py +0 -0
  8. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/broker.py +0 -0
  9. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/environment.py +0 -0
  10. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/logger.py +0 -0
  11. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/positionprovider.py +0 -0
  12. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/strategy.py +0 -0
  13. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/utils.py +0 -0
  14. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/backtest/wallclock.py +0 -0
  15. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/__init__.py +0 -0
  16. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/ebest/__init__.py +0 -0
  17. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
  18. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/ebest/oauth.py +0 -0
  19. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/ebest/simple.py +0 -0
  20. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/ebest/tr_client.py +0 -0
  21. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/helper.py +0 -0
  22. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/__init__.py +0 -0
  23. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/oauth.py +0 -0
  24. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
  25. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/simple.py +0 -0
  26. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
  27. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/kis/tr_client.py +0 -0
  28. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
  29. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/brokerage/tracker.py +0 -0
  30. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/config.py +0 -0
  31. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/__init__.py +0 -0
  32. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/daily.py +0 -0
  33. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/domestic.py +0 -0
  34. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/index.py +0 -0
  35. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/minutes.py +0 -0
  36. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/overseas.py +0 -0
  37. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/realtime.py +0 -0
  38. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/ticks.py +0 -0
  39. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/data/us_stocks.py +0 -0
  40. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/datatypes.py +0 -0
  41. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/executors/__init__.py +0 -0
  42. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/executors/hook.py +0 -0
  43. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/__init__.py +0 -0
  44. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/api_client.py +0 -0
  45. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/array.py +0 -0
  46. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/casting.py +0 -0
  47. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/compute.py +0 -0
  48. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/copycat.py +0 -0
  49. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/daily_tickers.py +0 -0
  50. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/display.py +0 -0
  51. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/kvstore.py +0 -0
  52. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/limiter.py +0 -0
  53. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/local_cache.py +0 -0
  54. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/logger.py +0 -0
  55. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/market_schedule.py +0 -0
  56. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/mock_api.py +0 -0
  57. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/position_classifier.py +0 -0
  58. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/retry.py +0 -0
  59. {pyqqq-0.12.196 → pyqqq-0.12.197}/pyqqq/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqqq
3
- Version: 0.12.196
3
+ Version: 0.12.197
4
4
  Summary: Package for quantitative strategy development on the PyQQQ platform
5
5
  License: MIT
6
6
  Author: PyQQQ team
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pyqqq"
3
- version = "0.12.196"
3
+ version = "0.12.197"
4
4
  description = "Package for quantitative strategy development on the PyQQQ platform"
5
5
  authors = ["PyQQQ team <pyqqq.cs@gmail.com>"]
6
6
  readme = "README.md"
@@ -1700,6 +1700,7 @@ class KISDomesticStock:
1700
1700
  fid_input_price_1: int = None,
1701
1701
  fid_input_price_2: int = None,
1702
1702
  fid_vol_cnt: int = None,
1703
+ fid_cond_mrkt_div_code: str = "J",
1703
1704
  ):
1704
1705
  """
1705
1706
  (국내주식시세) 거래량순위[v1_국내주식-047]
@@ -1745,6 +1746,8 @@ class KISDomesticStock:
1745
1746
  | ex) 100000
1746
1747
  | 전체 거래량 대상 조회 시 FID_VOL_CNT None
1747
1748
 
1749
+ fid_cond_mrkt_div_code (str): FID 조건시장분류코드 - J:KRX, NX:NXT
1750
+
1748
1751
  Returns:
1749
1752
  dict:
1750
1753
 
@@ -1786,11 +1789,12 @@ class KISDomesticStock:
1786
1789
  assert fid_input_price_1 is None or isinstance(fid_input_price_1, int), "fid_input_price_1 must be None or int"
1787
1790
  assert fid_input_price_2 is None or isinstance(fid_input_price_2, int), "fid_input_price_2 must be None or int"
1788
1791
  assert fid_vol_cnt is None or isinstance(fid_vol_cnt, int), "fid_vol_cnt must be None or int"
1792
+ assert fid_cond_mrkt_div_code in ["J", "NX"], "fid_cond_mrkt_div_code must be 'J' or 'NX'"
1789
1793
 
1790
1794
  url_path = "/uapi/domestic-stock/v1/quotations/volume-rank"
1791
1795
  tr_id = "FHPST01710000"
1792
1796
  params = {
1793
- "FID_COND_MRKT_DIV_CODE": "J",
1797
+ "FID_COND_MRKT_DIV_CODE": fid_cond_mrkt_div_code,
1794
1798
  "FID_COND_SCR_DIV_CODE": "20171",
1795
1799
  "FID_INPUT_ISCD": fid_input_iscd,
1796
1800
  "FID_DIV_CLS_CODE": fid_div_cls_code,
@@ -2194,7 +2198,14 @@ class KISDomesticStock:
2194
2198
 
2195
2199
  return result
2196
2200
 
2197
- def inquire_daily_trade_volume(self, fid_input_iscd: str, fid_input_date_1: dtm.date, fid_input_date_2: dtm.date, tr_cont: str = ""):
2201
+ def inquire_daily_trade_volume(
2202
+ self,
2203
+ fid_input_iscd: str,
2204
+ fid_input_date_1: dtm.date,
2205
+ fid_input_date_2: dtm.date,
2206
+ tr_cont: str = "",
2207
+ fid_cond_mrkt_div_code: str = "J",
2208
+ ):
2198
2209
  """
2199
2210
  (국내주식시세) 종목별일별매수매도체결량 [v1_국내주식-056]
2200
2211
 
@@ -2206,6 +2217,7 @@ class KISDomesticStock:
2206
2217
  fid_input_date_1 (dtm.date): 입력 일자1 - from
2207
2218
  fid_input_date_2 (dtm.date): 입력 일자2 - to
2208
2219
  tr_cont (str): 연속조회여부
2220
+ fid_cond_mrkt_div_code (str): FID 조건시장분류코드 - J:KRX, NX:NXT, UN:통합
2209
2221
 
2210
2222
  Returns:
2211
2223
  dict:
@@ -2230,10 +2242,17 @@ class KISDomesticStock:
2230
2242
  ValueError: API 에러 발생시
2231
2243
  """
2232
2244
  assert not self.auth.paper_trading, "실전계좌에서만 사용 가능한 API입니다."
2245
+ assert fid_cond_mrkt_div_code in ["J", "NX", "UN"], "fid_cond_mrkt_div_code must be 'J', 'NX' or 'UN'"
2233
2246
 
2234
2247
  url_path = "/uapi/domestic-stock/v1/quotations/inquire-daily-trade-volume"
2235
2248
  tr_id = "FHKST03010800"
2236
- params = {"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": fid_input_iscd, "FID_INPUT_DATE_1": fid_input_date_1.strftime("%Y%m%d"), "FID_INPUT_DATE_2": fid_input_date_2.strftime("%Y%m%d"), "FID_PERIOD_DIV_CODE": "D"}
2249
+ params = {
2250
+ "FID_COND_MRKT_DIV_CODE": fid_cond_mrkt_div_code,
2251
+ "FID_INPUT_ISCD": fid_input_iscd,
2252
+ "FID_INPUT_DATE_1": fid_input_date_1.strftime("%Y%m%d"),
2253
+ "FID_INPUT_DATE_2": fid_input_date_2.strftime("%Y%m%d"),
2254
+ "FID_PERIOD_DIV_CODE": "D",
2255
+ }
2237
2256
  res_body, res_headers = self._tr_request(url_path, tr_id, tr_cont, params=params)
2238
2257
 
2239
2258
  if res_body["rt_cd"] != "0":
@@ -0,0 +1,1011 @@
1
+ """
2
+ 기술적 분석 지표를 계산하는 모듈입니다.
3
+
4
+ 이 모듈은 주식 시장의 기술적 분석에 사용되는 다양한 지표들을 계산하는 기능을 제공합니다.
5
+ pandas DataFrame을 기반으로 하여 효율적인 계산을 수행하며, 두 가지 구현 방식을 제공합니다:
6
+ - Indicators: pandas 기반의 표준 구현
7
+ - FastIndicators: numpy 기반의 고성능 구현
8
+
9
+ 주요 지표:
10
+ - ROC (Rate of Change): 가격 변화율
11
+ - ATR (Average True Range): 평균 실질 범위
12
+ - Bollinger Bands: 볼린저 밴드
13
+ - MACD (Moving Average Convergence Divergence): 이동평균 수렴/발산
14
+ - RSI (Relative Strength Index): 상대강도지수
15
+ - RSI Cutler: 상대강도지수를 산술평균을 사용하여 계산한 값 (**주의** 한국의 증권사 차트에서는 RSI Cutler가 지수평균을 사용하고, RSI가 산술평균을 사용하도록 반대로 표시된 경우가 있음)
16
+ - OBV (On-Balance Volume): 거래량 균형
17
+ - ADX (Average Directional Index): 평균 방향성 지수
18
+ - Stochastic RSI: 가격 대신 RSI에 스토캐스틱 오실레이터 적용
19
+ - Williams %R: 윌리엄스 %R
20
+ - Stochastic: 스토캐스틱 오실레이터
21
+ - Ichimoku: 일목균형표
22
+ """
23
+
24
+ import pandas as pd
25
+ import numpy as np
26
+
27
+
28
+ class Indicators:
29
+ """
30
+ 기술적 분석 지표를 계산하는 클래스입니다.
31
+
32
+ 이 클래스는 pandas DataFrame을 기반으로 하여 다양한 기술적 분석 지표를 계산합니다.
33
+ 주식 시장의 가격 데이터를 분석하여 트레이딩 신호를 생성하는 데 사용됩니다.
34
+
35
+ Attributes:
36
+ df (pd.DataFrame): OHLCV 데이터를 포함하는 DataFrame. 시간 순 정렬
37
+ """
38
+
39
+ def __init__(self, df: pd.DataFrame):
40
+ """
41
+ Indicators 클래스 초기화
42
+
43
+ Args:
44
+ df (pd.DataFrame): OHLCV 데이터를 포함하는 DataFrame.
45
+ 반드시 'close', 'high', 'low', 'volume' 컬럼을 포함해야 함
46
+
47
+ Raises:
48
+ KeyError: 필수 컬럼이 없는 경우 발생합니다.
49
+ """
50
+ required_columns = ['close', 'high', 'low', 'volume']
51
+ missing_columns = [col for col in required_columns if col not in df.columns]
52
+ if missing_columns:
53
+ raise KeyError(f"필수 컬럼이 누락되었습니다: {missing_columns}")
54
+
55
+ self.df = df.copy()
56
+
57
+ # -----------------------
58
+ # 1. Rate of Change (ROC)
59
+ # -----------------------
60
+ def roc(self, period: int = 12) -> pd.Series:
61
+ """
62
+ Rate of Change (ROC) 지표를 계산합니다.
63
+
64
+ ROC는 현재 가격이 N기간 전 가격 대비 얼마나 변했는지를 백분율로 나타내는 지표입니다.
65
+ 가격의 모멘텀을 측정하는 데 사용되며, 0보다 크면 상승 모멘텀, 0보다 작으면 하락 모멘텀을 의미합니다.
66
+
67
+ Args:
68
+ period (int, optional): 계산 기간. 기본값은 12입니다.
69
+
70
+ Returns:
71
+ pd.Series: ROC 값이 포함된 Series. 인덱스는 원본 DataFrame과 동일합니다.
72
+
73
+ Examples:
74
+ >>> indicators = Indicators(df)
75
+ >>> roc_values = indicators.roc(period=12)
76
+ >>> df["roc"] = roc_values
77
+ """
78
+ return (self.df['close'].diff(period) / self.df['close'].shift(period)) * 100
79
+
80
+ # -----------------------
81
+ # 2. ATR (Average True Range)
82
+ # -----------------------
83
+ def atr(self, period: int = 14) -> pd.Series:
84
+ """
85
+ Average True Range (ATR) 지표를 계산합니다.
86
+
87
+ ATR은 주어진 기간 동안의 평균 실질 범위를 나타내는 지표입니다.
88
+ 가격의 변동성을 측정하는 데 사용되며, 높은 ATR 값은 높은 변동성을 의미합니다.
89
+
90
+ Args:
91
+ period (int, optional): 계산 기간. 기본값은 14입니다.
92
+
93
+ Returns:
94
+ pd.Series: ATR 값이 포함된 Series. 인덱스는 원본 DataFrame과 동일합니다.
95
+
96
+ Examples:
97
+ >>> indicators = Indicators(df)
98
+ >>> atr_values = indicators.atr(period=14)
99
+ >>> df["atr"] = atr_values
100
+ """
101
+ high, low, close = self.df['high'], self.df['low'], self.df['close']
102
+ tr1 = high - low
103
+ tr2 = (high - close.shift()).abs()
104
+ tr3 = (low - close.shift()).abs()
105
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
106
+ atr = tr.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
107
+ return atr
108
+
109
+ # -----------------------
110
+ # 3. Bollinger Bands
111
+ # -----------------------
112
+ def bollinger(self, period: int = 20, k: float = 2.0) -> pd.DataFrame:
113
+ """
114
+ Bollinger Bands 지표를 계산합니다.
115
+
116
+ 볼린저 밴드는 이동평균선을 중심으로 표준편차의 배수만큼 떨어진 상한선과 하한선을 그린 지표입니다.
117
+ 가격이 상한선 근처에 있으면 과매수 상태, 하한선 근처에 있으면 과매도 상태로 판단할 수 있습니다.
118
+ 밴드의 폭이 좁아지면 변동성이 낮아지고, 폭이 넓어지면 변동성이 높아진다는 신호로 해석됩니다.
119
+
120
+ Args:
121
+ period (int, optional): 이동평균 계산 기간. 기본값은 20입니다.
122
+ k (float, optional): 표준편차 배수. 기본값은 2.0입니다.
123
+
124
+ Returns:
125
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
126
+ - bb_middle: 중간선 (이동평균)
127
+ - bb_upper: 상한선 (중간선 + k * 표준편차)
128
+ - bb_lower: 하한선 (중간선 - k * 표준편차)
129
+
130
+ Examples:
131
+ >>> indicators = Indicators(df)
132
+ >>> bb = indicators.bollinger(period=20, k=2.0)
133
+ >>> df["bb_middle"] = bb["bb_middle"]
134
+ >>> df["bb_upper"] = bb["bb_upper"]
135
+ >>> df["bb_lower"] = bb["bb_lower"]
136
+ """
137
+ middle = self.df['close'].rolling(window=period).mean()
138
+ std = self.df['close'].rolling(window=period).std(ddof=0) # TradingView 호환
139
+ upper = middle + k * std
140
+ lower = middle - k * std
141
+ return pd.DataFrame({"bb_middle": middle, "bb_upper": upper, "bb_lower": lower})
142
+
143
+ # -----------------------
144
+ # 4. MACD
145
+ # -----------------------
146
+ def macd(self, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame:
147
+ """
148
+ MACD (Moving Average Convergence Divergence) 지표를 계산합니다.
149
+
150
+ MACD는 두 개의 지수이동평균선의 차이를 나타내는 지표입니다.
151
+ 트렌드의 변화와 모멘텀을 파악하는 데 사용되며, 다음과 같이 해석됩니다:
152
+ - MACD선이 시그널선을 상향 돌파하면 매수 신호
153
+ - MACD선이 시그널선을 하향 돌파하면 매도 신호
154
+ - 히스토그램이 0선을 상향 돌파하면 상승 모멘텀
155
+ - 히스토그램이 0선을 하향 돌파하면 하락 모멘텀
156
+
157
+ Args:
158
+ fast (int, optional): 빠른 지수이동평균 기간. 기본값은 12입니다.
159
+ slow (int, optional): 느린 지수이동평균 기간. 기본값은 26입니다.
160
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
161
+
162
+ Returns:
163
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
164
+ - macd: MACD선 (빠른 EMA - 느린 EMA)
165
+ - macd_signal: 시그널선 (MACD선의 EMA)
166
+ - macd_histogram: 히스토그램 (MACD선 - 시그널선)
167
+
168
+ Examples:
169
+ >>> indicators = Indicators(df)
170
+ >>> macd_data = indicators.macd(fast=12, slow=26, signal=9)
171
+ >>> df["macd"] = macd_data["macd"]
172
+ >>> df["macd_signal"] = macd_data["macd_signal"]
173
+ >>> df["macd_histogram"] = macd_data["macd_histogram"]
174
+ """
175
+ ema_fast = self.df['close'].ewm(span=fast, adjust=False).mean()
176
+ ema_slow = self.df['close'].ewm(span=slow, adjust=False).mean()
177
+ macd_line = ema_fast - ema_slow
178
+ signal_line = macd_line.ewm(span=signal, adjust=False).mean()
179
+ hist = macd_line - signal_line
180
+ return pd.DataFrame({"macd": macd_line, "macd_signal": signal_line, "macd_histogram": hist})
181
+
182
+ def macd_histogram_color(self, hist: pd.Series) -> pd.Series:
183
+ """
184
+ MACD 히스토그램의 색상을 결정합니다.
185
+
186
+ 히스토그램의 값과 이전 값과의 비교를 통해 색상을 결정합니다:
187
+ - PI (Positive Increasing): 양수이면서 증가
188
+ - PD (Positive Decreasing): 양수이면서 감소
189
+ - NI (Negative Increasing): 음수이면서 증가
190
+ - ND (Negative Decreasing): 음수이면서 감소
191
+
192
+ Args:
193
+ hist (pd.Series): MACD 히스토그램 값이 포함된 Series
194
+
195
+ Returns:
196
+ pd.Series: 색상 코드가 포함된 Series ('PI', 'PD', 'NI', 'ND')
197
+
198
+ Examples:
199
+ >>> indicators = Indicators(df)
200
+ >>> macd_data = indicators.macd(fast=12, slow=26, signal=9)
201
+ >>> hist = macd_data['macd_histogram']
202
+ >>> colors = indicators.macd_histogram_color(hist)
203
+ >>> df["macd_histogram_colors"] = colors
204
+ """
205
+ hist_color = pd.Series(index=hist.index, dtype=object)
206
+ for i in range(len(hist)):
207
+ if i == 0:
208
+ hist_color.iloc[i] = 'PI' if hist.iloc[i] > 0 else 'NI'
209
+ else:
210
+ if hist.iloc[i] > 0:
211
+ hist_color.iloc[i] = 'PI' if hist.iloc[i] > hist.iloc[i - 1] else 'PD'
212
+ else:
213
+ hist_color.iloc[i] = 'NI' if hist.iloc[i] > hist.iloc[i - 1] else 'ND'
214
+ return hist_color
215
+
216
+ # -----------------------
217
+ # 5. RSI (Wilder's method)
218
+ # -----------------------
219
+ def rsi(self, period: int = 14, signal: int = 9) -> pd.DataFrame:
220
+ """
221
+ RSI (Relative Strength Index) 지표를 Wilder 방법으로 계산합니다.
222
+
223
+ RSI는 주가의 상승과 하락의 상대적 강도를 0-100 사이의 값으로 나타내는 지표입니다.
224
+ Wilder의 방법은 지수이동평균을 사용하여 계산하며, 다음과 같이 해석됩니다:
225
+ - 70 이상: 과매수 상태 (매도 신호)
226
+ - 30 이하: 과매도 상태 (매수 신호)
227
+ - 50 근처: 중립 상태
228
+
229
+ Args:
230
+ period (int, optional): 계산 기간. 기본값은 14입니다.
231
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
232
+
233
+ Returns:
234
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
235
+ - rsi: RSI 값 (0-100)
236
+ - rsi_signal: RSI 시그널선 (RSI의 EMA)
237
+
238
+ Examples:
239
+ >>> indicators = Indicators(df)
240
+ >>> rsi_data = indicators.rsi(period=14, signal=9)
241
+ >>> df["rsi"] = rsi_data["rsi"]
242
+ >>> df["rsi_signal"] = rsi_data["rsi_signal"]
243
+ >>> rsi30_data = indicators.rsi(period=30, signal=9)
244
+ >>> df["rsi30"] = rsi30_data["rsi"]
245
+ """
246
+ delta = self.df['close'].diff()
247
+ gain = delta.clip(lower=0)
248
+ loss = -delta.clip(upper=0)
249
+ avg_gain = gain.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
250
+ avg_loss = loss.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
251
+ rs = avg_gain / avg_loss
252
+ rsi = 100 - (100 / (1 + rs))
253
+ rsi_signal = rsi.ewm(span=signal, adjust=False).mean()
254
+ return pd.DataFrame({"rsi": rsi, "rsi_signal": rsi_signal})
255
+
256
+ # -----------------------
257
+ # 6. RSI (Cutler's method)
258
+ # -----------------------
259
+ def rsi_cutler(self, period: int = 14, signal: int = 9) -> pd.DataFrame:
260
+ """
261
+ RSI (Relative Strength Index) 지표를 Cutler 방법으로 계산합니다.
262
+
263
+ Cutler의 방법은 단순이동평균을 사용하여 RSI를 계산합니다.
264
+ Wilder 방법과 달리 더 단순한 계산 방식을 사용하며, 다음과 같이 해석됩니다:
265
+ - 70 이상: 과매수 상태 (매도 신호)
266
+ - 30 이하: 과매도 상태 (매수 신호)
267
+ - 50 근처: 중립 상태
268
+
269
+ Args:
270
+ period (int, optional): 계산 기간. 기본값은 14입니다.
271
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
272
+
273
+ Returns:
274
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
275
+ - rsi_cutler: RSI 값 (0-100)
276
+ - rsi_cutler_signal: RSI 시그널선 (RSI의 EMA)
277
+
278
+ Examples:
279
+ >>> indicators = Indicators(df)
280
+ >>> rsi_data = indicators.rsi_cutler(period=14, signal=9)
281
+ """
282
+ delta = self.df['close'].diff()
283
+ gain = delta.clip(lower=0)
284
+ loss = -delta.clip(upper=0)
285
+ avg_gain = gain.rolling(window=period).mean()
286
+ avg_loss = loss.rolling(window=period).mean()
287
+ rs = avg_gain / avg_loss
288
+ rsi = 100 - (100 / (1 + rs))
289
+ rsi_signal = rsi.ewm(span=signal, adjust=False).mean()
290
+ return pd.DataFrame({"rsi_cutler": rsi, "rsi_cutler_signal": rsi_signal})
291
+
292
+ # -----------------------
293
+ # 7. OBV (On-Balance Volume)
294
+ # -----------------------
295
+ def obv(self) -> pd.Series:
296
+ """
297
+ OBV (On-Balance Volume) 지표를 계산합니다.
298
+
299
+ OBV는 거래량을 누적하여 가격의 움직임을 예측하는 지표입니다.
300
+ 종가가 상승하면 거래량을 더하고, 하락하면 거래량을 빼서 누적합니다.
301
+ OBV가 상승하면 매수 압력이 강하고, 하락하면 매도 압력이 강하다고 해석됩니다.
302
+ OBV는 데이터의 시작 시점에 따라 값은 변경되므로 값 대신 변화량을 사용하는 지표입니다.
303
+
304
+ Returns:
305
+ pd.Series: OBV 값이 포함된 Series. 인덱스는 원본 DataFrame과 동일합니다.
306
+
307
+ Examples:
308
+ >>> indicators = Indicators(df)
309
+ >>> obv_values = indicators.obv()
310
+ >>> df["obv"] = obv_values
311
+ """
312
+ direction = np.sign(self.df['close'].diff()).fillna(0)
313
+ obv = (direction * self.df['volume']).cumsum()
314
+ return obv
315
+
316
+ # -----------------------
317
+ # 8. ADX (Average Directional Index)
318
+ # -----------------------
319
+ def adx(self, period: int = 14) -> pd.DataFrame:
320
+ """
321
+ ADX (Average Directional Index) 지표를 계산합니다.
322
+
323
+ ADX는 트렌드의 강도를 측정하는 지표로, 방향성 지수(DI)와 함께 사용됩니다.
324
+ ADX 값이 높을수록 강한 트렌드가 형성되어 있다고 해석됩니다:
325
+ - ADX > 25: 강한 트렌드
326
+ - ADX < 20: 약한 트렌드 또는 횡보
327
+ - +DI > -DI: 상승 트렌드
328
+ - -DI > +DI: 하락 트렌드
329
+
330
+ Args:
331
+ period (int, optional): 계산 기간. 기본값은 14입니다.
332
+
333
+ Returns:
334
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
335
+ - plus_di: +DI (상승 방향성 지수)
336
+ - minus_di: -DI (하락 방향성 지수)
337
+ - adx: ADX (평균 방향성 지수)
338
+
339
+ Examples:
340
+ >>> indicators = Indicators(df)
341
+ >>> adx_data = indicators.adx(period=14)
342
+ >>> df["plus_di"] = adx_data["plus_di"]
343
+ >>> df["minus_di"] = adx_data["minus_di"]
344
+ >>> df["adx"] = adx_data["adx"]
345
+ """
346
+ high, low, close = self.df['high'], self.df['low'], self.df['close']
347
+
348
+ # 1. up_move, down_move 정의
349
+ up_move = high.diff()
350
+ down_move = -low.diff() # = low.shift() - low
351
+
352
+ # 2. +DM, -DM 계산
353
+ plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
354
+ minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
355
+
356
+ plus_dm = pd.Series(plus_dm, index=high.index)
357
+ minus_dm = pd.Series(minus_dm, index=high.index)
358
+
359
+ # 3. True Range (TR)
360
+ tr1 = high - low
361
+ tr2 = (high - close.shift()).abs()
362
+ tr3 = (low - close.shift()).abs()
363
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
364
+
365
+ # 4. Smoothed averages (Wilder 방식 → alpha=1/period)
366
+ atr = tr.ewm(alpha=1 / period, min_periods=period, adjust=False).mean()
367
+ plus_di = 100 * (plus_dm.ewm(alpha=1 / period, min_periods=period, adjust=False).mean() / atr)
368
+ minus_di = 100 * (minus_dm.ewm(alpha=1 / period, min_periods=period, adjust=False).mean() / atr)
369
+
370
+ # 5. DX와 ADX
371
+ dx = ((plus_di - minus_di).abs() / (plus_di + minus_di)) * 100
372
+ adx = dx.ewm(alpha=1/period, min_periods=period, adjust=False).mean()
373
+
374
+ return pd.DataFrame({"plus_di": plus_di, "minus_di": minus_di, "adx": adx})
375
+
376
+ # -----------------------
377
+ # 9. Stochastic RSI
378
+ # -----------------------
379
+ def stoch_rsi(self, period: int = 14, smooth_k: int = 3, smooth_d: int = 3) -> pd.DataFrame:
380
+ """
381
+ Stochastic RSI 지표를 계산합니다.
382
+
383
+ Stochastic RSI는 RSI를 스토캐스틱 공식에 적용한 지표입니다.
384
+ RSI의 과매수/과매도 상태를 더 민감하게 감지할 수 있으며, 다음과 같이 해석됩니다:
385
+ - 80 이상: 과매수 상태 (매도 신호)
386
+ - 20 이하: 과매도 상태 (매수 신호)
387
+ - %K가 %D를 상향 돌파: 매수 신호
388
+ - %K가 %D를 하향 돌파: 매도 신호
389
+
390
+ Args:
391
+ period (int, optional): RSI 계산 기간. 기본값은 14입니다.
392
+ smooth_k (int, optional): %K 스무딩 기간. 기본값은 3입니다.
393
+ smooth_d (int, optional): %D 스무딩 기간. 기본값은 3입니다.
394
+
395
+ Returns:
396
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
397
+ - stochrsi: Stochastic RSI 원본 값
398
+ - stochrsi_%K: %K 값 (스무딩된 Stochastic RSI)
399
+ - stochrsi_%D: %D 값 (%K의 이동평균)
400
+
401
+ Examples:
402
+ >>> indicators = Indicators(df)
403
+ >>> stoch_rsi_data = indicators.stoch_rsi(period=14, smooth_k=3, smooth_d=3)
404
+ >>> df["stochrsi"] = stoch_rsi_data["stochrsi"]
405
+ >>> df["stochrsi_K"] = stoch_rsi_data["stochrsi_%K"]
406
+ >>> df["stochrsi_D"] = stoch_rsi_data["stochrsi_%D"]
407
+ """
408
+ rsi_series = self.rsi(period=period)["rsi"]
409
+ min_rsi = rsi_series.rolling(window=period).min()
410
+ max_rsi = rsi_series.rolling(window=period).max()
411
+ stoch_rsi = 100 * (rsi_series - min_rsi) / (max_rsi - min_rsi)
412
+ k = stoch_rsi.rolling(window=smooth_k).mean()
413
+ d = k.rolling(window=smooth_d).mean()
414
+ return pd.DataFrame({"stochrsi": stoch_rsi, "stochrsi_%K": k, "stochrsi_%D": d})
415
+
416
+ # -----------------------
417
+ # 10. Williams %R
418
+ # -----------------------
419
+ def williams_r(self, period: int = 14) -> pd.Series:
420
+ """
421
+ Williams %R 지표를 계산합니다.
422
+
423
+ Williams %R은 주어진 기간 내에서 현재 가격이 최고가(0)와 최저가(-100) 사이에서 어느 위치에 있는지를 나타내는 지표입니다.
424
+ 스토캐스틱 오실레이터와 유사하지만 반대 방향으로 계산됩니다:
425
+ - -20 이상: 과매수 상태 (매도 신호)
426
+ - -80 이하: 과매도 상태 (매수 신호)
427
+ - -50 근처: 중립 상태
428
+
429
+ Args:
430
+ period (int, optional): 계산 기간. 기본값은 14입니다.
431
+
432
+ Returns:
433
+ pd.Series: Williams %R 값이 포함된 Series. 인덱스는 원본 DataFrame과 동일합니다.
434
+
435
+ Examples:
436
+ >>> indicators = Indicators(df)
437
+ >>> williams_r_values = indicators.williams_r(period=14)
438
+ >>> df["williams_r"] = williams_r_values
439
+ """
440
+ highest_high = self.df['high'].rolling(window=period).max()
441
+ lowest_low = self.df['low'].rolling(window=period).min()
442
+ wr = (highest_high - self.df['close']) / (highest_high - lowest_low) * -100
443
+ return wr
444
+
445
+ # -----------------------
446
+ # 11. Stochastic Oscillator
447
+ # -----------------------
448
+ def stochastic(self, period: int = 14, smooth_k: int = 3, smooth_d: int = 3) -> pd.DataFrame:
449
+ """
450
+ Stochastic Oscillator (%K, %D) 지표를 계산합니다.
451
+
452
+ 스토캐스틱 오실레이터는 주어진 기간 내에서 현재 가격이 최고가와 최저가 사이에서 어느 위치에 있는지를 나타내는 지표입니다.
453
+ 과매수/과매도 상태를 판단하는 데 사용되며, 다음과 같이 해석됩니다:
454
+ - 80 이상: 과매수 상태 (매도 신호)
455
+ - 20 이하: 과매도 상태 (매수 신호)
456
+ - %K가 %D를 상향 돌파: 매수 신호
457
+ - %K가 %D를 하향 돌파: 매도 신호
458
+
459
+ Args:
460
+ period (int, optional): 계산 기간. 기본값은 14입니다.
461
+ smooth_k (int, optional): %K 스무딩 기간. 기본값은 3입니다.
462
+ smooth_d (int, optional): %D 스무딩 기간. 기본값은 3입니다.
463
+
464
+ Returns:
465
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
466
+ - stoch_fast_%K: Fast %K 값 (원본)
467
+ - stoch_%K: Slow %K 값 (스무딩된 %K)
468
+ - stoch_%D: %D 값 (Slow %K의 이동평균)
469
+
470
+ Examples:
471
+ >>> indicators = Indicators(df)
472
+ >>> stoch_data = indicators.stochastic(period=14, smooth_k=3, smooth_d=3)
473
+ >>> df["stoch_fast_K"] = stoch_data["stoch_fast_%K"]
474
+ >>> df["stoch_K"] = stoch_data["stoch_%K"]
475
+ >>> df["stoch_D"] = stoch_data["stoch_%D"]
476
+ """
477
+ low_min = self.df['low'].rolling(window=period).min()
478
+ high_max = self.df['high'].rolling(window=period).max()
479
+
480
+ # %K 원본
481
+ k_fast = 100 * (self.df['close'] - low_min) / (high_max - low_min)
482
+
483
+ # Slow %K (보통 3일 SMA)
484
+ k_slow = k_fast.rolling(window=smooth_k).mean()
485
+
486
+ # %D (Slow %K의 이동평균)
487
+ d = k_slow.rolling(window=smooth_d).mean()
488
+
489
+ return pd.DataFrame({"stoch_fast_%K": k_fast, "stoch_%K": k_slow, "stoch_%D": d})
490
+
491
+ # -----------------------
492
+ # 12. Ichimoku Kinko Hyo
493
+ # -----------------------
494
+ def ichimoku(self, tenkan_period: int = 9, kijun_period: int = 26, senkou_period: int = 52, displacement: int = 26) -> pd.DataFrame:
495
+ """
496
+ Ichimoku Kinko Hyo (일목균형표) 지표를 계산합니다.
497
+
498
+ 일목균형표는 일본의 기술적 분석 도구로, 5개의 선으로 구성되어 있습니다:
499
+ - 전환선 (Tenkan-sen): 9일간의 최고가와 최저가의 중간값
500
+ - 기준선 (Kijun-sen): 26일간의 최고가와 최저가의 중간값
501
+ - 선행스팬A (Senkou Span A): 전환선과 기준선의 중간값을 26일 앞으로 이동
502
+ - 선행스팬B (Senkou Span B): 52일간의 최고가와 최저가의 중간값을 26일 앞으로 이동
503
+ - 후행스팬 (Chikou Span): 현재 종가를 26일 뒤로 이동
504
+
505
+ 해석 방법:
506
+ - 가격이 구름대(선행스팬A, B 사이) 위에 있으면 상승 추세
507
+ - 가격이 구름대 아래에 있으면 하락 추세
508
+ - 전환선이 기준선을 상향 돌파하면 매수 신호
509
+ - 전환선이 기준선을 하향 돌파하면 매도 신호
510
+
511
+ Args:
512
+ tenkan_period (int, optional): 전환선 기간. 기본값은 9입니다.
513
+ kijun_period (int, optional): 기준선 기간. 기본값은 26입니다.
514
+ senkou_period (int, optional): 선행스팬B 기간. 기본값은 52입니다.
515
+ displacement (int, optional): 선행스팬 이동 기간. 기본값은 26입니다.
516
+
517
+ Returns:
518
+ pd.DataFrame: 다음 컬럼을 포함하는 DataFrame:
519
+ - tenkan: 전환선
520
+ - kijun: 기준선
521
+ - senkou_a: 선행스팬A
522
+ - senkou_b: 선행스팬B
523
+ - chikou: 후행스팬
524
+
525
+ Examples:
526
+ >>> indicators = Indicators(df)
527
+ >>> ichimoku_data = indicators.ichimoku(tenkan_period=9, kijun_period=26, senkou_period=52, displacement=26)
528
+ >>> df["tenkan"] = ichimoku_data["tenkan"]
529
+ >>> df["kijun"] = ichimoku_data["kijun"]
530
+ >>> df["senkou_a"] = ichimoku_data["senkou_a"]
531
+ >>> df["senkou_b"] = ichimoku_data["senkou_b"]
532
+ >>> df["chikou"] = ichimoku_data["chikou"]
533
+ """
534
+
535
+ high = self.df["high"]
536
+ low = self.df["low"]
537
+ close = self.df["close"]
538
+
539
+ # 전환선 (Tenkan-sen)
540
+ tenkan = (high.rolling(window=tenkan_period).max() + low.rolling(window=tenkan_period).min()) / 2
541
+
542
+ # 기준선 (Kijun-sen)
543
+ kijun = (high.rolling(window=kijun_period).max() + low.rolling(window=kijun_period).min()) / 2
544
+
545
+ # 선행스팬1 (Senkou Span A)
546
+ senkou_a = ((tenkan + kijun) / 2).shift(displacement - 1)
547
+
548
+ # 선행스팬2 (Senkou Span B)
549
+ senkou_b = ((high.rolling(window=senkou_period).max() + low.rolling(window=senkou_period).min()) / 2).shift(displacement - 1)
550
+
551
+ # 후행스팬 (Chikou Span)
552
+ chikou = close.shift(-displacement)
553
+
554
+ return pd.DataFrame({
555
+ "tenkan": tenkan,
556
+ "kijun": kijun,
557
+ "senkou_a": senkou_a,
558
+ "senkou_b": senkou_b,
559
+ "chikou": chikou
560
+ })
561
+
562
+
563
+ class FastIndicators:
564
+ """
565
+ 고성능 기술적 분석 지표를 계산하는 클래스입니다.
566
+
567
+ 이 클래스는 numpy 배열을 기반으로 하여 pandas 기반 구현보다 빠른 계산을 제공합니다.
568
+ 대용량 데이터나 실시간 계산이 필요한 경우에 사용하는 것이 좋습니다.
569
+ Indicators 클래스와 동일한 지표들을 제공하지만, 내부적으로 numpy를 사용하여 최적화되었습니다.
570
+
571
+ Attributes:
572
+ df (pd.DataFrame): 원본 OHLCV 데이터를 포함하는 DataFrame
573
+ """
574
+
575
+ def __init__(self, df: pd.DataFrame):
576
+ """
577
+ FastIndicators 클래스를 초기화합니다.
578
+
579
+ Args:
580
+ df (pd.DataFrame): OHLCV 데이터를 포함하는 DataFrame.
581
+ 반드시 'close', 'high', 'low', 'volume' 컬럼을 포함해야 합니다.
582
+
583
+ Raises:
584
+ KeyError: 필수 컬럼이 없는 경우 발생합니다.
585
+ """
586
+ required_columns = ['close', 'high', 'low', 'volume']
587
+ missing_columns = [col for col in required_columns if col not in df.columns]
588
+ if missing_columns:
589
+ raise KeyError(f"필수 컬럼이 누락되었습니다: {missing_columns}")
590
+
591
+ self.df = df.copy()
592
+ self.close = self.df['close'].to_numpy(dtype=float)
593
+ self.high = self.df['high'].to_numpy(dtype=float)
594
+ self.low = self.df['low'].to_numpy(dtype=float)
595
+ self.volume = self.df['volume'].to_numpy(dtype=float)
596
+ self.index = self.df.index
597
+
598
+ # -----------------------
599
+ # Helper: EMA, RMA
600
+ # -----------------------
601
+ def _ema(self, arr: np.ndarray, span: int) -> np.ndarray:
602
+ """
603
+ 지수이동평균(EMA)을 계산합니다.
604
+
605
+ Args:
606
+ arr (np.ndarray): 입력 데이터 배열
607
+ span (int): EMA 계산 기간
608
+
609
+ Returns:
610
+ np.ndarray: EMA 값이 포함된 배열
611
+ """
612
+ out = np.full_like(arr, np.nan, dtype=float)
613
+ alpha = 2 / (span + 1)
614
+ n = len(arr)
615
+ # 첫 유효값 찾기
616
+ mask = ~np.isnan(arr)
617
+ if not mask.any() or mask.sum() < span:
618
+ return out
619
+ first = np.argmax(mask) # 처음 NaN 아닌 값
620
+ out[first + span - 1] = np.nanmean(arr[first:first+span]) # 초기값: SMA
621
+ for i in range(first + span, n):
622
+ prev = out[i-1] if not np.isnan(out[i-1]) else arr[i-1]
623
+ out[i] = prev + alpha * (arr[i] - prev)
624
+ return out
625
+
626
+ def _rma(self, arr: np.ndarray, period: int) -> np.ndarray:
627
+ """
628
+ Wilder's RMA (Running Moving Average)를 계산합니다.
629
+
630
+ RMA는 Wilder의 방법을 사용한 지수이동평균으로, alpha = 1 / period 를 사용합니다.
631
+ 초기값은 첫 period 구간의 단순이동평균으로 설정됩니다.
632
+
633
+ Args:
634
+ arr (np.ndarray): 입력 데이터 배열
635
+ period (int): RMA 계산 기간
636
+
637
+ Returns:
638
+ np.ndarray: RMA 값이 포함된 배열
639
+ """
640
+ out = np.full_like(arr, np.nan, dtype=float)
641
+ n = len(arr)
642
+ if period <= 0 or n == 0:
643
+ return out
644
+
645
+ finite = np.isfinite(arr)
646
+ csum = np.cumsum(finite.astype(int))
647
+
648
+ # 첫 'period'개가 연속으로 유효한 구간의 끝 인덱스 찾기
649
+ first_idx = None
650
+ for j in range(period - 1, n):
651
+ cnt = csum[j] - (csum[j - period] if j - period >= 0 else 0)
652
+ if cnt == period:
653
+ start = j - period + 1
654
+ first_idx = j
655
+ first_avg = np.nanmean(arr[start: j + 1])
656
+ out[first_idx] = first_avg
657
+ break
658
+
659
+ if first_idx is None:
660
+ return out
661
+
662
+ alpha = 1.0 / period
663
+ for i in range(first_idx + 1, n):
664
+ x = arr[i]
665
+ if np.isnan(x):
666
+ out[i] = out[i-1] # 결측은 이전값 유지(일반적 구현)
667
+ else:
668
+ out[i] = out[i-1] + alpha * (x - out[i-1])
669
+ return out
670
+
671
+ # -----------------------
672
+ # 1. Rate of Change (ROC)
673
+ # -----------------------
674
+ def roc(self, period: int = 12) -> pd.Series:
675
+ """
676
+ Rate of Change (ROC) 지표를 계산합니다.
677
+
678
+ Args:
679
+ period (int, optional): 계산 기간. 기본값은 12입니다.
680
+
681
+ Returns:
682
+ pd.Series: ROC 값이 포함된 Series.
683
+ """
684
+ result = np.full_like(self.close, np.nan, dtype=float)
685
+ result[period:] = (self.close[period:] - self.close[:-period]) / self.close[:-period] * 100
686
+ return pd.Series(result, index=self.index)
687
+
688
+ # -----------------------
689
+ # 2. ATR (Average True Range)
690
+ # -----------------------
691
+ def atr(self, period: int = 14) -> pd.Series:
692
+ """
693
+ Average True Range (ATR) 지표를 계산합니다.
694
+
695
+ Args:
696
+ period (int, optional): 계산 기간. 기본값은 14입니다.
697
+
698
+ Returns:
699
+ pd.Series: ATR 값이 포함된 Series.
700
+ """
701
+ high, low, close = self.high, self.low, self.close
702
+ n = len(close)
703
+
704
+ tr = np.zeros(n, dtype=float)
705
+ for i in range(1, n):
706
+ tr1 = high[i] - low[i]
707
+ tr2 = abs(high[i] - close[i-1])
708
+ tr3 = abs(low[i] - close[i-1])
709
+ tr[i] = max(tr1, tr2, tr3)
710
+
711
+ atr = self._rma(tr, period)
712
+ return pd.Series(atr, index=self.index)
713
+
714
+ # -----------------------
715
+ # 3. Bollinger Bands
716
+ # -----------------------
717
+ def bollinger(self, period: int = 20, k: float = 2.0) -> pd.DataFrame:
718
+ """
719
+ Bollinger Bands 지표를 계산합니다.
720
+
721
+ Args:
722
+ period (int, optional): 이동평균 계산 기간. 기본값은 20입니다.
723
+ k (float, optional): 표준편차 배수. 기본값은 2.0입니다.
724
+
725
+ Returns:
726
+ pd.DataFrame: 볼린저 밴드 값이 포함된 DataFrame.
727
+ """
728
+ middle = pd.Series(self.close, index=self.index).rolling(window=period).mean().to_numpy()
729
+ std = pd.Series(self.close, index=self.index).rolling(window=period).std(ddof=0).to_numpy()
730
+ upper = middle + k * std
731
+ lower = middle - k * std
732
+ return pd.DataFrame({"bb_middle": middle, "bb_upper": upper, "bb_lower": lower}, index=self.index)
733
+
734
+ # -----------------------
735
+ # 4. MACD
736
+ # -----------------------
737
+ def macd(self, fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame:
738
+ """
739
+ MACD 지표를 계산합니다.
740
+
741
+ Args:
742
+ fast (int, optional): 빠른 지수이동평균 기간. 기본값은 12입니다.
743
+ slow (int, optional): 느린 지수이동평균 기간. 기본값은 26입니다.
744
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
745
+
746
+ Returns:
747
+ pd.DataFrame: MACD 값이 포함된 DataFrame.
748
+ """
749
+ ema_fast = self._ema(self.close, fast)
750
+ ema_slow = self._ema(self.close, slow)
751
+ macd_line = ema_fast - ema_slow
752
+ signal_line = self._ema(macd_line, signal)
753
+ hist = macd_line - signal_line
754
+ return pd.DataFrame({"macd": macd_line, "macd_signal": signal_line, "macd_histogram": hist}, index=self.index)
755
+
756
+ def macd_histogram_color(self, hist: pd.Series) -> pd.Series:
757
+ """
758
+ MACD 히스토그램의 색상을 결정합니다.
759
+
760
+ Args:
761
+ hist (pd.Series): MACD 히스토그램 값이 포함된 Series
762
+
763
+ Returns:
764
+ pd.Series: 색상 코드가 포함된 Series ('PI', 'PD', 'NI', 'ND')
765
+ """
766
+ hist_color = pd.Series(index=hist.index, dtype=object)
767
+ for i in range(len(hist)):
768
+ if i == 0:
769
+ hist_color.iloc[i] = 'PI' if hist.iloc[i] > 0 else 'NI'
770
+ else:
771
+ if hist.iloc[i] > 0:
772
+ hist_color.iloc[i] = 'PI' if hist.iloc[i] > hist.iloc[i - 1] else 'PD'
773
+ else:
774
+ hist_color.iloc[i] = 'NI' if hist.iloc[i] > hist.iloc[i - 1] else 'ND'
775
+ return hist_color
776
+
777
+ # -----------------------
778
+ # 5. RSI (Wilder's method)
779
+ # -----------------------
780
+ def rsi(self, period: int = 14, signal: int = 9) -> pd.DataFrame:
781
+ """
782
+ RSI 지표를 Wilder 방법으로 계산합니다.
783
+
784
+ Args:
785
+ period (int, optional): 계산 기간. 기본값은 14입니다.
786
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
787
+
788
+ Returns:
789
+ pd.DataFrame: RSI 값이 포함된 DataFrame.
790
+ """
791
+ deltas = np.diff(self.close)
792
+ gains = np.where(deltas > 0, deltas, 0.0)
793
+ losses = np.where(deltas < 0, -deltas, 0.0)
794
+
795
+ rsi = np.full_like(self.close, np.nan, dtype=float)
796
+
797
+ avg_gain = np.sum(gains[:period]) / period
798
+ avg_loss = np.sum(losses[:period]) / period
799
+ rs = avg_gain / avg_loss if avg_loss != 0 else np.inf
800
+ rsi[period] = 100 - (100 / (1 + rs))
801
+
802
+ for i in range(period + 1, len(self.close)):
803
+ avg_gain = (avg_gain * (period - 1) + gains[i - 1]) / period
804
+ avg_loss = (avg_loss * (period - 1) + losses[i - 1]) / period
805
+ rs = avg_gain / avg_loss if avg_loss != 0 else np.inf
806
+ rsi[i] = 100 - (100 / (1 + rs))
807
+
808
+ rsi_signal = self._ema(rsi, signal)
809
+ return pd.DataFrame({"rsi": rsi, "rsi_signal": rsi_signal}, index=self.index)
810
+
811
+ # -----------------------
812
+ # 6. RSI (Cutler's method)
813
+ # -----------------------
814
+ def rsi_cutler(self, period: int = 14, signal: int = 9) -> pd.DataFrame:
815
+ """
816
+ RSI 지표를 Cutler 방법으로 계산합니다.
817
+
818
+ Args:
819
+ period (int, optional): 계산 기간. 기본값은 14입니다.
820
+ signal (int, optional): 시그널선 기간. 기본값은 9입니다.
821
+
822
+ Returns:
823
+ pd.DataFrame: RSI 값이 포함된 DataFrame.
824
+ """
825
+ deltas = np.diff(self.close)
826
+ gains = np.where(deltas > 0, deltas, 0.0)
827
+ losses = np.where(deltas < 0, -deltas, 0.0)
828
+
829
+ avg_gain = np.convolve(gains, np.ones(period)/period, mode='valid')
830
+ avg_loss = np.convolve(losses, np.ones(period)/period, mode='valid')
831
+
832
+ rs = avg_gain / avg_loss
833
+ rsi = 100 - (100 / (1 + rs))
834
+
835
+ result = np.full_like(self.close, np.nan, dtype=float)
836
+ result[period:] = rsi
837
+ rsi_signal = self._ema(result, signal)
838
+ return pd.DataFrame({"rsi_cutler": result, "rsi_cutler_signal": rsi_signal}, index=self.index)
839
+
840
+ # -----------------------
841
+ # 7. OBV (On-Balance Volume)
842
+ # -----------------------
843
+ def obv(self) -> pd.Series:
844
+ """
845
+ OBV 지표를 계산합니다.
846
+
847
+ Returns:
848
+ pd.Series: OBV 값이 포함된 Series.
849
+ """
850
+ diff = np.diff(self.close)
851
+ direction = np.sign(diff)
852
+ direction = np.insert(direction, 0, 0)
853
+ obv = np.cumsum(direction * self.volume)
854
+ return pd.Series(obv, index=self.index)
855
+
856
+ # -----------------------
857
+ # 8. ADX (Average Directional Index)
858
+ # -----------------------
859
+ def adx(self, period: int = 14) -> pd.DataFrame:
860
+ """
861
+ ADX 지표를 계산합니다.
862
+
863
+ Args:
864
+ period (int, optional): 계산 기간. 기본값은 14입니다.
865
+
866
+ Returns:
867
+ pd.DataFrame: ADX 값이 포함된 DataFrame.
868
+ """
869
+ high, low, close = self.high, self.low, self.close
870
+ n = len(close)
871
+
872
+ # Up/Down move
873
+ up_move = np.empty(n); up_move[0] = np.nan; up_move[1:] = high[1:] - high[:-1]
874
+ down_move = np.empty(n); down_move[0] = np.nan; down_move[1:] = low[:-1] - low[1:]
875
+
876
+ plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
877
+ minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
878
+ plus_dm[0] = np.nan; minus_dm[0] = np.nan # 선행 NaN 유지
879
+
880
+ # True Range
881
+ prev_close = np.empty_like(close)
882
+ prev_close[0] = np.nan
883
+ prev_close[1:] = close[:-1]
884
+
885
+ tr = np.maximum.reduce([
886
+ high - low,
887
+ np.abs(high - prev_close),
888
+ np.abs(low - prev_close)
889
+ ])
890
+
891
+ # Wilder smoothing (RMA)
892
+ atr = self._rma(tr, period)
893
+ plus_dm_sm = self._rma(plus_dm, period)
894
+ minus_dm_sm = self._rma(minus_dm, period)
895
+
896
+ # DI
897
+ with np.errstate(divide='ignore', invalid='ignore'):
898
+ plus_di = 100.0 * (plus_dm_sm / atr)
899
+ minus_di = 100.0 * (minus_dm_sm / atr)
900
+
901
+ den = plus_di + minus_di
902
+ dx = 100.0 * np.abs(plus_di - minus_di) / den
903
+ dx[~np.isfinite(dx)] = np.nan # 분모 0 등 처리
904
+
905
+ # ADX (RMA of DX)
906
+ adx = self._rma(dx, period)
907
+
908
+ return pd.DataFrame(
909
+ {"plus_di": plus_di, "minus_di": minus_di, "adx": adx},
910
+ index=self.index
911
+ )
912
+
913
+ # -----------------------
914
+ # 9. Stochastic RSI
915
+ # -----------------------
916
+ def stoch_rsi(self, period: int = 14, smooth_k: int = 3, smooth_d: int = 3) -> pd.DataFrame:
917
+ """
918
+ Stochastic RSI 지표를 계산합니다.
919
+
920
+ Args:
921
+ period (int, optional): RSI 계산 기간. 기본값은 14입니다.
922
+ smooth_k (int, optional): %K 스무딩 기간. 기본값은 3입니다.
923
+ smooth_d (int, optional): %D 스무딩 기간. 기본값은 3입니다.
924
+
925
+ Returns:
926
+ pd.DataFrame: Stochastic RSI 값이 포함된 DataFrame.
927
+ """
928
+ rsi_vals = self.rsi(period=period)["rsi"].to_numpy()
929
+ min_rsi = pd.Series(rsi_vals).rolling(window=period).min().to_numpy()
930
+ max_rsi = pd.Series(rsi_vals).rolling(window=period).max().to_numpy()
931
+ with np.errstate(divide='ignore', invalid='ignore'):
932
+ stoch_rsi = 100 * (rsi_vals - min_rsi) / (max_rsi - min_rsi)
933
+ stoch_rsi[~np.isfinite(stoch_rsi)] = np.nan # 분모 0 등 처리
934
+ k = pd.Series(stoch_rsi).rolling(window=smooth_k).mean().to_numpy()
935
+ d = pd.Series(k).rolling(window=smooth_d).mean().to_numpy()
936
+ return pd.DataFrame({"stochrsi": stoch_rsi, "stochrsi_%K": k, "stochrsi_%D": d}, index=self.index)
937
+
938
+ # -----------------------
939
+ # 10. Williams %R
940
+ # -----------------------
941
+ def williams_r(self, period: int = 14) -> pd.Series:
942
+ """
943
+ Williams %R 지표를 계산합니다.
944
+
945
+ Args:
946
+ period (int, optional): 계산 기간. 기본값은 14입니다.
947
+
948
+ Returns:
949
+ pd.Series: Williams %R 값이 포함된 Series.
950
+ """
951
+ highest_high = pd.Series(self.high).rolling(window=period).max().to_numpy()
952
+ lowest_low = pd.Series(self.low).rolling(window=period).min().to_numpy()
953
+ with np.errstate(divide='ignore', invalid='ignore'):
954
+ wr = (highest_high - self.close) / (highest_high - lowest_low) * -100
955
+ wr[~np.isfinite(wr)] = np.nan # 분모 0 등 처리
956
+ return pd.Series(wr, index=self.index)
957
+
958
+ # -----------------------
959
+ # 11. Stochastic Oscillator
960
+ # -----------------------
961
+ def stochastic(self, period: int = 14, smooth_k: int = 3, smooth_d: int = 3) -> pd.DataFrame:
962
+ """
963
+ Stochastic Oscillator 지표를 계산합니다.
964
+
965
+ Args:
966
+ period (int, optional): 계산 기간. 기본값은 14입니다.
967
+ smooth_k (int, optional): %K 스무딩 기간. 기본값은 3입니다.
968
+ smooth_d (int, optional): %D 스무딩 기간. 기본값은 3입니다.
969
+
970
+ Returns:
971
+ pd.DataFrame: Stochastic Oscillator 값이 포함된 DataFrame.
972
+ """
973
+ low_min = pd.Series(self.low).rolling(window=period).min().to_numpy()
974
+ high_max = pd.Series(self.high).rolling(window=period).max().to_numpy()
975
+ k_fast = 100 * (self.close - low_min) / (high_max - low_min)
976
+ k_slow = pd.Series(k_fast).rolling(window=smooth_k).mean().to_numpy()
977
+ d = pd.Series(k_slow).rolling(window=smooth_d).mean().to_numpy()
978
+ return pd.DataFrame({"stoch_fast_%K": k_fast, "stoch_%K": k_slow, "stoch_%D": d}, index=self.index)
979
+
980
+ # -----------------------
981
+ # 12. Ichimoku Kinko Hyo
982
+ # -----------------------
983
+ def ichimoku(self, tenkan_period: int = 9, kijun_period: int = 26,
984
+ senkou_period: int = 52, displacement: int = 26) -> pd.DataFrame:
985
+ """
986
+ Ichimoku Kinko Hyo (일목균형표) 지표를 계산합니다.
987
+
988
+ Args:
989
+ tenkan_period (int, optional): 전환선 기간. 기본값은 9입니다.
990
+ kijun_period (int, optional): 기준선 기간. 기본값은 26입니다.
991
+ senkou_period (int, optional): 선행스팬B 기간. 기본값은 52입니다.
992
+ displacement (int, optional): 선행스팬 이동 기간. 기본값은 26입니다.
993
+
994
+ Returns:
995
+ pd.DataFrame: Ichimoku 값이 포함된 DataFrame.
996
+ """
997
+ tenkan = (pd.Series(self.high).rolling(window=tenkan_period).max() +
998
+ pd.Series(self.low).rolling(window=tenkan_period).min()) / 2
999
+ kijun = (pd.Series(self.high).rolling(window=kijun_period).max() +
1000
+ pd.Series(self.low).rolling(window=kijun_period).min()) / 2
1001
+ senkou_a = ((tenkan + kijun) / 2).shift(displacement - 1)
1002
+ senkou_b = ((pd.Series(self.high).rolling(window=senkou_period).max() +
1003
+ pd.Series(self.low).rolling(window=senkou_period).min()) / 2).shift(displacement - 1)
1004
+ chikou = pd.Series(self.close).shift(-displacement)
1005
+ return pd.DataFrame({
1006
+ "tenkan": tenkan.to_numpy(),
1007
+ "kijun": kijun.to_numpy(),
1008
+ "senkou_a": senkou_a.to_numpy(),
1009
+ "senkou_b": senkou_b.to_numpy(),
1010
+ "chikou": chikou.to_numpy()
1011
+ }, index=self.index)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes