diff --git a/pyproject.toml b/pyproject.toml index 0992376..39ba4ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pyrb" -version = "0.9.4" +version = "0.9.5" description = "Python Rebalancer" authors = ["Minki Kim "] readme = "README.md" diff --git a/pyrb/controllers/cli/main.py b/pyrb/controllers/cli/main.py index 72725d4..6082f94 100644 --- a/pyrb/controllers/cli/main.py +++ b/pyrb/controllers/cli/main.py @@ -229,7 +229,7 @@ def _print_portfolio_table(context: RebalanceContext) -> None: rtn_style = "blue" table.add_row( - position.symbol, + position.asset.symbol, _format(position.quantity, "number"), _format(position.sellable_quantity, "number"), _format(position.average_buy_price, "currency"), diff --git a/pyrb/enums.py b/pyrb/enums.py index f3fb331..d15b70d 100644 --- a/pyrb/enums.py +++ b/pyrb/enums.py @@ -23,3 +23,11 @@ class OrderSide(StrEnum): class AssetAllocationStrategyEnum(StrEnum): ALL_WEATHER_KR = "all-weather-kr" + + +class AssetClassEnum(StrEnum): + STOCK = "STOCK" + BOND = "BOND" + CASH = "CASH" + COMMODITY = "COMMODITY" + OTHER = "OTHER" diff --git a/pyrb/models/position.py b/pyrb/models/position.py index 1856b6f..6631379 100644 --- a/pyrb/models/position.py +++ b/pyrb/models/position.py @@ -1,8 +1,30 @@ -from pydantic import BaseModel, PositiveFloat, PositiveInt +from pydantic import BaseModel, PositiveFloat, PositiveInt, computed_field +from pyrb.enums import AssetClassEnum -class Position(BaseModel): +asset_class_by_symbols: dict[str, AssetClassEnum] = { + "361580": AssetClassEnum.STOCK, # KBSTAR 200TR + "379800": AssetClassEnum.STOCK, # KODEX 미국S&P500TR + "411060": AssetClassEnum.COMMODITY, # ACE KRX금현물 + "365780": AssetClassEnum.BOND, # ACE 국고채10년 + "308620": AssetClassEnum.BOND, # KODEX 미국채10년선물 + "272580": AssetClassEnum.CASH, # TIGER 단기채권액티브 + "005930": AssetClassEnum.STOCK, # 삼성전자 + "000660": AssetClassEnum.STOCK, # SK하이닉스 +} + + +class Asset(BaseModel): symbol: str # 종목코드 + label: str # 종목명 + + @computed_field + def asset_class(self) -> str: + return asset_class_by_symbols.get(self.symbol, AssetClassEnum.OTHER) + + +class Position(BaseModel): + asset: Asset # 종목코드 quantity: PositiveInt # 보유수량 sellable_quantity: PositiveInt # 매도가능수량 average_buy_price: PositiveFloat # 매입단가 diff --git a/pyrb/repositories/brokerages/ebest/portfolio.py b/pyrb/repositories/brokerages/ebest/portfolio.py index 9b179ca..2c1da1e 100644 --- a/pyrb/repositories/brokerages/ebest/portfolio.py +++ b/pyrb/repositories/brokerages/ebest/portfolio.py @@ -2,7 +2,7 @@ from pydantic import NonNegativeFloat -from pyrb.models.position import Position +from pyrb.models.position import Asset, Position from pyrb.repositories.brokerages.base.portfolio import Portfolio from pyrb.repositories.brokerages.ebest.client import EbestAPIClient @@ -24,7 +24,7 @@ def cash_balance(self) -> NonNegativeFloat: def positions(self) -> list[Position]: positions = [ Position( - symbol=item["expcode"], + asset=Asset(symbol=item["expcode"], label=item["hname"]), quantity=item["janqty"], sellable_quantity=item["mdposqt"], average_buy_price=item["pamt"], @@ -38,10 +38,12 @@ def positions(self) -> list[Position]: @property def holding_symbols(self) -> list[str]: - return [position.symbol for position in self.positions] + return [position.asset.symbol for position in self.positions] def get_position(self, symbol: str) -> Position | None: - return next((position for position in self.positions if position.symbol == symbol), None) + return next( + (position for position in self.positions if position.asset.symbol == symbol), None + ) def get_position_amount(self, symbol: str) -> NonNegativeFloat: position = self.get_position(symbol) diff --git a/tests/conftest.py b/tests/conftest.py index e572ab7..bec15d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import pytest from pyrb.models.order import Order -from pyrb.models.position import Position +from pyrb.models.position import Asset, Position from pyrb.models.price import CurrentPrice from pyrb.repositories.account import AccountRepository, LocalConfigAccountRepository from pyrb.repositories.brokerages.base.fetcher import PriceFetcher @@ -30,7 +30,7 @@ def cash_balance(self) -> float: def positions(self) -> list[Position]: return [ Position( - symbol="000660", + asset=Asset(symbol="000660", label="SK하이닉스"), quantity=100, sellable_quantity=100, average_buy_price=100, @@ -38,7 +38,7 @@ def positions(self) -> list[Position]: rtn=0.0, ), Position( - symbol="005930", + asset=Asset(symbol="005930", label="삼성전자"), quantity=50, sellable_quantity=50, average_buy_price=150, @@ -49,10 +49,12 @@ def positions(self) -> list[Position]: @property def holding_symbols(self) -> list[str]: - return [position.symbol for position in self.positions] + return [position.asset.symbol for position in self.positions] def get_position(self, symbol: str) -> Position | None: - return next((position for position in self.positions if position.symbol == symbol), None) + return next( + (position for position in self.positions if position.asset.symbol == symbol), None + ) def get_position_amount(self, symbol: str) -> float: position = self.get_position(symbol) diff --git a/tests/controllers/test_api.py b/tests/controllers/test_api.py index 3656c4c..bc23100 100644 --- a/tests/controllers/test_api.py +++ b/tests/controllers/test_api.py @@ -99,7 +99,7 @@ def test_get_portfolio(fake_rebalance_context: RebalanceContext) -> None: "cash_balance": 0, "positions": [ { - "symbol": "000660", + "asset": {"symbol": "000660", "label": "SK하이닉스", "asset_class": "STOCK"}, "quantity": 100, "sellable_quantity": 100, "average_buy_price": 100, @@ -107,7 +107,7 @@ def test_get_portfolio(fake_rebalance_context: RebalanceContext) -> None: "rtn": 0.0, }, { - "symbol": "005930", + "asset": {"symbol": "005930", "label": "삼성전자", "asset_class": "STOCK"}, "quantity": 50, "sellable_quantity": 50, "average_buy_price": 150,