Skip to content

Commit

Permalink
feat: implement fetch order with order_id
Browse files Browse the repository at this point in the history
  • Loading branch information
zeroam committed Nov 13, 2024
1 parent 84056e0 commit 3b08c24
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 108 deletions.
21 changes: 14 additions & 7 deletions kispy/client.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import logging
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal
from typing import Literal

from kispy.auth import KisAuth
from kispy.constants import (
OHLCV,
PERIOD_TO_MINUTES,
REAL_URL,
VIRTUAL_URL,
AccountSummary,
Balance,
ExchangeLongCodeMap,
Nation,
PendingOrder,
Period,
Position,
Symbol,
)
from kispy.domestic_stock import DomesticStock
from kispy.models.account import AccountSummary, Balance, Order, PendingOrder, Position
from kispy.models.market import OHLCV, Symbol
from kispy.overseas_stock import OverseasStock
from kispy.utils import get_symbol_map

Expand Down Expand Up @@ -146,6 +142,17 @@ def fetch_pending_orders(self) -> list[PendingOrder]:
)
return result

def fetch_order(self, order_id: str, lookback_days: int = 30) -> Order | None:
now = datetime.now()
start_date = (now - timedelta(days=lookback_days)).strftime("%Y%m%d")
end_date = now.strftime("%Y%m%d")
orders = self.client.overseas_stock.order.inquire_orders(start_date, end_date, order_id)
if not orders:
return None

order = orders[0]
return Order.from_response(order)

def fetch_account_summary(self) -> AccountSummary:
"""총 자산 정보를 조회"""
balance = self.fetch_balance()
Expand Down
97 changes: 2 additions & 95 deletions kispy/constants.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
from dataclasses import asdict, dataclass
from datetime import datetime
from decimal import Decimal
from typing import Any, Literal, Self
from typing import Literal

from zoneinfo import ZoneInfo

Expand All @@ -17,6 +14,7 @@
LongExchangeCode = Literal["NASD", "NYSE", "AMEX", "SEHK", "SHAA", "SZAA", "TKSE", "HASE", "VNSE"]
Currency = Literal["USD", "HKD", "CNY", "JPY", "VND"]
OrderSide = Literal["buy", "sell"]
OrderStatus = Literal["open", "canceled", "closed", "rejected"]

NationExchangeCodeMap: dict[Nation, list[ExchangeCode]] = {
"US": ["NAS", "NYS", "AMS"],
Expand Down Expand Up @@ -82,94 +80,3 @@
"2h": "120",
"4h": "240",
}


@dataclass
class Symbol:
symbol: str
exchange_code: ExchangeCode
realtime_symbol: str


@dataclass
class OHLCV:
date: datetime
open: str
high: str
low: str
close: str
volume: str

def to_dict(self) -> dict[str, Any]:
return asdict(self)


@dataclass
class Balance:
available_balance: str # 주문가능외화금액
buyable_balance: str # 수수료까지 고려한 매수가능외화금액 (거래수수료 0.25% 포함)
exchange_rate: str # 환율
currency: Currency # 통화


@dataclass
class Position:
symbol: str # 종목코드
item_name: str # 종목명
quantity: str # 보유수량
average_price: str # 평균단가
unrealized_pnl: str # 외화평가손익금액
pnl_percentage: str # 평가손익율(%)
current_price: str # 현재가격
market_value: str # 평가금액


@dataclass
class PendingOrder:
order_id: str # 주문번호
symbol: str # 종목코드
side: OrderSide # 주문유형
requested_price: str # 주문가격
requested_quantity: str # 주문수량
filled_amount: str # 체결수량
remaining_quantity: str # 미체결수량
average_price: str # 체결가격
order_amount: str # 체결금액
locked_amount: str # 주문중금액


@dataclass
class AccountSummary:
total_balance: str # 총 자산
locked_balance: str # 주문중금액
available_balance: str # 주문가능외화금액
buyable_balance: str # 수수료까지 고려한 매수가능외화금액 (거래수수료 0.25% 포함)
exchange_rate: str # 환율
currency: Currency # 통화
total_unrealized_pnl: str # 총 외화평가손익금액
total_pnl_percentage: str # 총 평가손익율(%)
positions: list[Position]
pending_orders: list[PendingOrder]

@classmethod
def create(cls, balance: Balance, positions: list[Position], pending_orders: list[PendingOrder]) -> Self:
# TODO: 원화 예수금 조회
available_balance = Decimal(balance.available_balance)
total_position_market_value = sum(Decimal(position.market_value) for position in positions)
total_position_price = sum(Decimal(position.average_price) * Decimal(position.quantity) for position in positions)
total_locked_balance = sum(Decimal(order.locked_amount) for order in pending_orders)
total_balance = available_balance + total_position_market_value + total_locked_balance
total_unrealized_pnl = sum(Decimal(position.unrealized_pnl) for position in positions)
total_pnl_percentage = (total_position_market_value - total_position_price) / total_position_price * 100
return cls(
total_balance=str(total_balance),
locked_balance=str(total_locked_balance),
available_balance=str(available_balance),
buyable_balance=str(balance.buyable_balance),
exchange_rate=balance.exchange_rate,
currency=balance.currency,
total_unrealized_pnl=str(total_unrealized_pnl),
total_pnl_percentage=f"{total_pnl_percentage:.2f}",
positions=positions,
pending_orders=pending_orders,
)
Empty file added kispy/models/__init__.py
Empty file.
125 changes: 125 additions & 0 deletions kispy/models/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
from dataclasses import dataclass
from datetime import datetime
from decimal import Decimal
from typing import Any, Self

