diff --git a/pykis/adapter/account_product/order_modify.py b/pykis/adapter/account_product/order_modify.py new file mode 100644 index 00000000..1cc82eee --- /dev/null +++ b/pykis/adapter/account_product/order_modify.py @@ -0,0 +1,112 @@ +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +from pykis.utils.params import EMPTY, EMPTY_TYPE + +if TYPE_CHECKING: + from pykis.api.account.order import ( + IN_ORDER_QUANTITY, + ORDER_CONDITION, + ORDER_EXECUTION, + ORDER_PRICE, + KisOrder, + KisOrderNumber, + ) + + +@runtime_checkable +class KisCancelableOrder(Protocol): + """취소 가능 주문 프로토콜""" + + def cancel(self) -> "KisOrder": + """ + 한국투자증권 통합 주식 주문취소 (해외 주간거래 모의투자 미지원) + + 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] + 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + """ + raise NotImplementedError + + +class KisModifyableOrder(Protocol): + """정정 가능 주문 프로토콜""" + + def modify( + self, + price: "ORDER_PRICE | None | EMPTY_TYPE" = EMPTY, + qty: "IN_ORDER_QUANTITY | None" = None, + condition: "ORDER_CONDITION | None | EMPTY_TYPE" = EMPTY, + execution: "ORDER_EXECUTION | None | EMPTY_TYPE" = EMPTY, + ) -> "KisOrder": + """ + 한국투자증권 통합 주식 주문정정 (국내 모의투자 미지원, 해외 주간거래 모의투자 미지원) + + 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] + 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + + Args: + price (ORDER_PRICE, optional): 주문가격 + qty (IN_ORDER_QUANTITY, optional): 주문수량 + condition (ORDER_CONDITION, optional): 주문조건 + execution (ORDER_EXECUTION_CONDITION, optional): 체결조건 + """ + raise NotImplementedError + + +@runtime_checkable +class KisOrderableOrder(KisCancelableOrder, KisModifyableOrder, Protocol): + """주문 가능 주문 프로토콜""" + + +class KisCancelableOrderImpl: + """취소 가능 주문""" + + def cancel( + self: "KisOrderNumber", + ) -> "KisOrder": + """ + 한국투자증권 통합 주식 주문취소 (해외 주간거래 모의투자 미지원) + + 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] + 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + """ + from pykis.api.account.order_modify import cancel_order + + return cancel_order(self.kis, order=self) + + +class KisModifyableOrderImpl: + """정정 가능 주문""" + + def modify( + self: "KisOrderNumber", + price: "ORDER_PRICE | None | EMPTY_TYPE" = EMPTY, + qty: "IN_ORDER_QUANTITY | None" = None, + condition: "ORDER_CONDITION | None | EMPTY_TYPE" = EMPTY, + execution: "ORDER_EXECUTION | None | EMPTY_TYPE" = EMPTY, + ) -> "KisOrder": + """ + 한국투자증권 통합 주식 주문정정 (국내 모의투자 미지원, 해외 주간거래 모의투자 미지원) + + 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] + 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + + Args: + price (ORDER_PRICE, optional): 주문가격 + qty (IN_ORDER_QUANTITY, optional): 주문수량 + condition (ORDER_CONDITION, optional): 주문조건 + execution (ORDER_EXECUTION_CONDITION, optional): 체결조건 + """ + from pykis.api.account.order_modify import modify_order + + return modify_order( + self.kis, + order=self, + price=price, + qty=qty, + condition=condition, + execution=execution, + ) + + +class KisOrderableOrderImpl(KisCancelableOrderImpl, KisModifyableOrderImpl): + """주문 가능 주문""" diff --git a/pykis/adapter/websocket/execution.py b/pykis/adapter/websocket/execution.py index ca1e80ef..d3226d15 100644 --- a/pykis/adapter/websocket/execution.py +++ b/pykis/adapter/websocket/execution.py @@ -1,10 +1,11 @@ from typing import TYPE_CHECKING, Callable, Literal, Protocol, runtime_checkable from pykis.api.base.account import KisAccountProtocol -from pykis.event.handler import KisEventFilter, KisEventTicket +from pykis.event.handler import KisEventFilter, KisEventTicket, KisMultiEventFilter from pykis.event.subscription import KisSubscriptionEventArgs if TYPE_CHECKING: + from pykis.api.account.order import KisOrder from pykis.api.websocket.order_execution import KisRealtimeExecution from pykis.client.websocket import KisWebsocketClient @@ -22,9 +23,7 @@ def on( self, event: Literal["execution"], callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, once: bool = False, ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": """ @@ -44,9 +43,7 @@ def once( self, event: Literal["execution"], callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": """ 웹소켓 이벤트 핸들러 등록 @@ -68,9 +65,7 @@ def on( self: "KisAccountProtocol", event: Literal["execution"], callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, once: bool = False, ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": """ @@ -100,9 +95,7 @@ def once( self: "KisAccountProtocol", event: Literal["execution"], callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": """ 웹소켓 이벤트 핸들러 등록 @@ -125,3 +118,65 @@ def once( ) raise ValueError(f"Unknown event: {event}") + + +class KisRealtimeOrderableOrderImpl: + """한국투자증권 실시간 주문 가능 주문""" + + def on( + self: "KisOrder", + event: Literal["execution"], + callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, + once: bool = False, + ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": + """ + 웹소켓 이벤트 핸들러 등록 + + [국내주식] 실시간시세 -> 국내주식 실시간체결통보[실시간-005] + [해외주식] 실시간시세 -> 해외주식 실시간체결통보[실시간-009] + + Args: + callback (Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]): 콜백 함수 + where (KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None, optional): 이벤트 필터. Defaults to None. + once (bool, optional): 한번만 실행 여부. Defaults to False. + """ + from pykis.api.websocket.order_execution import on_account_execution + + if event == "execution": + return on_account_execution( + self, + callback=callback, + where=KisMultiEventFilter(self, where) if where else self, + once=once, + ) + + raise ValueError(f"Unknown event: {event}") + + def once( + self: "KisOrder", + event: Literal["execution"], + callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", + where: "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" = None, + ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": + """ + 웹소켓 이벤트 핸들러 등록 + + [국내주식] 실시간시세 -> 국내주식 실시간체결통보[실시간-005] + [해외주식] 실시간시세 -> 해외주식 실시간체결통보[실시간-009] + + Args: + callback (Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]): 콜백 함수 + where (KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None, optional): 이벤트 필터. Defaults to None. + """ + from pykis.api.websocket.order_execution import on_account_execution + + if event == "execution": + return on_account_execution( + self, + callback=callback, + where=KisMultiEventFilter(self, where) if where else self, + once=True, + ) + + raise ValueError(f"Unknown event: {event}") diff --git a/pykis/api/account/daily_order.py b/pykis/api/account/daily_order.py index 32721667..dc14aed9 100644 --- a/pykis/api/account/daily_order.py +++ b/pykis/api/account/daily_order.py @@ -9,6 +9,7 @@ ORDER_QUANTITY, ORDER_TYPE, KisOrder, + KisSimpleOrder, ) from pykis.api.base.account import KisAccountBase, KisAccountProtocol from pykis.api.base.account_product import ( @@ -372,7 +373,7 @@ class KisDomesticDailyOrder(KisDynamic, KisDailyOrderBase): @property def order_number(self) -> KisOrder: """주문번호""" - return KisOrder.from_order( + return KisSimpleOrder.from_order( account_number=self.account_number, symbol=self.symbol, market=self.market, @@ -495,7 +496,7 @@ class KisForeignDailyOrder(KisDynamic, KisDailyOrderBase): @cached def order_number(self) -> KisOrder: """주문번호""" - return KisOrder.from_order( + return KisSimpleOrder.from_order( account_number=self.account_number, symbol=self.symbol, market=self.market, diff --git a/pykis/api/account/order.py b/pykis/api/account/order.py index 3bfffaf7..46457acf 100644 --- a/pykis/api/account/order.py +++ b/pykis/api/account/order.py @@ -11,7 +11,16 @@ runtime_checkable, ) -from pykis.adapter.websocket.execution import KisRealtimeOrderableAccount +from typing_extensions import deprecated + +from pykis.adapter.account_product.order_modify import ( + KisOrderableOrder, + KisOrderableOrderImpl, +) +from pykis.adapter.websocket.execution import ( + KisRealtimeOrderableAccount, + KisRealtimeOrderableOrderImpl, +) from pykis.api.base.account import KisAccountProtocol from pykis.api.base.account_product import ( KisAccountProductBase, @@ -28,19 +37,17 @@ from pykis.api.stock.quote import quote from pykis.client.account import KisAccountNumber from pykis.event.filters.order import KisOrderNumberEventFilter -from pykis.event.handler import KisEventFilter, KisEventTicket, KisMultiEventFilter +from pykis.event.handler import KisEventFilter from pykis.event.subscription import KisSubscriptionEventArgs from pykis.responses.exceptions import KisMarketNotOpenedError from pykis.responses.response import KisAPIResponse, raise_not_found from pykis.responses.types import KisString -from pykis.utils.params import EMPTY, EMPTY_TYPE from pykis.utils.timezone import TIMEZONE from pykis.utils.typing import Checkable if TYPE_CHECKING: from pykis.api.account.pending_order import KisPendingOrder from pykis.api.base.account_product import KisAccountProductProtocol - from pykis.api.websocket.order_execution import KisRealtimeExecution from pykis.client.websocket import KisWebsocketClient from pykis.kis import PyKis @@ -295,11 +302,7 @@ def order_condition( virtual_not_supported = True raise ValueError( - ( - "모의투자는 해당 주문조건을 지원하지 않습니다." - if virtual_not_supported - else "주문조건이 잘못되었습니다." - ) + ("모의투자는 해당 주문조건을 지원하지 않습니다." if virtual_not_supported else "주문조건이 잘못되었습니다.") + f" (market={market!r}, order={order!r}, price={price!r}, condition={condition!r}, execution={execution!r})\n" "아래 주문 가능 조건을 참고하세요.\n\n" + orderable_conditions_repr() ) @@ -307,9 +310,7 @@ def order_condition( return ORDER_CONDITION_MAP[tuple(order_condition)] # type: ignore -DOMESTIC_REVERSE_ORDER_CONDITION_MAP: dict[ - str, tuple[bool, ORDER_CONDITION | None, ORDER_EXECUTION | None] -] = { +DOMESTIC_REVERSE_ORDER_CONDITION_MAP: dict[str, tuple[bool, ORDER_CONDITION | None, ORDER_EXECUTION | None]] = { # 주문구분코드: (지정가여부, 주문조건, 체결조건) "00": (True, None, None), "01": (False, None, None), @@ -338,9 +339,7 @@ def resolve_domestic_order_condition( @runtime_checkable -class KisOrderNumber( - KisAccountProductProtocol, KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs], Protocol -): +class KisOrderNumber(KisAccountProductProtocol, KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs], Protocol): """한국투자증권 주문번호""" @property @@ -361,7 +360,7 @@ def __hash__(self) -> int: @runtime_checkable -class KisOrder(KisOrderNumber, KisRealtimeOrderableAccount, Protocol): +class KisOrder(KisOrderNumber, KisOrderableOrder, KisRealtimeOrderableAccount, Protocol): """한국투자증권 주문""" @property @@ -389,36 +388,6 @@ def pending_order(self) -> "KisPendingOrder | None": """미체결 주문""" raise NotImplementedError - def modify( - self, - price: ORDER_PRICE | None | EMPTY_TYPE = EMPTY, - qty: IN_ORDER_QUANTITY | None = None, - condition: ORDER_CONDITION | None | EMPTY_TYPE = EMPTY, - execution: ORDER_EXECUTION | None | EMPTY_TYPE = EMPTY, - ) -> "KisOrder": - """ - 한국투자증권 통합 주식 주문정정 (국내 모의투자 미지원, 해외 주간거래 모의투자 미지원) - - 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] - 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] - - Args: - price (ORDER_PRICE, optional): 주문가격 - qty (IN_ORDER_QUANTITY, optional): 주문수량 - condition (ORDER_CONDITION, optional): 주문조건 - execution (ORDER_EXECUTION_CONDITION, optional): 체결조건 - """ - raise NotImplementedError - - def cancel(self) -> "KisOrder": - """ - 한국투자증권 통합 주식 주문취소 (해외 주간거래 모의투자 미지원) - - 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] - 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] - """ - raise NotImplementedError - @staticmethod def from_number( kis: "PyKis", @@ -439,7 +408,7 @@ def from_number( branch (str): 지점코드 number (str): 주문번호 """ - return KisOrderNumberBase( + return KisSimpleOrderNumber.from_number( kis=kis, symbol=symbol, market=market, @@ -470,14 +439,14 @@ def from_order( number (str): 주문번호 time_kst (datetime): 주문시간 (한국시간) """ - return KisOrderBase( - account_number=account_number, + return KisSimpleOrder.from_order( + kis=kis, symbol=symbol, - market=market, # type: ignore + market=market, + account_number=account_number, branch=branch, number=number, time_kst=time_kst, - kis=kis, ) @@ -550,20 +519,20 @@ def __init__( self.number = number - def __eq__(self, value: "object | KisOrderNumberBase") -> bool: - if not isinstance(value, KisOrderNumberBase): + def __eq__(self, value: object | KisOrderNumber) -> bool: + try: + return ( + self.account_number == value.account_number # type: ignore + and self.symbol == value.symbol # type: ignore + and self.market == value.market # type: ignore + and self.branch == value.branch # type: ignore + and int(self.number) == int(value.number) # type: ignore + ) + except AttributeError: return False - return ( - self.account_number == value.account_number # type: ignore - and self.symbol == value.symbol # type: ignore - and self.market == value.market # type: ignore - and self.branch == value.branch # type: ignore - and self.number == value.number # type: ignore - ) - def __hash__(self) -> int: - return hash((self.account_number, self.symbol, self.market, self.branch, self.number)) + return hash((self.account_number, self.symbol, self.market, self.branch, int(self.number))) def __repr__(self) -> str: return f"""{self.__class__.__name__}( @@ -575,69 +544,8 @@ def __repr__(self) -> str: number={self.number!r} )""" - @staticmethod - def from_number( - kis: "PyKis", - symbol: str, - market: MARKET_TYPE, - account_number: KisAccountNumber, - branch: str, - number: str, - ) -> "KisOrderNumber": - """ - 주문번호 생성 - - Args: - kis (PyKis): 한국투자증권 API - symbol (str): 종목코드 - market (MARKET_TYPE): 상품유형 - account_number (KisAccountNumber): 계좌번호 - branch (str): 지점코드 - number (str): 주문번호 - """ - return KisOrder.from_number( - kis=kis, - symbol=symbol, - market=market, - account_number=account_number, - branch=branch, - number=number, - ) - - @staticmethod - def from_order( - kis: "PyKis", - symbol: str, - market: MARKET_TYPE, - account_number: KisAccountNumber, - branch: str, - number: str, - time_kst: datetime, - ) -> "KisOrder": - """ - 주문 생성 - - Args: - kis (PyKis): 한국투자증권 API - symbol (str): 종목코드 - market (MARKET_TYPE): 상품유형 - account_number (KisAccountNumber): 계좌번호 - branch (str): 지점코드 - number (str): 주문번호 - time_kst (datetime): 주문시간 (한국시간) - """ - return KisOrder.from_order( - kis=kis, - symbol=symbol, - market=market, - account_number=account_number, - branch=branch, - number=number, - time_kst=time_kst, - ) - -class KisOrderBase(KisOrderNumberBase): +class KisOrderBase(KisOrderNumberBase, KisOrderableOrderImpl, KisRealtimeOrderableOrderImpl): """한국투자증권 주문""" symbol: str @@ -739,108 +647,137 @@ def pending_order(self) -> "KisPendingOrder | None": country=get_market_country(self.market), ).order(self) - def modify( - self, - price: ORDER_PRICE | None | EMPTY_TYPE = EMPTY, - qty: IN_ORDER_QUANTITY | None = None, - condition: ORDER_CONDITION | None | EMPTY_TYPE = EMPTY, - execution: ORDER_EXECUTION | None | EMPTY_TYPE = EMPTY, - ) -> KisOrder: + @staticmethod + @deprecated("Use KisOrder.from_number() instead") + def from_number( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + ) -> "KisOrderNumber": """ - 한국투자증권 통합 주식 주문정정 (국내 모의투자 미지원, 해외 주간거래 모의투자 미지원) - - 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] - 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + 주문번호 생성 Args: - price (ORDER_PRICE, optional): 주문가격 - qty (IN_ORDER_QUANTITY, optional): 주문수량 - condition (ORDER_CONDITION, optional): 주문조건 - execution (ORDER_EXECUTION_CONDITION, optional): 체결조건 + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 """ - from pykis.api.account.order_modify import modify_order - - return modify_order( - self.kis, - order=self, - price=price, - qty=qty, - condition=condition, - execution=execution, + return KisSimpleOrderNumber.from_number( + kis=kis, + symbol=symbol, + market=market, + account_number=account_number, + branch=branch, + number=number, ) - def cancel(self) -> "KisOrder": + @staticmethod + @deprecated("Use KisOrder.from_order() instead") + def from_order( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + time_kst: datetime, + ) -> "KisOrder": """ - 한국투자증권 통합 주식 주문취소 (해외 주간거래 모의투자 미지원) + 주문 생성 - 국내주식주문 -> 주식주문(정정취소)[v1_국내주식-003] - 국내주식주문 -> 해외주식 정정취소주문[v1_해외주식-003] + Args: + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 + time_kst (datetime): 주문시간 (한국시간) """ - from pykis.api.account.order_modify import cancel_order + return KisSimpleOrder.from_order( + kis=kis, + symbol=symbol, + market=market, + account_number=account_number, + branch=branch, + number=number, + time_kst=time_kst, + ) - return cancel_order(self.kis, order=self) - def on( - self, - event: Literal["execution"], - callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, - once: bool = False, - ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": - """ - 웹소켓 이벤트 핸들러 등록 +class KisSimpleOrderNumber(KisOrderNumberBase): + """한국투자증권 주문번호""" - [국내주식] 실시간시세 -> 국내주식 실시간체결통보[실시간-005] - [해외주식] 실시간시세 -> 해외주식 실시간체결통보[실시간-009] + @staticmethod + def from_number( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + ) -> "KisOrderNumber": + """ + 주문번호 생성 Args: - callback (Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]): 콜백 함수 - where (KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None, optional): 이벤트 필터. Defaults to None. - once (bool, optional): 한번만 실행 여부. Defaults to False. + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 """ - from pykis.api.websocket.order_execution import on_account_execution + return KisSimpleOrderNumber( + kis=kis, + symbol=symbol, + market=market, + account_number=account_number, + branch=branch, + number=number, + ) - if event == "execution": - return on_account_execution( - self, - callback=callback, - where=KisMultiEventFilter(self, where) if where else self, - once=once, - ) - raise ValueError(f"Unknown event: {event}") +class KisSimpleOrder(KisOrderBase): + """한국투자증권 주문번호""" - def once( - self, - event: Literal["execution"], - callback: "Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]", - where: ( - "KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None" - ) = None, - ) -> "KisEventTicket[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]]": + @staticmethod + def from_order( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + time_kst: datetime, + ) -> "KisOrder": """ - 웹소켓 이벤트 핸들러 등록 - - [국내주식] 실시간시세 -> 국내주식 실시간체결통보[실시간-005] - [해외주식] 실시간시세 -> 해외주식 실시간체결통보[실시간-009] + 주문 생성 Args: - callback (Callable[[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]], None]): 콜백 함수 - where (KisEventFilter[KisWebsocketClient, KisSubscriptionEventArgs[KisRealtimeExecution]] | None, optional): 이벤트 필터. Defaults to None. + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 + time_kst (datetime): 주문시간 (한국시간) """ - from pykis.api.websocket.order_execution import on_account_execution - - if event == "execution": - return on_account_execution( - self, - callback=callback, - where=KisMultiEventFilter(self, where) if where else self, - once=True, - ) - - raise ValueError(f"Unknown event: {event}") + return KisSimpleOrder( + account_number=account_number, + symbol=symbol, + market=market, # type: ignore + branch=branch, + number=number, + time_kst=time_kst, + kis=kis, + ) if TYPE_CHECKING: diff --git a/pykis/api/account/order_modify.py b/pykis/api/account/order_modify.py index 2738ef87..ec0329e8 100644 --- a/pykis/api/account/order_modify.py +++ b/pykis/api/account/order_modify.py @@ -6,18 +6,15 @@ ORDER_CONDITION, ORDER_EXECUTION, ORDER_PRICE, - ORDER_QUANTITY, KisOrder, KisOrderBase, KisOrderNumber, - KisOrderNumberBase, ensure_price, order_condition, ) from pykis.api.stock.info import get_market_country from pykis.api.stock.market import DAYTIME_MARKETS, MARKET_TYPE, get_market_code from pykis.api.stock.quote import quote -from pykis.client.account import KisAccountNumber from pykis.client.exceptions import KisAPIError from pykis.responses.response import KisAPIResponse from pykis.responses.types import KisString diff --git a/pykis/api/account/pending_order.py b/pykis/api/account/pending_order.py index 03a1d38e..f50fa2f2 100644 --- a/pykis/api/account/pending_order.py +++ b/pykis/api/account/pending_order.py @@ -3,6 +3,13 @@ from typing import TYPE_CHECKING, Any, Iterable, Protocol, runtime_checkable from zoneinfo import ZoneInfo +from typing_extensions import deprecated + +from pykis.adapter.account_product.order_modify import ( + KisOrderableOrder, + KisOrderableOrderImpl, +) +from pykis.adapter.websocket.execution import KisRealtimeOrderableOrderImpl from pykis.api.account.order import ( ORDER_CONDITION, ORDER_EXECUTION, @@ -10,6 +17,9 @@ ORDER_TYPE, KisOrder, KisOrderNumber, + KisOrderNumberBase, + KisSimpleOrder, + KisSimpleOrderNumber, resolve_domestic_order_condition, ) from pykis.api.base.account import KisAccountBase, KisAccountProtocol @@ -23,15 +33,16 @@ KisMarketType, get_market_code, get_market_code_timezone, - get_market_timezone, ) from pykis.client.account import KisAccountNumber from pykis.client.page import KisPage +from pykis.event.filters.order import KisOrderNumberEventFilter from pykis.responses.dynamic import KisDynamic, KisList from pykis.responses.response import KisPaginationAPIResponse -from pykis.responses.types import KisAny, KisDecimal, KisInt, KisString +from pykis.responses.types import KisAny, KisDecimal, KisString from pykis.utils.repr import kis_repr from pykis.utils.timezone import TIMEZONE +from pykis.utils.typing import Checkable if TYPE_CHECKING: from pykis.kis import PyKis @@ -44,24 +55,9 @@ @runtime_checkable -class KisPendingOrder(KisAccountProductProtocol, Protocol): +class KisPendingOrder(KisOrder, Protocol): """한국투자증권 미체결 주식""" - @property - def time(self) -> datetime: - """주문시각""" - raise NotImplementedError - - @property - def time_kst(self) -> datetime: - """주문시각(KST)""" - raise NotImplementedError - - @property - def timezone(self) -> ZoneInfo: - """시간대""" - raise NotImplementedError - @property def order_number(self) -> KisOrder: """주문번호""" @@ -192,7 +188,9 @@ def __iter__(self) -> Iterable[KisPendingOrder]: "execution", lines="multiple", ) -class KisPendingOrderBase(KisAccountProductBase): +class KisPendingOrderBase( + KisAccountProductBase, KisOrderNumberEventFilter, KisRealtimeOrderableOrderImpl, KisOrderableOrderImpl +): """한국투자증권 미체결 주식""" symbol: str @@ -202,6 +200,16 @@ class KisPendingOrderBase(KisAccountProductBase): account_number: KisAccountNumber """계좌번호""" + @property + def branch(self) -> str: + """지점코드""" + return self.order_number.branch + + @property + def number(self) -> str: + """주문번호""" + return self.order_number.number + time: datetime """주문시각""" time_kst: datetime @@ -209,6 +217,16 @@ class KisPendingOrderBase(KisAccountProductBase): timezone: ZoneInfo """시간대""" + @property + def pending(self) -> bool: + """미체결 여부""" + return True + + @property + def pending_order(self) -> "KisPendingOrder | None": + """미체결 주문""" + return self + order_number: KisOrder """주문번호""" @@ -277,6 +295,84 @@ def pending_qty(self) -> ORDER_QUANTITY: """미체결수량""" return self.pending_quantity + def __init__(self) -> None: + super().__init__(lambda: self) + + @staticmethod + @deprecated("Use KisOrder.from_number() instead") + def from_number( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + ) -> "KisOrderNumber": + """ + 주문번호 생성 + + Args: + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 + """ + return KisSimpleOrderNumber.from_number( + kis=kis, + symbol=symbol, + market=market, + account_number=account_number, + branch=branch, + number=number, + ) + + @staticmethod + @deprecated("Use KisOrder.from_order() instead") + def from_order( + kis: "PyKis", + symbol: str, + market: MARKET_TYPE, + account_number: KisAccountNumber, + branch: str, + number: str, + time_kst: datetime, + ) -> "KisOrder": + """ + 주문 생성 + + Args: + kis (PyKis): 한국투자증권 API + symbol (str): 종목코드 + market (MARKET_TYPE): 상품유형 + account_number (KisAccountNumber): 계좌번호 + branch (str): 지점코드 + number (str): 주문번호 + time_kst (datetime): 주문시간 (한국시간) + """ + return KisSimpleOrder.from_order( + kis=kis, + symbol=symbol, + market=market, + account_number=account_number, + branch=branch, + number=number, + time_kst=time_kst, + ) + + def __eq__(self, value: object | KisOrderNumber) -> bool: + return self.order_number == value + + def __hash__(self) -> int: + return hash(self.order_number) + + +if TYPE_CHECKING: + # IDE Type Checking + Checkable[KisOrderNumber](KisPendingOrderBase) + Checkable[KisOrder](KisPendingOrderBase) + @kis_repr( "account_number", @@ -387,9 +483,7 @@ def __pre_init__(self, data: dict[str, Any]): def __post_init__(self): super().__post_init__() - has_price, self.condition, self.execution = resolve_domestic_order_condition( - self.__data__["ord_dvsn_cd"] - ) + has_price, self.condition, self.execution = resolve_domestic_order_condition(self.__data__["ord_dvsn_cd"]) if not has_price: self.unit_price = None @@ -397,7 +491,7 @@ def __post_init__(self): def __kis_post_init__(self): super().__kis_post_init__() - self.order_number = KisOrder.from_order( + self.order_number = KisSimpleOrder.from_order( kis=self.kis, symbol=self.symbol, market=self.market, @@ -501,7 +595,7 @@ def __post_init__(self): def __kis_post_init__(self): super().__kis_post_init__() - self.order_number = KisOrder.from_order( + self.order_number = KisSimpleOrder.from_order( kis=self.kis, symbol=self.symbol, market=self.market, @@ -831,7 +925,5 @@ def account_product_pending_orders( return KisSimplePendingOrders( account_number=self.account_number, - orders=[ - order for order in orders.orders if order.symbol == self.symbol and order.market == self.market - ], + orders=[order for order in orders.orders if order.symbol == self.symbol and order.market == self.market], ) diff --git a/pykis/api/websocket/order_execution.py b/pykis/api/websocket/order_execution.py index 6e513cfe..e2208e3d 100644 --- a/pykis/api/websocket/order_execution.py +++ b/pykis/api/websocket/order_execution.py @@ -10,6 +10,7 @@ ORDER_TYPE, KisOrder, KisOrderNumber, + KisSimpleOrder, resolve_domestic_order_condition, ) from pykis.api.base.account import KisAccountProtocol @@ -225,9 +226,7 @@ class KisDomesticRealtimeOrderExecution(KisRealtimeExecutionBase): KisAny(KisAccountNumber)["account_number"], # 1 ACNT_NO 계좌번호 None, # 2 ODER_NO 주문번호 None, # 3 OODER_NO 원주문번호 - KisAny(lambda x: "sell" if x == "01" else "buy")[ - "type" - ], # 4 SELN_BYOV_CLS 매도매수구분 01 : 매도 02 : 매수 + KisAny(lambda x: "sell" if x == "01" else "buy")["type"], # 4 SELN_BYOV_CLS 매도매수구분 01 : 매도 02 : 매수 None, # 5 RCTF_CLS 정정구분 None, # 6 ODER_KIND 주문종류 00 : 지정가 01 : 시장가 02 : 조건부지정가 03 : 최유리지정가 04 : 최우선지정가 05 : 장전 시간외 06 : 장후 시간외 07 : 시간외 단일가 08 : 자기주식 09 : 자기주식S-Option 10 : 자기주식금전신탁 11 : IOC지정가 (즉시체결,잔량취소) 12 : FOK지정가 (즉시체결,전량취소) 13 : IOC시장가 (즉시체결,잔량취소) 14 : FOK시장가 (즉시체결,전량취소) 15 : IOC최유리 (즉시체결,잔량취소) 16 : FOK최유리 (즉시체결,전량취소) None, # 7 ODER_COND 주문조건 @@ -323,7 +322,7 @@ def __post_init__(self): def __kis_post_init__(self): super().__kis_post_init__() - self.order_number = KisOrder.from_order( + self.order_number = KisSimpleOrder.from_order( kis=self.kis, symbol=self.symbol, market=self.market, @@ -477,7 +476,7 @@ def __post_init__(self): def __kis_post_init__(self): super().__kis_post_init__() - self.order_number = KisOrder.from_order( + self.order_number = KisSimpleOrder.from_order( kis=self.kis, symbol=self.symbol, market=self.market, diff --git a/pykis/client/websocket.py b/pykis/client/websocket.py index ad01c500..edbdf730 100644 --- a/pykis/client/websocket.py +++ b/pykis/client/websocket.py @@ -30,7 +30,6 @@ KisEventFilter, KisEventHandler, KisEventTicket, - KisLambdaEventFilter, KisMultiEventFilter, ) from pykis.event.subscription import KisSubscribedEventArgs, KisSubscriptionEventArgs @@ -302,9 +301,7 @@ def on( id: str, key: str, callback: Callable[["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]], None], - where: ( - KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]] | None - ) = None, + where: KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]] | None = None, once: bool = False, primary: bool = False, ) -> KisEventTicket["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]]: diff --git a/pykis/event/filters/order.py b/pykis/event/filters/order.py index 45dd75fc..8d620883 100644 --- a/pykis/event/filters/order.py +++ b/pykis/event/filters/order.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, Protocol, overload, runtime_checkable +from typing import TYPE_CHECKING, Callable, Protocol, overload, runtime_checkable from pykis.api.stock.market import MARKET_TYPE from pykis.client.account import KisAccountNumber @@ -78,23 +78,21 @@ def __init__(self, symbol: str, market: MARKET_TYPE, branch: str, number: str, a self.account_number = account -class KisOrderNumberEventFilter( - KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]] -): - - _order: KisSimpleOrderNumberProtocol +class KisOrderNumberEventFilter(KisEventFilter["KisWebsocketClient", KisSubscriptionEventArgs[TWebsocketResponse]]): + _order: KisSimpleOrderNumberProtocol | Callable[..., KisSimpleOrderNumberProtocol] @overload - def __init__( - self, symbol: str, market: MARKET_TYPE, branch: str, number: str, account: KisAccountNumber - ): ... + def __init__(self, symbol: str, /, market: MARKET_TYPE, branch: str, number: str, account: KisAccountNumber): ... @overload def __init__(self, symbol: "KisOrderNumber", /): ... + @overload + def __init__(self, callable: Callable[..., KisSimpleOrderNumberProtocol], /): ... + def __init__( self, - symbol: "KisOrderNumber | str", + symbol_or_callable: "KisOrderNumber | str | Callable[..., KisSimpleOrderNumberProtocol]", market: MARKET_TYPE | None = None, branch: str | None = None, number: str | None = None, @@ -102,7 +100,7 @@ def __init__( ): super().__init__() - if isinstance(symbol, str): + if isinstance(symbol_or_callable, str): if market is None: raise ValueError("market is required") @@ -116,14 +114,14 @@ def __init__( raise ValueError("account is required") self._order = KisSimpleOrderNumber( - symbol=symbol, + symbol=symbol_or_callable, market=market, branch=branch, number=number, account=account, ) else: - self._product = symbol + self._order = symbol_or_callable def __filter__( self, @@ -146,11 +144,12 @@ def __filter__( return True order = e.response.order_number + value = self._order() if callable(self._order) else self._order return not ( - order.symbol == self._order.symbol - and order.market == self._order.market - and order.branch == self._order.branch - and order.number == self._order.number - and order.account_number == self._order.account_number + order.symbol == value.symbol + and order.market == value.market + and order.branch == value.branch + and int(order.number) == int(value.number) + and order.account_number == value.account_number ) diff --git a/pykis/event/handler.py b/pykis/event/handler.py index 7c857d11..3640ce80 100644 --- a/pykis/event/handler.py +++ b/pykis/event/handler.py @@ -158,11 +158,7 @@ def __filter__(self, handler: "KisEventHandler", sender: TSender, e: TEventArgs) if self.where is None: return False - return ( - self.where.__filter__(handler, sender, e) - if isinstance(self.where, KisEventFilter) - else self.where(sender, e) - ) + return self.where.__filter__(handler, sender, e) if isinstance(self.where, KisEventFilter) else self.where(sender, e) def __callback__(self, handler: "KisEventHandler", sender: TSender, e: TEventArgs): if self.once: