pyqqq 0.12.166__tar.gz → 0.12.168__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 (64) hide show
  1. {pyqqq-0.12.166 → pyqqq-0.12.168}/PKG-INFO +1 -1
  2. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyproject.toml +1 -1
  3. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/broker.py +16 -9
  4. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/tracker.py +114 -15
  5. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/daily.py +25 -32
  6. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/domestic.py +33 -51
  7. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/minutes.py +32 -41
  8. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/datatypes.py +13 -1
  9. pyqqq-0.12.168/pyqqq/utils/compute.py +168 -0
  10. pyqqq-0.12.166/pyqqq/utils/compute.py +0 -110
  11. {pyqqq-0.12.166 → pyqqq-0.12.168}/README.md +0 -0
  12. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/__init__.py +0 -0
  13. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/__init__.py +0 -0
  14. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/daily.py +0 -0
  15. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/domestic.py +0 -0
  16. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/market_schedule.py +0 -0
  17. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/minute.py +0 -0
  18. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/__init__.py +0 -0
  19. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/environment.py +0 -0
  20. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/logger.py +0 -0
  21. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/positionprovider.py +0 -0
  22. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/strategy.py +0 -0
  23. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/utils.py +0 -0
  24. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/wallclock.py +0 -0
  25. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/__init__.py +0 -0
  26. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/__init__.py +0 -0
  27. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
  28. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/oauth.py +0 -0
  29. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/simple.py +0 -0
  30. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/tr_client.py +0 -0
  31. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/helper.py +0 -0
  32. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/__init__.py +0 -0
  33. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/domestic_stock.py +0 -0
  34. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/oauth.py +0 -0
  35. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
  36. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/simple.py +0 -0
  37. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
  38. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/tr_client.py +0 -0
  39. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
  40. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/config.py +0 -0
  41. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/__init__.py +0 -0
  42. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/index.py +0 -0
  43. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/overseas.py +0 -0
  44. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/realtime.py +0 -0
  45. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/ticks.py +0 -0
  46. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/us_stocks.py +0 -0
  47. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/executors/__init__.py +0 -0
  48. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/executors/hook.py +0 -0
  49. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/__init__.py +0 -0
  50. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/api_client.py +0 -0
  51. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/array.py +0 -0
  52. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/casting.py +0 -0
  53. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/copycat.py +0 -0
  54. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/daily_tickers.py +0 -0
  55. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/display.py +0 -0
  56. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/kvstore.py +0 -0
  57. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/limiter.py +0 -0
  58. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/local_cache.py +0 -0
  59. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/logger.py +0 -0
  60. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/market_schedule.py +0 -0
  61. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/mock_api.py +0 -0
  62. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/position_classifier.py +0 -0
  63. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/retry.py +0 -0
  64. {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/singleton.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyqqq
3
- Version: 0.12.166
3
+ Version: 0.12.168
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.166"
3
+ version = "0.12.168"
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"
@@ -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=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=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")
@@ -8,6 +8,7 @@ from pyqqq.utils.api_client import raise_for_status, send_request
8
8
  from pyqqq.utils.array import find
9
9
  from pyqqq.utils.logger import get_logger
10
10
  from pyqqq.utils.market_schedule import get_market_schedule, get_last_trading_day
11
+ from pyqqq.utils.singleton import singleton
11
12
  from typing import Dict, Optional, List
12
13
  import asyncio
13
14
  import os
@@ -15,6 +16,111 @@ import pyqqq.config as c
15
16
  import datetime as dtm
16
17
 
17
18
 
19
+ @singleton
20
+ class TrackerSocket:
21
+ """
22
+ 거래 내역 추적을 위한 WebSocket 소켓 클래스입니다.
23
+
24
+ Args:
25
+ simple_api (EBestSimpleDomesticStock | KISSimpleDomesticStock): 간편 거래 API 객체
26
+ """
27
+
28
+ def __init__(self, simple_api: EBestSimpleDomesticStock | KISSimpleDomesticStock):
29
+ self.simple_api = simple_api
30
+ self.task: asyncio.Task = None
31
+ self.stop_event = asyncio.Event()
32
+ self.logger = get_logger(__name__ + ".TrackerSocket")
33
+ self.trading_tracker_counter = 1
34
+ self.event_callbacks: Dict[callable] = {}
35
+
36
+ async def start(self):
37
+ """
38
+ TradingSocket에서 거래내역 추적을 시작합니다.
39
+ """
40
+ if self.task is not None and not self.task.done():
41
+ self.logger.info("TrackerSocket already started!")
42
+ return
43
+
44
+ self.task = asyncio.create_task(self._listen_order_event())
45
+ self.logger.info("TrackerSocket started!")
46
+
47
+ async def _listen_order_event(self):
48
+ """
49
+ 주문 이벤트를 수신하고 처리하는 비동기 메서드입니다.
50
+ """
51
+ self.logger.info("Listening for order events...")
52
+ try:
53
+ async for event in self.simple_api.listen_order_event(self.stop_event):
54
+ self._relay_order_event(event)
55
+ except asyncio.CancelledError:
56
+ self.logger.info("Order event listening cancelled.")
57
+ except Exception as e:
58
+ self.logger.exception(f"Error while listening to order events: {e}")
59
+
60
+ def _relay_order_event(self, event: OrderEvent):
61
+ """
62
+ 주문 이벤트를 등록된 콜백 함수로 전달합니다.
63
+
64
+ Args:
65
+ event (OrderEvent): 주문 이벤트 객체
66
+ """
67
+ self.logger.debug(f"Relay order event: {event}")
68
+
69
+ for callback in self.event_callbacks.values():
70
+ try:
71
+ callback(event)
72
+ except Exception as e:
73
+ self.logger.exception(f"Error in callback {callback}: {e}")
74
+
75
+ def add_tracker(self, callback: callable):
76
+ """
77
+ TradingTracker를 추가합니다.
78
+
79
+ Args:
80
+ callback (callable): 거래 내역 추적 이벤트를 처리할 콜백 함수
81
+ """
82
+ ret = self.trading_tracker_counter
83
+ self.event_callbacks[self.trading_tracker_counter] = callback
84
+ self.trading_tracker_counter += 1
85
+
86
+ self.logger.info(f"Tracker added: {ret}")
87
+ return ret
88
+
89
+ async def remove_tracker(self, tracker_number: int):
90
+ """
91
+ TradingTracker를 제거합니다.
92
+ Args:
93
+ tracker_number (int): 제거할 트래커 번호
94
+ """
95
+ if tracker_number in self.event_callbacks:
96
+ del self.event_callbacks[tracker_number]
97
+ self.logger.info(f"Tracker removed: {tracker_number}")
98
+ else:
99
+ self.logger.warning(f"Tracker {tracker_number} not found!")
100
+
101
+ if len(self.event_callbacks) == 0:
102
+ self.logger.info("No more trackers, stopping TrackerSocket.")
103
+ await self.stop()
104
+
105
+ async def stop(self):
106
+ """
107
+ 거래 내역 추적을 중지합니다.
108
+ """
109
+ if self.task is None or self.task.done():
110
+ self.logger.info("TrackerSocket already stopped!")
111
+ return
112
+
113
+ self.stop_event.set()
114
+ self.task.cancel()
115
+ try:
116
+ await self.task
117
+ except asyncio.CancelledError:
118
+ pass
119
+ self.logger.info("TrackerSocket stopped!")
120
+
121
+ self.task = asyncio.create_task(self._monitor_schedule())
122
+
123
+
18
124
  class TradingTracker:
19
125
  """
20
126
  거래 내역 추적을 위한 클래스입니다
@@ -69,7 +175,6 @@ class TradingTracker:
69
175
  """ 백그라운드로 실행되는 거래 이벤트 모니터링 Task """
70
176
 
71
177
  self.simple_api = simple_api
72
- self.stop_event = asyncio.Event()
73
178
  self.account_no = None
74
179
  self.fee_rate = fee_rate # 증권사 수수료율
75
180
  self.tax_rate = Decimal("0.0018") # KOSPI, KOSDAQ 매도시 거래세율 0.18%
@@ -77,6 +182,8 @@ class TradingTracker:
77
182
  self.ticker_date: dtm.datetime = None # 종목 정보 갱신 시간
78
183
  self.save_trading_history = False # 거래 내역 저장 여부
79
184
  self.last_t_day_tickers: Dict[str, Dict] = {}
185
+ self.tracker_socket = TrackerSocket(simple_api)
186
+ self.tracker_number = None
80
187
 
81
188
  self.started = False
82
189
  self.callback_id = 0
@@ -94,7 +201,7 @@ class TradingTracker:
94
201
  elif isinstance(self.simple_api, KISSimpleDomesticStock):
95
202
  self.account_no = self.simple_api.account_no + self.simple_api.account_product_code
96
203
 
97
- self.logger.info(f"Trading tarcker started! Account No: {self.account_no} / save history: {self.save_trading_history}")
204
+ self.logger.info(f"Trading tracker started! Account No: {self.account_no} / save history: {self.save_trading_history}")
98
205
 
99
206
  self._fetch_tickers()
100
207
  self._sync_positions_and_pending_orders()
@@ -104,8 +211,10 @@ class TradingTracker:
104
211
  for o in self.pending_orders:
105
212
  self.logger.info(f"- {o.order_no}({o.org_order_no})\t{o.side}\t{o.asset_code}\t{o.filled_quantity}/{o.quantity}\t{o.is_pending}")
106
213
 
214
+ self.tracker_number = self.tracker_socket.add_tracker(self._handle_order_event)
215
+
107
216
  self.tasks = [
108
- asyncio.create_task(self._monitor_trading()),
217
+ asyncio.create_task(self.tracker_socket.start()),
109
218
  asyncio.create_task(self._monitor_schedule()),
110
219
  ]
111
220
  self.started = True
@@ -142,25 +251,15 @@ class TradingTracker:
142
251
  """
143
252
  거래 내역 추적을 중지합니다
144
253
  """
145
- self.stop_event.set()
146
254
  for t in self.tasks:
147
255
  t.cancel()
148
-
149
256
  await asyncio.gather(*self.tasks)
257
+ await self.tracker_socket.remove_tracker(self.tracker_number)
150
258
  self.started = False
151
259
 
152
- async def _monitor_trading(self):
153
- try:
154
- async for event in self.simple_api.listen_order_event(self.stop_event):
155
- self._handle_order_event(event)
156
- except asyncio.CancelledError:
157
- return
158
- except Exception as e:
159
- self.logger.exception(f"Error on handling order event: {e}")
160
-
161
260
  async def _monitor_schedule(self):
162
261
  """거래 시간대별 작업을 위한 스케줄을 모니터링합니다"""
163
- while not self.stop_event.is_set():
262
+ while not self.tracker_socket.stop_event.is_set():
164
263
  market_schedule = get_market_schedule(dtm.date.today())
165
264
 
166
265
  if not market_schedule.full_day_closed:
@@ -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
@@ -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