from kispy.constants import Currency, OrderSide, OrderStatus
from kispy.models.base import BaseModel


@dataclass
class Balance:
available_balance: str # 주문가능외화금액
buyable_balance: str # 수수료까지 고려한 매수가능외화금액 (거래수수료 0.25% 포함)
exchange_rate: str # 환율
currency: Currency # 통화


@dataclass
class Position:
symbol: str # 종목코드
item_name: str # 종목명
quantity: str # 보유수량
average_price: str # 평균단가
unrealized_pnl: str # 외화평가손익금액
pnl_percentage: str # 평가손익율(%)
current_price: str # 현재가격
market_value: str # 평가금액


@dataclass
class PendingOrder:
order_id: str # 주문번호
symbol: str # 종목코드
side: OrderSide # 주문유형
requested_price: str # 주문가격
requested_quantity: str # 주문수량
filled_amount: str # 체결수량
remaining_quantity: str # 미체결수량
average_price: str # 체결가격
order_amount: str # 체결금액
locked_amount: str # 주문중금액


@dataclass
class AccountSummary:
total_balance: str # 총 자산
locked_balance: str # 주문중금액
available_balance: str # 주문가능외화금액
buyable_balance: str # 수수료까지 고려한 매수가능외화금액 (거래수수료 0.25% 포함)
exchange_rate: str # 환율
currency: Currency # 통화
total_unrealized_pnl: str # 총 외화평가손익금액
total_pnl_percentage: str # 총 평가손익율(%)
positions: list[Position]
pending_orders: list[PendingOrder]

@classmethod
def create(cls, balance: Balance, positions: list[Position], pending_orders: list[PendingOrder]) -> Self:
# TODO: 원화 예수금 조회
available_balance = Decimal(balance.available_balance)
total_position_market_value = sum(Decimal(position.market_value) for position in positions)
total_position_price = sum(Decimal(position.average_price) * Decimal(position.quantity) for position in positions)
total_locked_balance = sum(Decimal(order.locked_amount) for order in pending_orders)
total_balance = available_balance + total_position_market_value + total_locked_balance
total_unrealized_pnl = sum(Decimal(position.unrealized_pnl) for position in positions)
total_pnl_percentage = (total_position_market_value - total_position_price) / total_position_price * 100
return cls(
total_balance=str(total_balance),
locked_balance=str(total_locked_balance),
available_balance=str(available_balance),
buyable_balance=str(balance.buyable_balance),
exchange_rate=balance.exchange_rate,
currency=balance.currency,
total_unrealized_pnl=str(total_unrealized_pnl),
total_pnl_percentage=f"{total_pnl_percentage:.2f}",
positions=positions,
pending_orders=pending_orders,
)


@dataclass
class Order(BaseModel):
order_id: str
symbol: str
side: OrderSide
status: OrderStatus
requested_price: str # 주문가격
requested_quantity: str # 주문수량
filled_quantity: str # 체결수량
average_price: str # 체결가격
filled_amount: str # 체결금액
reject_reason: str # 거부사유
order_date: datetime # 주문일시

@classmethod
def from_response(cls, response: dict[str, Any]) -> Self:
order_date = datetime.strptime(response["ord_dt"] + response["ord_tmd"], "%Y%m%d%H%M%S")
process_status = response["prcs_stat_name"]
reject_reason = response["rjct_rson_name"]
revise_cancel_status = response["rvse_cncl_dvsn_name"]
status: OrderStatus = "open"
if process_status == "완료":
if reject_reason:
status = "rejected"
elif revise_cancel_status == "취소":
# TODO: 실제로 취소 검증 필요
status = "canceled"
else:
status = "closed"
elif process_status == "거부":
status = "rejected"

return cls(
order_id=response["odno"],
symbol=response["pdno"],
side="buy" if response["sll_buy_dvsn_cd_name"] == "매수" else "sell",
status=status,
requested_price=response["ft_ord_unpr3"],
requested_quantity=response["ft_ord_qty"],
filled_quantity=response["ft_ccld_qty"],
average_price=response["ft_ccld_unpr3"],
filled_amount=response["ft_ccld_amt3"],
reject_reason=reject_reason,
order_date=order_date,
)
10 changes: 10 additions & 0 deletions kispy/models/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from abc import ABC, abstractmethod
from typing import Any, Self


class BaseModel(ABC):
@classmethod
@abstractmethod
def from_response(cls, item: dict[str, Any]) -> Self:
"""API 응답으로부터 모델 인스턴스를 생성"""
pass
25 changes: 25 additions & 0 deletions kispy/models/market.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from dataclasses import asdict, dataclass
from datetime import datetime
from typing import Any

from kispy.constants import ExchangeCode


@dataclass
class Symbol:
symbol: str
exchange_code: ExchangeCode
realtime_symbol: str


@dataclass
class OHLCV:
date: datetime
open: str
high: str
low: str
close: str
volume: str

def to_dict(self) -> dict[str, Any]:
return asdict(self)
3 changes: 2 additions & 1 deletion kispy/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import requests

from kispy.constants import ExchangeCode, Nation, NationExchangeCodeMap, Symbol
from kispy.constants import ExchangeCode, Nation, NationExchangeCodeMap
from kispy.models.market import Symbol


def get_overseas_master_data(exchange_code: ExchangeCode) -> list[dict]:
Expand Down
Empty file added tests/models/__init__.py
Empty file.
Loading

0 comments on commit 3b08c24

Please sign in to comment.