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.
- {pyqqq-0.12.166 → pyqqq-0.12.168}/PKG-INFO +1 -1
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyproject.toml +1 -1
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/broker.py +16 -9
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/tracker.py +114 -15
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/daily.py +25 -32
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/domestic.py +33 -51
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/minutes.py +32 -41
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/datatypes.py +13 -1
- pyqqq-0.12.168/pyqqq/utils/compute.py +168 -0
- pyqqq-0.12.166/pyqqq/utils/compute.py +0 -110
- {pyqqq-0.12.166 → pyqqq-0.12.168}/README.md +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/daily.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/domestic.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/market_schedule.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/ai/minute.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/environment.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/logger.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/positionprovider.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/strategy.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/utils.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/backtest/wallclock.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/domestic_stock.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/oauth.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/simple.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/ebest/tr_client.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/helper.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/domestic_stock.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/oauth.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/overseas_stock.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/simple.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/simple_overseas.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/kis/tr_client.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/brokerage/multiprocess_tracker.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/config.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/index.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/overseas.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/realtime.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/ticks.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/data/us_stocks.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/executors/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/executors/hook.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/__init__.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/api_client.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/array.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/casting.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/copycat.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/daily_tickers.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/display.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/kvstore.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/limiter.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/local_cache.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/logger.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/market_schedule.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/mock_api.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/position_classifier.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/retry.py +0 -0
- {pyqqq-0.12.166 → pyqqq-0.12.168}/pyqqq/utils/singleton.py +0 -0
|
@@ -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
|
|
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.
|
|
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:
|
|
20
|
+
date: datetime.date,
|
|
19
21
|
adjusted: bool = True,
|
|
20
|
-
exchange: Union[
|
|
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 (
|
|
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(
|
|
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,
|
|
67
|
+
if isinstance(date, datetime.datetime):
|
|
66
68
|
date = date.date()
|
|
67
69
|
|
|
68
|
-
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":
|
|
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:
|
|
100
|
-
end_date: Optional[
|
|
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[
|
|
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 (
|
|
118
|
-
end_date (Optional[
|
|
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'],
|
|
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 =
|
|
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":
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
340
|
-
lower = price * 0.7
|
|
357
|
+
upper = int(upper // tick_size * tick_size)
|
|
341
358
|
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|