pyqqq 0.12.166__py3-none-any.whl → 0.12.167__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.

Potentially problematic release.


This version of pyqqq might be problematic. Click here for more details.

pyqqq/backtest/broker.py CHANGED
@@ -144,7 +144,7 @@ class BaseBroker(ABC):
144
144
  raise NotImplementedError
145
145
 
146
146
  @abstractmethod
147
- def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int | Decimal = 0) -> str:
147
+ def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int | Decimal = 0, exchange: OrderExchange = OrderExchange.KRX) -> str:
148
148
  """새로운 주문을 생성합니다.
149
149
 
150
150
  Args:
@@ -153,6 +153,7 @@ class BaseBroker(ABC):
153
153
  quantity (int): 주문 수량
154
154
  order_type (OrderType, optional): 주문 타입 (기본값: MARKET)
155
155
  price (int, optional): 지정가 주문 시 주문 가격 (기본값: 0)
156
+ exchange (OrderExchange, optional): 거래소 (기본값: KRX)
156
157
 
157
158
  Returns:
158
159
  str: 생성된 주문 번호
@@ -164,7 +165,7 @@ class BaseBroker(ABC):
164
165
  raise NotImplementedError
165
166
 
166
167
  @abstractmethod
167
- def update_order(self, org_order_no: str, order_type: OrderType, price: int | Decimal, quantity: int = 0) -> str:
168
+ def update_order(self, org_order_no: str, order_type: OrderType, price: int | Decimal, quantity: int = 0, exchange: OrderExchange = OrderExchange.KRX) -> str:
168
169
  """기존 주문을 수정합니다.
169
170
 
170
171
  Args:
@@ -288,21 +289,23 @@ class TradingBroker(BaseBroker):
288
289
  def get_positions(self):
289
290
  return self.trading_api.get_positions()
290
291
 
291
- def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int | Decimal = 0):
292
- self.logger.debug(f"create_order: {asset_code} {side} {quantity} {order_type} {price}")
293
- return self.trading_api.create_order(asset_code, side, quantity, order_type, price)
292
+ def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int | Decimal = 0, exchange: OrderExchange = OrderExchange.KRX) -> str:
293
+ self.logger.debug(f"create_order: {asset_code} {side} {quantity} {order_type} {price} {exchange}")
294
+ return self.trading_api.create_order(asset_code, side, quantity, order_type, price, exchange)
294
295
 
295
- def update_order(self, org_order_no: str, order_type: OrderType, price: int | Decimal, quantity: int = 0):
296
+ def update_order(self, org_order_no: str, order_type: OrderType, price: int | Decimal, quantity: int = 0, exchange: OrderExchange = OrderExchange.KRX):
296
297
  if isinstance(self.trading_api, KISSimpleOverseasStock):
297
298
  order = self._get_pending_order(org_order_no)
298
299
  if order is not None:
299
300
  self.logger.debug(f"update_order: ({order.asset_code}) {org_order_no} {order_type} {price} {quantity}")
301
+ # 해외주식의 경우 asset_code와 org_order_no를 사용하여 주문을 업데이트
302
+ # 또한 exchange 정보가 들어가지 않음
300
303
  return self.trading_api.update_order(order.asset_code, org_order_no, order_type, price, quantity)
301
304
  else:
302
305
  raise ValueError(f"order not found: {org_order_no}")
303
306
  else:
304
307
  self.logger.debug(f"update_order: {org_order_no} {order_type} {price} {quantity}")
305
- return self.trading_api.update_order(org_order_no, order_type, price, quantity)
308
+ return self.trading_api.update_order(org_order_no, order_type, price, quantity, exchange)
306
309
 
307
310
  def cancel_order(self, order_no: str, quantity: int = 0):
308
311
  if isinstance(self.trading_api, KISSimpleOverseasStock):
@@ -661,7 +664,8 @@ class MockBroker(BaseBroker):
661
664
  positions = [p for p in self.positions if p.quantity > 0]
662
665
  return positions
663
666
 
664
- def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int = 0):
667
+ def create_order(self, asset_code: str, side: OrderSide, quantity: int, order_type: OrderType = OrderType.MARKET, price: int = 0, exchange: OrderExchange = OrderExchange.KRX) -> str:
668
+ # TODO: exchange에 따라 다른 봉을 보는 처리가 추가로 되어야 함
665
669
  price = self.get_price(asset_code) if order_type == OrderType.MARKET else price
666
670
  self.logger.info(f"CREATE ORDER: {self._get_asset_name(asset_code)} side:{side} price:{price} quantity:{quantity} order_type:{order_type}")
667
671
  order_no = str(self.next_order_no)
@@ -687,6 +691,7 @@ class MockBroker(BaseBroker):
687
691
  pending_quantity=quantity,
688
692
  order_type=order_type,
689
693
  order_time=self.clock.now(),
694
+ exchange=exchange,
690
695
  )
691
696
 
692
697
  self.pending_orders.append(order)
@@ -694,7 +699,7 @@ class MockBroker(BaseBroker):
694
699
 
695
700
  return order_no
696
701
 
697
- def update_order(self, org_order_no: str, order_type: OrderType, price: int, quantity: int = 0):
702
+ def update_order(self, org_order_no: str, order_type: OrderType, price: int, quantity: int = 0, exchange: OrderExchange = OrderExchange.KRX):
698
703
  order = next((order for order in self.pending_orders if order.order_no == org_order_no), None)
699
704
  if order is None:
700
705
  raise Exception(f"order not found: {org_order_no}")
@@ -727,6 +732,7 @@ class MockBroker(BaseBroker):
727
732
  pending_quantity=quantity,
728
733
  order_type=order_type,
729
734
  order_time=self.clock.now(),
735
+ exchange=exchange,
730
736
  )
731
737
 
732
738
  order.pending_quantity -= quantity
@@ -788,6 +794,7 @@ class MockBroker(BaseBroker):
788
794
  # 강제로 1분 차이를 만들어 줌
789
795
  before_time = current_time - dtm.timedelta(seconds=60)
790
796
 
797
+ # TODO: exchange에 따라 다른 봉을 보는 처리가 추가로 되어야 함
791
798
  df = self.get_minute_price(order.asset_code)
792
799
  if df.empty:
793
800
  self.logger.warning(f"ORDER CANCELED: {order.asset_code} minute data is empty")
pyqqq/data/daily.py CHANGED
@@ -1,13 +1,15 @@
1
+ import datetime
1
2
  from typing import Dict, List, Optional, Union
3
+
4
+ import pandas as pd
5
+ import pytz
6
+
7
+ import pyqqq.config as c
2
8
  from pyqqq.datatypes import DataExchange
3
- from pyqqq.utils.array import chunk
4
9
  from pyqqq.utils.api_client import raise_for_status, send_request
10
+ from pyqqq.utils.array import chunk
5
11
  from pyqqq.utils.local_cache import DiskCacheManager
6
12
  from pyqqq.utils.logger import get_logger
7
- import datetime as dtm
8
- import pandas as pd
9
- import pyqqq.config as c
10
- import pytz
11
13
 
12
14
  logger = get_logger(__name__)
13
15
  dailyCache = DiskCacheManager("daily_cache")
@@ -15,9 +17,9 @@ dailyCache = DiskCacheManager("daily_cache")
15
17
 
16
18
  @dailyCache.memoize()
17
19
  def get_all_ohlcv_for_date(
18
- date: dtm.date,
20
+ date: datetime.date,
19
21
  adjusted: bool = True,
20
- exchange: Union[DataExchange, str] = DataExchange.KRX,
22
+ exchange: Union[str, DataExchange] = "KRX",
21
23
  ) -> pd.DataFrame:
22
24
  """
23
25
  주어진 날짜에 대한 모든 주식의 OHLCV(Open, High, Low, Close, Volume) 데이터를 조회합니다.
@@ -29,9 +31,9 @@ def get_all_ohlcv_for_date(
29
31
  NXT: 2025년 3월 4일 데이터 부터 조회 가능합니다.
30
32
 
31
33
  Args:
32
- date (dtm.date): 조회할 날짜.
34
+ date (datetime.date): 조회할 날짜.
33
35
  adjusted (bool): 수정주가 여부. 기본값은 True.
34
- exchange (DataExchange): 거래소. 기본값은 KRX.
36
+ exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX.
35
37
 
36
38
  Returns:
37
39
  pd.DataFrame: OHLCV 데이터를 포함하는 DataFrame. 'code' 컬럼은 DataFrame의 인덱스로 설정됩니다.
@@ -51,7 +53,7 @@ def get_all_ohlcv_for_date(
51
53
  HTTPError: API 요청이 실패했을 때 발생.
52
54
 
53
55
  Examples:
54
- >>> ohlcv_data = get_all_ohlcv_for_date(dtm.date(2023, 5, 8))
56
+ >>> ohlcv_data = get_all_ohlcv_for_date(datetime.date(2023, 5, 8))
55
57
  >>> print(ohlcv_data)
56
58
  open high low close volume value diff diff_rate
57
59
  code
@@ -62,17 +64,17 @@ def get_all_ohlcv_for_date(
62
64
  000075 54800 54900 54800 54800 177 9702400 -100 -0.18
63
65
  ...
64
66
  """
65
- if isinstance(date, dtm.datetime):
67
+ if isinstance(date, datetime.datetime):
66
68
  date = date.date()
67
69
 
68
- exchange = _validate_exchange(exchange)
70
+ exchange = DataExchange.validate(exchange)
69
71
  url = f"{c.PYQQQ_API_URL}/domestic-stock/ohlcv/daily/all/{date}"
70
72
  r = send_request(
71
73
  "GET",
72
74
  url,
73
75
  params={
74
76
  "adjusted": "true" if adjusted else "false",
75
- "current_date": dtm.date.today(),
77
+ "current_date": datetime.date.today(),
76
78
  "exchange": exchange.value,
77
79
  },
78
80
  )
@@ -96,11 +98,11 @@ def get_all_ohlcv_for_date(
96
98
 
97
99
  def get_ohlcv_by_codes_for_period(
98
100
  codes: List[str],
99
- start_date: dtm.date,
100
- end_date: Optional[dtm.date] = None,
101
+ start_date: datetime.date,
102
+ end_date: Optional[datetime.date] = None,
101
103
  adjusted: bool = True,
102
104
  ascending: bool = False,
103
- exchange: Union[DataExchange, str] = DataExchange.KRX,
105
+ exchange: Union[str, DataExchange] = "KRX",
104
106
  ) -> Dict[str, pd.DataFrame]:
105
107
  """
106
108
  지정된 코드 리스트와 기간에 대한 OHLCV 데이터를 조회합니다.
@@ -114,10 +116,11 @@ def get_ohlcv_by_codes_for_period(
114
116
 
115
117
  Args:
116
118
  codes (List[str]): 조회할 주식 코드들의 리스트.
117
- start_date (dtm.date): 조회할 기간의 시작 날짜.
118
- end_date (Optional[dtm.date]): 조회할 기간의 종료 날짜. 지정하지 않으면 최근 거래일 까지 조회됩니다.
119
+ start_date (datetime.date): 조회할 기간의 시작 날짜.
120
+ end_date (Optional[datetime.date]): 조회할 기간의 종료 날짜. 지정하지 않으면 최근 거래일 까지 조회됩니다.
119
121
  adjusted (bool): 수정주가 여부. 기본값은 True.
120
122
  ascending (bool): 날짜 오름차순 여부. 기본값은 False.
123
+ exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX.
121
124
 
122
125
  Returns:
123
126
  dict: 주식 코드를 키로 하고, 해당 코드의 OHLCV 데이터를 포함하는 DataFrame을 값으로 하는 딕셔너리.
@@ -137,7 +140,7 @@ def get_ohlcv_by_codes_for_period(
137
140
  HTTPError: API 요청이 실패했을 때 발생.
138
141
 
139
142
  Examples:
140
- >>> dfs = get_ohlcv_by_codes_for_period(['005930', '319640'], dtm.date(2024, 5, 7), dtm.date(2024, 5, 9))
143
+ >>> dfs = get_ohlcv_by_codes_for_period(['005930', '319640'], datetime.date(2024, 5, 7), datetime.date(2024, 5, 9))
141
144
  >>> print(dfs)
142
145
  {'319640': open high low close volume value diff diff_rate
143
146
  date
@@ -150,7 +153,7 @@ def get_ohlcv_by_codes_for_period(
150
153
  2024-05-08 80800 81400 80500 81300 12960682 1050108654400 0 0.00
151
154
  2024-05-07 79600 81300 79400 81300 26238868 2112619288066 3700 4.77}
152
155
  """
153
- exchange = _validate_exchange(exchange)
156
+ exchange = DataExchange.validate(exchange)
154
157
  tz = pytz.timezone("Asia/Seoul")
155
158
  chunks = chunk(codes, 20)
156
159
  result = {}
@@ -161,7 +164,7 @@ def get_ohlcv_by_codes_for_period(
161
164
  "codes": ",".join(asset_codes),
162
165
  "start_date": start_date,
163
166
  "adjusted": "true" if adjusted else "false",
164
- "current_date": dtm.date.today(),
167
+ "current_date": datetime.date.today(),
165
168
  "exchange": exchange.value,
166
169
  }
167
170
  if end_date is not None:
@@ -180,7 +183,7 @@ def get_ohlcv_by_codes_for_period(
180
183
  dt = row[0]
181
184
  if dt[-1] == "Z":
182
185
  dt = dt[:-1] + "+00:00"
183
- dt = dtm.datetime.fromisoformat(dt).astimezone(tz).replace(tzinfo=None)
186
+ dt = datetime.datetime.fromisoformat(dt).astimezone(tz).replace(tzinfo=None)
184
187
  row[0] = dt
185
188
 
186
189
  rows.reverse()
@@ -192,13 +195,3 @@ def get_ohlcv_by_codes_for_period(
192
195
  result[code] = df
193
196
 
194
197
  return result
195
-
196
-
197
- def _validate_exchange(exchange: Union[str, DataExchange]) -> DataExchange:
198
- if isinstance(exchange, str):
199
- assert exchange in [e.value for e in DataExchange], "지원하지 않는 거래소 코드입니다."
200
- exchange = DataExchange(exchange)
201
- else:
202
- assert exchange in DataExchange, "지원하지 않는 거래소 코드입니다."
203
-
204
- return exchange
pyqqq/data/domestic.py CHANGED
@@ -1,13 +1,16 @@
1
+ import datetime as dtm
2
+ from typing import List, Optional, Union
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+
7
+ import pyqqq.config as c
8
+ from pyqqq.datatypes import DataExchange
1
9
  from pyqqq.utils.api_client import raise_for_status, send_request
2
10
  from pyqqq.utils.array import chunk
3
- from pyqqq.utils.compute import quantize_adjusted_price
11
+ from pyqqq.utils.compute import get_krx_tick_size, quantize_adjusted_price
4
12
  from pyqqq.utils.local_cache import DiskCacheManager
5
13
  from pyqqq.utils.market_schedule import get_last_trading_day, get_market_schedule
6
- from typing import List, Optional, Union
7
- import datetime as dtm
8
- import pandas as pd
9
- import numpy as np
10
- import pyqqq.config as c
11
14
 
12
15
  domesticCache = DiskCacheManager("domestic_cache")
13
16
 
@@ -227,19 +230,27 @@ def _isoformat_to_readable(isodate: str) -> str:
227
230
 
228
231
 
229
232
  @domesticCache.memoize()
230
- def get_tickers(date: Optional[dtm.date] = None, market: Optional[str] = None, adjusted: Optional[bool] = True):
233
+ def get_tickers(
234
+ date: Optional[dtm.date] = None,
235
+ market: Optional[str] = None,
236
+ adjusted: Optional[bool] = True,
237
+ exchange: Union[str, DataExchange] = "KRX",
238
+ ):
231
239
  """
232
240
  주어진 날짜와 시장에 따른 주식 종목 코드와 관련 정보를 조회합니다.
233
241
 
234
242
  이 함수는 지정된 날짜(기본값은 오늘)와 선택적 시장('KOSPI', 'KOSDAQ')에 대한 주식 종목 코드와 추가 정보를 API를 통해 요청합니다.
235
243
  반환된 정보는 pandas DataFrame 형태로 제공되며, 데이터가 없는 경우 빈 DataFrame을 반환합니다. DataFrame은 'code'를 인덱스로 사용합니다.
236
244
 
245
+ KRX 거래소에서는 거래정지 종목 등이 포함되어 있으나, NXT 거래소에서는 거래정지 종목이 제외되어 있습니다.
246
+
237
247
  2018년 1월 1일 데이터 부터 조회 가능합니다. 수정주가는 소수점 첫째 자리에서 반올림합니다.
238
248
 
239
249
  Args:
240
250
  date (Optional[dtm.date]): 조회할 날짜. 기본값은 현재 날짜입니다.
241
- market (Optional[str]): 조회할 시장. 'KOSPI' 또는 'KOSDAQ' 중 선택할 수 있습니다.
251
+ market (Optional[str]): 조회할 시장. 'KOSPI' 또는 'KOSDAQ' 중 선택할 수 있습니다. 기본값은 None 이며, 모든 시장을 조회합니다.
242
252
  adjusted (Optional[bool]): 수정주가 여부. 기본값은 True.
253
+ exchange (Union[str, DataExchange]): 조회할 거래소. 'KRX' 또는 'NXT' 중 선택할 수 있습니다. 기본값은 'KRX' 입니다.
243
254
 
244
255
  Returns:
245
256
  pd.DataFrame: 주식 종목 코드와 관련 정보를 포함하는 DataFrame. 'code' 컬럼은 인덱스로 설정됩니다.
@@ -274,13 +285,16 @@ def get_tickers(date: Optional[dtm.date] = None, market: Optional[str] = None, a
274
285
  "KOSDAQ",
275
286
  ], "market은 'KOSPI' 또는 'KOSDAQ'이어야 합니다."
276
287
 
288
+ # TODO: NXT 거래소 2025년 3월 4일 이전 요청 시 에러 발생
289
+ exchange = DataExchange.validate(exchange)
290
+
277
291
  if date is None:
278
292
  date = dtm.date.today()
279
293
  schedule = get_market_schedule(date)
280
294
  if schedule.full_day_closed:
281
295
  date = get_last_trading_day(date)
282
296
 
283
- return _get_tickers(date, market, adjusted)
297
+ return _get_tickers(date, market, adjusted, exchange)
284
298
 
285
299
 
286
300
  def _get_tickers_check_not_expected_res(res):
@@ -291,7 +305,7 @@ def _get_tickers_check_not_expected_res(res):
291
305
 
292
306
 
293
307
  @domesticCache.memoize(not_expected_res=_get_tickers_check_not_expected_res)
294
- def _get_tickers(date: dtm.date, market: str = None, adjusted: bool = True):
308
+ def _get_tickers(date: dtm.date, market: str = None, adjusted: bool = True, exchange: DataExchange = DataExchange.KRX):
295
309
  """
296
310
  get_tickers 함수의 실제 구현부.
297
311
  기존 함수에선 date가 None이어도 정상적으로 돌아서 메모이제이션 하기 좋지않았음.
@@ -300,6 +314,7 @@ def _get_tickers(date: dtm.date, market: str = None, adjusted: bool = True):
300
314
  params = {
301
315
  "adjusted": "true" if adjusted else "false",
302
316
  "current_date": dtm.date.today(),
317
+ "exchange": exchange.value,
303
318
  }
304
319
  if market:
305
320
  params["market"] = market
@@ -333,50 +348,17 @@ def _get_tickers(date: dtm.date, market: str = None, adjusted: bool = True):
333
348
  def calculate_price_limit(row):
334
349
  price = row["reference_price"]
335
350
  market = row["market"]
351
+ etf_etn = row["type"] == "ETF" or row["type"] == "ETN"
352
+
353
+ tick_size = get_krx_tick_size(price, etf_etn, market, date)
336
354
 
337
- # 상한가 계산 (1.3배)
355
+ # 상한가 계산 (1.3배) 후 호가단위로 절삭 (내림)
338
356
  upper = price * 1.3
339
- # 하한가 계산 (0.7배)
340
- lower = price * 0.7
357
+ upper = int(upper // tick_size * tick_size)
341
358
 
342
- # 호가단위 계산
343
- def get_tick_size(price, market):
344
- if market == "KOSPI":
345
- if price < 1000:
346
- return 1
347
- elif price < 5000:
348
- return 5
349
- elif price < 10000:
350
- return 10
351
- elif price < 50000:
352
- return 50
353
- elif price < 100000:
354
- return 100
355
- elif price < 500000:
356
- return 500
357
- else:
358
- return 1000
359
- elif market == "KOSDAQ":
360
- if price < 1000:
361
- return 1
362
- elif price < 5000:
363
- return 5
364
- elif price < 10000:
365
- return 10
366
- elif price < 50000:
367
- return 50
368
- else:
369
- return 100
370
- return 1 # 기본값
371
-
372
- # 호가단위로 절삭
373
- upper_tick = get_tick_size(upper, market)
374
- lower_tick = get_tick_size(lower, market)
375
-
376
- # 호가단위로 절삭 (내림)
377
- upper = int(upper // upper_tick * upper_tick)
378
- # 호가단위로 절삭 (올림)
379
- lower = int((lower + lower_tick - 0.01) // lower_tick * lower_tick)
359
+ # 하한가 계산 (0.7배) 후 호가단위로 절삭 (올림)
360
+ lower = price * 0.7
361
+ lower = int((lower + tick_size - 0.01) // tick_size * tick_size)
380
362
 
381
363
  return pd.Series([upper, lower])
382
364
 
pyqqq/data/minutes.py CHANGED
@@ -1,14 +1,15 @@
1
+ import datetime
1
2
  from typing import Dict, Union
2
- from pyqqq.datatypes import DataExchange
3
- from pyqqq.utils.api_client import raise_for_status, send_request
4
- from pyqqq.utils.local_cache import DiskCacheManager
5
- from pyqqq.utils.logger import get_logger
6
- import datetime as dtm
3
+
7
4
  import numpy as np
8
5
  import pandas as pd
9
- import pyqqq.config as c
10
6
  import pytz
11
7
 
8
+ import pyqqq.config as c
9
+ from pyqqq.datatypes import DataExchange
10
+ from pyqqq.utils.api_client import raise_for_status, send_request
11
+ from pyqqq.utils.local_cache import DiskCacheManager
12
+ from pyqqq.utils.logger import get_logger
12
13
 
13
14
  logger = get_logger(__name__)
14
15
  minuteCache = DiskCacheManager("minute_cache")
@@ -16,10 +17,10 @@ minuteCache = DiskCacheManager("minute_cache")
16
17
 
17
18
  @minuteCache.memoize()
18
19
  def get_all_minute_data(
19
- time: dtm.datetime,
20
+ time: datetime.datetime,
20
21
  source: str = "ebest",
21
22
  adjusted: bool = True,
22
- exchange: Union[DataExchange, str] = DataExchange.KRX,
23
+ exchange: Union[str, DataExchange] = "KRX",
23
24
  ) -> pd.DataFrame:
24
25
  """
25
26
  모든 종목의 분봉 데이터를 반환합니다.
@@ -29,10 +30,10 @@ def get_all_minute_data(
29
30
  NXT 거래소 데이터의 조회 가능 시작일은 데이터 소스에 따라 다릅니다. kis는 2025년 3월 4일부터, ebest는 2025년 5월 12일부터 데이터를 조회할 수 있습니다.
30
31
 
31
32
  Args:
32
- time (dtm.datetime): 조회할 시간
33
+ time (datetime.datetime): 조회할 시간
33
34
  source (str): 데이터를 검색할 API. 'ebest' 또는 'kis'를 지정할 수 있습니다. 기본값은 'ebest'입니다.
34
35
  adjusted (bool): 수정주가 여부. 기본값은 True.
35
- exchange (Union[DataExchange, str]): 거래소. 기본값은 KRX.
36
+ exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX.
36
37
 
37
38
  Returns:
38
39
  pd.DataFrame: 모든 종목의 분봉 데이터가 포함된 pandas DataFrame.
@@ -63,7 +64,7 @@ def get_all_minute_data(
63
64
  - msvolumetm (int): 시간별매수체결량
64
65
 
65
66
  Examples:
66
- >>> df = get_all_minute_data(dtm.datetime(2024, 5, 2, 15, 30))
67
+ >>> df = get_all_minute_data(datetime.datetime(2024, 5, 2, 15, 30))
67
68
  >>> print(df)
68
69
  time open high low ... totofferrem totbidrem mdvolumetm msvolumetm
69
70
  code ...
@@ -76,13 +77,13 @@ def get_all_minute_data(
76
77
  [5 rows x 23 columns]
77
78
  """
78
79
  tz = pytz.timezone("Asia/Seoul")
79
- exchange = _validate_exchange(exchange)
80
+ exchange = DataExchange.validate(exchange)
80
81
 
81
82
  url = f"{c.PYQQQ_API_URL}/domestic-stock/ohlcv/minutes/all/{time.date()}/{time.strftime('%H%M')}"
82
83
  params = {
83
84
  "brokerage": source,
84
85
  "adjusted": "true" if adjusted else "false",
85
- "current_date": dtm.date.today(),
86
+ "current_date": datetime.date.today(),
86
87
  "exchange": exchange.value,
87
88
  }
88
89
 
@@ -94,7 +95,7 @@ def get_all_minute_data(
94
95
  rows = r.json()
95
96
  for data in rows:
96
97
  time = data["time"].replace("Z", "+00:00")
97
- time = dtm.datetime.fromisoformat(time).astimezone(tz).replace(tzinfo=None)
98
+ time = datetime.datetime.fromisoformat(time).astimezone(tz).replace(tzinfo=None)
98
99
  data["time"] = time
99
100
 
100
101
  df = pd.DataFrame(rows)
@@ -126,13 +127,13 @@ def get_all_minute_data(
126
127
 
127
128
  @minuteCache.memoize()
128
129
  def get_all_day_data(
129
- date: dtm.date,
130
+ date: datetime.date,
130
131
  codes: list[str] | str,
131
- period: dtm.timedelta = dtm.timedelta(minutes=1),
132
+ period: datetime.timedelta = datetime.timedelta(minutes=1),
132
133
  source: str = "ebest",
133
134
  adjusted: bool = True,
134
135
  ascending: bool = True,
135
- exchange: Union[DataExchange, str] = DataExchange.KRX,
136
+ exchange: Union[str, DataExchange] = "KRX",
136
137
  ) -> dict[str, pd.DataFrame] | pd.DataFrame:
137
138
  """
138
139
  지정된 날짜에 대해 하나 이상의 주식 코드에 대한 전체 분별 OHLCV(시가, 고가, 저가, 종가, 거래량) 데이터를 검색하여 반환합니다.
@@ -142,13 +143,13 @@ def get_all_day_data(
142
143
  NXT 거래소 데이터의 조회 가능 시작일은 데이터 소스에 따라 다릅니다. kis는 2025년 3월 4일부터, ebest는 2025년 5월 12일부터 데이터를 조회할 수 있습니다.
143
144
 
144
145
  Args:
145
- date (dtm.date): 데이터를 검색할 날짜.
146
+ date (datetime.date): 데이터를 검색할 날짜.
146
147
  codes (list[str]): 조회할 주식 코드들의 리스트. 최대 20개까지 지정할 수 있습니다.
147
- period (dtm.timedelta, optional): 반환된 데이터의 시간 간격. 기본값은 1분입니다. 30초 이상의 값을 30초간격으로 지정할 수 있습니다.
148
+ period (datetime.timedelta, optional): 반환된 데이터의 시간 간격. 기본값은 1분입니다. 30초 이상의 값을 30초간격으로 지정할 수 있습니다.
148
149
  source (str, optional): 데이터를 검색할 API. 'ebest' 또는 'kis'를 지정할 수 있습니다. 기본값은 'ebest'입니다.
149
150
  adjusted (bool): 수정주가 여부. 기본값은 True.
150
151
  ascending (bool): 오름차순 여부. 기본값은 True.
151
- exchange (Union[DataExchange, str]): 거래소. 기본값은 KRX.
152
+ exchange (Union[str, DataExchange]): 거래소. 기본값은 KRX.
152
153
 
153
154
  Returns:
154
155
  dict[str, pd.DataFrame]: 주식 코드를 키로 하고, 해당 주식의 일일 OHLCV 데이터가 포함된 pandas DataFrame을 값으로 하는 딕셔너리.
@@ -183,7 +184,7 @@ def get_all_day_data(
183
184
  requests.exceptions.RequestException: PYQQQ API로부터 데이터를 검색하는 과정에서 오류가 발생한 경우.
184
185
 
185
186
  Examples:
186
- >>> result = get_all_day_data(dtm.date(2024, 4, 26), ["005930", "319640"], dtm.timedelta(minutes=1))
187
+ >>> result = get_all_day_data(datetime.date(2024, 4, 26), ["005930", "319640"], datetime.timedelta(minutes=1))
187
188
  >>> print(result["069500"])
188
189
  open high low close volume sign change diff \
189
190
  time
@@ -193,7 +194,7 @@ def get_all_day_data(
193
194
  2024-04-26 09:03:00 77500 77500 77200 77500 3033307 2 1200 1.57
194
195
  2024-04-26 09:04:00 77400 77600 77400 77500 3268502 2 1200 1.57
195
196
  """
196
- assert isinstance(date, dtm.date), "date must be a datetime.date object"
197
+ assert isinstance(date, datetime.date), "date must be a datetime.date object"
197
198
  assert isinstance(codes, list) or isinstance(codes, str), "codes must be a list of strings or single code"
198
199
 
199
200
  if isinstance(codes, list):
@@ -202,13 +203,13 @@ def get_all_day_data(
202
203
  assert len(codes) <= 20, "codes must not exceed 20"
203
204
 
204
205
  if period is not None:
205
- assert period >= dtm.timedelta(seconds=30), "period must be at least 30 seconds"
206
+ assert period >= datetime.timedelta(seconds=30), "period must be at least 30 seconds"
206
207
  assert period.total_seconds() % 30 == 0, "period must be a multiple of 30 seconds"
207
208
 
208
209
  tz = pytz.timezone("Asia/Seoul")
209
210
  target_codes = codes if isinstance(codes, list) else [codes]
210
211
 
211
- exchange = _validate_exchange(exchange)
212
+ exchange = DataExchange.validate(exchange)
212
213
  if exchange == DataExchange.NXT or source == "kis":
213
214
  url = f"{c.PYQQQ_API_URL}/domestic-stock/ohlcv/minutes/{date}"
214
215
  else:
@@ -221,7 +222,7 @@ def get_all_day_data(
221
222
  "codes": ",".join(target_codes) if target_codes else None,
222
223
  "brokerage": source,
223
224
  "adjusted": "true" if adjusted else "false",
224
- "current_date": dtm.date.today(),
225
+ "current_date": datetime.date.today(),
225
226
  "exchange": exchange.value,
226
227
  },
227
228
  )
@@ -246,7 +247,7 @@ def get_all_day_data(
246
247
  rows = multirows[code]
247
248
  for row in rows:
248
249
  time = row[time_index].replace("Z", "+00:00")
249
- time = dtm.datetime.fromisoformat(time).astimezone(tz).replace(tzinfo=None)
250
+ time = datetime.datetime.fromisoformat(time).astimezone(tz).replace(tzinfo=None)
250
251
  row[time_index] = time
251
252
 
252
253
  rows.reverse()
@@ -270,7 +271,7 @@ def get_all_day_data(
270
271
 
271
272
  def resample_ebest_data(df, period):
272
273
  if period is not None and period.total_seconds() != 30:
273
- df["time"] = df["time"] - dtm.timedelta(seconds=30)
274
+ df["time"] = df["time"] - datetime.timedelta(seconds=30)
274
275
  df.set_index("time", inplace=True)
275
276
 
276
277
  minutes = period.total_seconds() / 60
@@ -368,13 +369,13 @@ def resample_kis_data(df, period):
368
369
  return df
369
370
 
370
371
 
371
- def get_orderbook(code: str, time: dtm.datetime) -> Dict:
372
+ def get_orderbook(code: str, time: datetime.datetime) -> Dict:
372
373
  """
373
374
  주식 종목의 주문 호가 정보를 반환합니다.
374
375
 
375
376
  Args:
376
377
  code (str): 종목 코드
377
- time (dtm.datetime): 조회할 시간
378
+ time (datetime.datetime): 조회할 시간
378
379
 
379
380
  Returns:
380
381
  dict: 호가 정보가 포함된 사전.
@@ -384,7 +385,7 @@ def get_orderbook(code: str, time: dtm.datetime) -> Dict:
384
385
  - ask_volume (int): 1차 매도 호가 잔량.
385
386
  - bid_price (int): 1차 매수 호가 가격.
386
387
  - bid_volume (int): 1차 매수 호가 잔량.
387
- - time (dtm.datetime): 현지 기준 호가 정보 조회 시간.
388
+ - time (datetime.datetime): 현지 기준 호가 정보 조회 시간.
388
389
  - bids (list): 매수 호가 목록 (각 항목은 price와 volume을 포함하는 dict).
389
390
  - asks (list): 매도 호가 목록 (각 항목은 price과 volume을 포함하는 dict).
390
391
  """
@@ -398,16 +399,6 @@ def get_orderbook(code: str, time: dtm.datetime) -> Dict:
398
399
 
399
400
  data = r.json()
400
401
  data.pop("code")
401
- data["time"] = dtm.datetime.fromisoformat(data["time"]).astimezone(pytz.timezone("Asia/Seoul")).replace(tzinfo=None)
402
+ data["time"] = datetime.datetime.fromisoformat(data["time"]).astimezone(pytz.timezone("Asia/Seoul")).replace(tzinfo=None)
402
403
 
403
404
  return data
404
-
405
-
406
- def _validate_exchange(exchange: Union[str, DataExchange]) -> DataExchange:
407
- if isinstance(exchange, str):
408
- assert exchange in [e.value for e in DataExchange], "지원하지 않는 거래소 코드입니다."
409
- exchange = DataExchange(exchange)
410
- else:
411
- assert exchange in DataExchange, "지원하지 않는 거래소 코드입니다."
412
-
413
- return exchange
pyqqq/datatypes.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import Enum
3
3
  from decimal import Decimal
4
- from typing import Optional
4
+ from typing import Optional, Union
5
5
  import datetime
6
6
 
7
7
 
@@ -20,6 +20,18 @@ class DataExchange(Enum):
20
20
  NXT = "NXT"
21
21
  """ 넥스트레이드 """
22
22
 
23
+ @classmethod
24
+ def validate(cls, exchange: Union[str, "DataExchange"]) -> "DataExchange":
25
+ if isinstance(exchange, cls):
26
+ return exchange
27
+ if isinstance(exchange, str):
28
+ try:
29
+ return cls(exchange)
30
+ except ValueError:
31
+ raise ValueError(f"지원하지 않는 거래소 코드입니다: {exchange}")
32
+
33
+ raise TypeError(f"exchange는 str 또는 DataExchange 타입이어야 합니다. 현재 타입: {type(exchange)}")
34
+
23
35
 
24
36
  class OrderExchange(Enum):
25
37
  KRX = 1
pyqqq/utils/compute.py CHANGED
@@ -1,4 +1,5 @@
1
- from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP
1
+ import datetime
2
+ from decimal import ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP, Decimal
2
3
  from typing import Union
3
4
 
4
5
 
@@ -45,19 +46,30 @@ def quantize_krx_price(price: Union[Decimal, int, float], etf_etn: bool, roundin
45
46
  return int((price / tick_size).quantize(Decimal("1"), rounding=constant_rounding) * tick_size)
46
47
 
47
48
 
48
- def get_krx_tick_size(price: float, etf_etn: bool) -> int:
49
+ def get_krx_tick_size(
50
+ price: float,
51
+ etf_etn: bool,
52
+ market: str = "KOSPI",
53
+ date: datetime.date = None,
54
+ ) -> int:
49
55
  """
50
- 주어진 가격과 금융 상품 유형에 따라 적절한 사이즈를 반환합니다.
56
+ 주어진 가격과 금융 상품 유형에 따라 적절한 호가가격단위를 반환합니다.
51
57
 
52
- 한국거래소(KRX)의 사이즈 규칙에 따라, 특정 가격대의 주식 또는 ETF/ETN의 최소 가격 변동 단위(틱 사이즈)를 결정합니다.
53
- 입력된 price가 각 가격대의 최소값 미만일 경우 해당하는 사이즈를 반환하며, 모든 조건에 부합하지 않는 경우 최대 가격을 반환합니다.
58
+ 한국거래소(KRX)의 호가가격단위 규칙에 따라, 특정 가격대의 주식 또는 ETF/ETN의 최소 가격 변동 단위(호가가격단위)를 결정합니다.
59
+ 입력된 price가 각 가격대의 최소값 미만일 경우 해당하는 호가가격단위를 반환하며, 모든 조건에 부합하지 않는 경우 최대 가격을 반환합니다.
60
+
61
+ 날짜별 규칙 변경사항:
62
+ - ETF/ETN: 2023-12-11 이전 5원, 이후 2000원 미만 1원/이상 5원
63
+ - 일반 주식: 2023-01-25 이전 market별 규칙, 이후 현재 통합 규칙
54
64
 
55
65
  Args:
56
66
  price (float): 상품의 가격.
57
67
  etf_etn (bool): 상품이 ETF 또는 ETN인 경우 True, 아니면 False.
68
+ market (str): 상품의 시장. 기본값은 "KOSPI".
69
+ date (datetime.date): 상품의 날짜. 기본값은 None.
58
70
 
59
71
  Returns:
60
- int: 결정된 틱 사이즈.
72
+ int: 결정된 호가가격단위.
61
73
 
62
74
  Raises:
63
75
  AssertionError: price가 0 이하일 경우 오류를 발생시킵니다.
@@ -73,22 +85,68 @@ def get_krx_tick_size(price: float, etf_etn: bool) -> int:
73
85
 
74
86
  assert price > 0, "price should be greater than 0"
75
87
 
88
+ # 날짜가 None인 경우 현재 날짜를 사용
89
+ if date is None:
90
+ date = datetime.date.today()
91
+
76
92
  conds = []
77
93
  max_value = 0
78
94
 
79
95
  if etf_etn:
80
- conds = [(2000, 1)]
81
- max_value = 5
96
+ # 2023년 12월 11일 이전에는 ETF/ETN 상품에 대해 가격에 상관없이 5원
97
+ etf_rule_change_date = datetime.date(2023, 12, 11)
98
+ if date < etf_rule_change_date:
99
+ conds = [] # 조건 없이 max_value로 처리
100
+ max_value = 5
101
+ else:
102
+ conds = [(2000, 1)]
103
+ max_value = 5
82
104
  else:
83
- conds = [
84
- (2000, 1),
85
- (5000, 5),
86
- (20000, 10),
87
- (50000, 50),
88
- (200000, 100),
89
- (500000, 500),
90
- ]
91
- max_value = 1000
105
+ # 일반 주식: 2023년 1월 25일 이전에는 market에 따른 규칙 적용
106
+ stock_rule_change_date = datetime.date(2023, 1, 25)
107
+
108
+ if date < stock_rule_change_date:
109
+ # 2023년 1월 25일 이전: market에 따른 규칙
110
+ if market == "KOSPI":
111
+ conds = [
112
+ (1000, 1),
113
+ (5000, 5),
114
+ (10000, 10),
115
+ (50000, 50),
116
+ (100000, 100),
117
+ (500000, 500),
118
+ ]
119
+ max_value = 1000
120
+ elif market == "KOSDAQ":
121
+ conds = [
122
+ (1000, 1),
123
+ (5000, 5),
124
+ (10000, 10),
125
+ (50000, 50),
126
+ ]
127
+ max_value = 100
128
+ else:
129
+ # 기본값 (KOSPI 규칙)
130
+ conds = [
131
+ (1000, 1),
132
+ (5000, 5),
133
+ (10000, 10),
134
+ (50000, 50),
135
+ (100000, 100),
136
+ (500000, 500),
137
+ ]
138
+ max_value = 1000
139
+ else:
140
+ # 2023년 1월 25일 이후: 현재 규칙
141
+ conds = [
142
+ (2000, 1),
143
+ (5000, 5),
144
+ (20000, 10),
145
+ (50000, 50),
146
+ (200000, 100),
147
+ (500000, 500),
148
+ ]
149
+ max_value = 1000
92
150
 
93
151
  for min_price, size in conds:
94
152
  if price < min_price:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqqq
3
- Version: 0.12.166
3
+ Version: 0.12.167
4
4
  Summary: Package for quantitative strategy development on the PyQQQ platform
5
5
  License: MIT
6
6
  Author: PyQQQ team
@@ -5,7 +5,7 @@ pyqqq/ai/domestic.py,sha256=FiJNInRlhcnxG7Jxmz2hDvaLhS8_jn-JFpQMze8Ch9s,1888
5
5
  pyqqq/ai/market_schedule.py,sha256=8HiivwC-xI2EKr8lXS_g4mTj2LYpCQ2QfZsJmIq61O0,818
6
6
  pyqqq/ai/minute.py,sha256=C0sTVkBY4-Vuj8Q9VZ7d9kZYAv963FUX4k3vIvhetng,1754
7
7
  pyqqq/backtest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pyqqq/backtest/broker.py,sha256=Z7GJEaO52UC2pdqOSHZEJcdZOI-BGIPZ75o8BP06eck,57661
8
+ pyqqq/backtest/broker.py,sha256=y4QRjxlHayvrojYjISamXBY9693sMxxEutN4QLG5ddA,58472
9
9
  pyqqq/backtest/environment.py,sha256=Vb9h-rh_gS--1Ku99tD36XtH0bVgYsPHqKxP1lT0XEQ,8890
10
10
  pyqqq/backtest/logger.py,sha256=BmoEMjUU76z8rZtMCYCwbspD3AVaHJrdbbT1EAFgrAE,3294
11
11
  pyqqq/backtest/positionprovider.py,sha256=wrR7Bntg28Q5_vGQV6XNzxe-SYoO9_GLcV9gDVEDAN4,4164
@@ -30,22 +30,22 @@ pyqqq/brokerage/multiprocess_tracker.py,sha256=Xx0hSpRZYITBGWjxclOEtNZdHV5agX94s
30
30
  pyqqq/brokerage/tracker.py,sha256=KpaxAQoWtFxMcljgTFqNgHVeR8gdDofqTQTl7FQqFmA,20499
31
31
  pyqqq/config.py,sha256=55Vqc_pGkdbrBdCV1aLgoH_n5IFxmMC59sbPHId3LoI,498
32
32
  pyqqq/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
- pyqqq/data/daily.py,sha256=e8J_EM8ZYLQPMPs7lbkwFs9PgNy26gek-ole0wbL2ss,8227
34
- pyqqq/data/domestic.py,sha256=dQkjzozPoiVhLG5GMEsDl3BM8JkubwlzWakuLqMBWW0,30739
33
+ pyqqq/data/daily.py,sha256=hLrVf5COqrZNXXuzp_CDDsBAHDl-I6-82ySkelkMQPU,7973
34
+ pyqqq/data/domestic.py,sha256=2FOYxDGw2W7DGwY61p3uGFb4IWqWUKiNdR3RlewjkCU,30352
35
35
  pyqqq/data/index.py,sha256=d5b-8a7IXu7yNJWt1tIe1Mj83NW0ZnQq8nsj2Sl3Gx8,6988
36
- pyqqq/data/minutes.py,sha256=Z-fhTJiIoG0EV95dLflPmHKMvxg2TTujuCC6XCOWA08,14910
36
+ pyqqq/data/minutes.py,sha256=mVgaeku-de5Vx-U-majIW3K5FpLYHE1ewPabK7UT9uM,14609
37
37
  pyqqq/data/overseas.py,sha256=yx7tCZHW8AvjIbtrP4dqIeC6wseRSzbg5ag3dm6H0LY,1234
38
38
  pyqqq/data/realtime.py,sha256=W2UJ1cU_h4Bc4XrqIs96n6tkrZSAVpIZ-RrnIfU2LfI,14888
39
39
  pyqqq/data/ticks.py,sha256=DXioiKBsGTzwXyvEH0lpm8t5g-1nHIOLKMXoSrE1Rko,4127
40
40
  pyqqq/data/us_stocks.py,sha256=jXR9dQEVigrwTLEpX1aX1_AQvOlBopW265gwx8Nq8OA,12959
41
- pyqqq/datatypes.py,sha256=eUAreHmT33VTX6W_GuZC51-KL2xfAzTCk8WxqtRyCas,7858
41
+ pyqqq/datatypes.py,sha256=KnanWzat6w5w0vNvKHsWt9VOBwf9gh0njdt36PJBgXc,8370
42
42
  pyqqq/executors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
43
  pyqqq/executors/hook.py,sha256=xV9SVUpUwGm8AgEuz8aD7U4ema47lRoKn4KFthuLJwQ,36985
44
44
  pyqqq/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  pyqqq/utils/api_client.py,sha256=WLcvZ9sZp2b9YrTauZt2VkcikfByOhHxPHCvGXmgEzw,585
46
46
  pyqqq/utils/array.py,sha256=8E8JW-P1GWzluiqIDuKdUEALZ30AkKRtfxgdrWmu__Q,1747
47
47
  pyqqq/utils/casting.py,sha256=nCHnJQ_F88R22xfnBg58fiJXwHYYnsnk3qSDw_rVIY8,135
48
- pyqqq/utils/compute.py,sha256=tPNYDfcyi7AnP3-mvRXtPDfj2_Mm54PV41v2kif0a3I,3542
48
+ pyqqq/utils/compute.py,sha256=nM5WUZ7aWnsrcHnKEQT-Omv56lrZJO6LfmDH6tnrg1M,5645
49
49
  pyqqq/utils/copycat.py,sha256=1cMuQKteOuzBbH3aAdsDCH7ZTxTyM6OyJ5Wii7gmBpk,10837
50
50
  pyqqq/utils/daily_tickers.py,sha256=_zK-U1jJgQlmXARVPA4Wnmtd_mkxeZAp4-Dg_xMLEOs,3474
51
51
  pyqqq/utils/display.py,sha256=kFoXw52ODDgbR-ufAKRJdY5NEA7UTikrosZRukEIWFc,1177
@@ -58,6 +58,6 @@ pyqqq/utils/mock_api.py,sha256=7EsaVQ9mOVZQAqtQW24isPnk9QTbJII7x3guhFyEMAE,10569
58
58
  pyqqq/utils/position_classifier.py,sha256=EaomByAWM2lVuYow5OFdJNrN64Fpukhj-lhFkjYpjeo,14908
59
59
  pyqqq/utils/retry.py,sha256=4mw9MQvgSBC8bTLvDauaCEI5N9tL8upHCk8rSfaVRG8,2066
60
60
  pyqqq/utils/singleton.py,sha256=m6NZ8fwVDpI6U-gUUihMPgVK_NkDh-Z1NSAtjisrpjY,810
61
- pyqqq-0.12.166.dist-info/METADATA,sha256=9uiWorCdeOgTPj4KkrGk3l1ffZDhRCpeJQVPOHP83Bg,1664
62
- pyqqq-0.12.166.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
63
- pyqqq-0.12.166.dist-info/RECORD,,
61
+ pyqqq-0.12.167.dist-info/METADATA,sha256=EzjcpodmI8pe1mQKvfEPediFokW0_LfEjThpTLRwxK4,1664
62
+ pyqqq-0.12.167.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
63
+ pyqqq-0.12.167.dist-info/RECORD,,