From 68c67a4d22f270ca4224320d416aa4806f0e8414 Mon Sep 17 00:00:00 2001 From: "marek.galvanek" Date: Fri, 27 Mar 2026 16:36:16 +0100 Subject: [PATCH] fix: add debt ratio handling and various portfolio metrics to Debank API and models, expand test coverage --- .../debank/test_debank_portfolio_parser.py | 38 +++++++++++++++++++ blockapi/v2/api/debank.py | 11 ++++++ blockapi/v2/models.py | 33 +++++++++++++++- 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/blockapi/test/v2/api/debank/test_debank_portfolio_parser.py b/blockapi/test/v2/api/debank/test_debank_portfolio_parser.py index 5f5201f4..6f6c32ea 100644 --- a/blockapi/test/v2/api/debank/test_debank_portfolio_parser.py +++ b/blockapi/test/v2/api/debank/test_debank_portfolio_parser.py @@ -132,6 +132,44 @@ def test_parse_pool_names(portfolio_parser, tokenset_portfolio_response): assert parsed[1].pool_info.tokens == ['ETH', 'USDC'] +def test_portfolio_detail_types(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.detail_types == ['lending'] + + +def test_portfolio_asset_usd_value(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.asset_usd_value == Decimal('547045.4515305705') + + +def test_portfolio_debt_usd_value(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.debt_usd_value == Decimal('0') + + +def test_portfolio_net_usd_value(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.net_usd_value == Decimal('547045.4515305705') + + +def test_portfolio_update_at(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.update_at is not None + assert isinstance(pool.update_at, datetime) + + +def test_portfolio_debt_ratio_none_when_absent(portfolio_parser, portfolio_response): + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.debt_ratio is None + + +def test_portfolio_debt_ratio_when_present(portfolio_parser, portfolio_response): + """debt_ratio appears for leveraged_farming positions.""" + portfolio_response['portfolio_item_list'][0]['detail']['debt_ratio'] = 0.65 + pool = portfolio_parser.parse([portfolio_response])[0] + assert pool.debt_ratio == Decimal('0.65') + + def test_require_pool_or_pool_id(): with pytest.raises(ValueError, match="either pool or pool_id must have a value"): detail = DebankModelPoolItemDetail() diff --git a/blockapi/v2/api/debank.py b/blockapi/v2/api/debank.py index f9f01145..1ea68fb7 100644 --- a/blockapi/v2/api/debank.py +++ b/blockapi/v2/api/debank.py @@ -36,6 +36,7 @@ DebankApp, DebankModelApp, DebankModelAppPortfolioItem, + DebankModelAppStats, DebankModelPredictionDetail, DebankPrediction, FetchResult, @@ -70,6 +71,7 @@ class DebankModelPoolItemDetail(BaseModel): description: Optional[str] = None health_rate: Optional[float] = None unlock_at: Optional[float] = None + debt_ratio: Optional[float] = None token_list: Optional[list[dict]] = None supply_token_list: Optional[list[dict]] = None borrow_token_list: Optional[list[dict]] = None @@ -90,6 +92,9 @@ class DebankModelPortfolioItem(BaseModel): pool_id: Optional[str] = None pool: Optional[DebankModelPoolItem] = None position_index: Optional[str] = None + stats: DebankModelAppStats + detail_types: list[str] + update_at: float @validator('pool') def require_pool_or_pool_id(cls, v, values, **kwargs): @@ -481,6 +486,12 @@ def _parse_portfolio_item( locked_until=locked_until, health_rate=health_rate, items=[], + detail_types=item.detail_types, + asset_usd_value=item.stats.asset_usd_value, + debt_usd_value=item.stats.debt_usd_value, + net_usd_value=item.stats.net_usd_value, + debt_ratio=detail.debt_ratio, + update_at=item.update_at, ) items = list(self._parse_balances(detail, item, pool.pool_info)) diff --git a/blockapi/v2/models.py b/blockapi/v2/models.py index 66c715f0..99616a14 100644 --- a/blockapi/v2/models.py +++ b/blockapi/v2/models.py @@ -1163,6 +1163,19 @@ def from_api( ) +# uint256.max / 1e18 — Aave's sentinel for "no debt / infinite health factor" +_HEALTH_RATE_SENTINEL_THRESHOLD = Decimal('1e18') + + +def _normalize_health_rate(value) -> Optional[Decimal]: + if value is None: + return None + d = to_decimal(value) + if d >= _HEALTH_RATE_SENTINEL_THRESHOLD: + return None + return d + + @attr.s(auto_attribs=True, slots=True, frozen=True) class Pool: pool_info: PoolInfo @@ -1170,6 +1183,12 @@ class Pool: items: List[BalanceItem] locked_until: Optional[datetime] = attr.ib(default=None) health_rate: Optional[Decimal] = attr.ib(default=None) + detail_types: List[str] = attr.ib(factory=list) + asset_usd_value: Decimal = attr.ib(default=Decimal('0')) + debt_usd_value: Decimal = attr.ib(default=Decimal('0')) + net_usd_value: Decimal = attr.ib(default=Decimal('0')) + debt_ratio: Optional[Decimal] = attr.ib(default=None) + update_at: Optional[datetime] = attr.ib(default=None) @classmethod def from_api( @@ -1180,13 +1199,25 @@ def from_api( locked_until: Optional[Union[int, str, float]] = None, health_rate: Optional[Union[float, str]] = None, items: List[BalanceItem], + detail_types: Optional[List[str]] = None, + asset_usd_value: Union[float, str] = 0, + debt_usd_value: Union[float, str] = 0, + net_usd_value: Union[float, str] = 0, + debt_ratio: Optional[Union[float, str]] = None, + update_at: Optional[Union[int, str, float]] = None, ) -> 'Pool': return cls( pool_info=pool_info, protocol=protocol, items=items, locked_until=(parse_dt(locked_until) if locked_until is not None else None), - health_rate=to_decimal(health_rate) if health_rate is not None else None, + health_rate=_normalize_health_rate(health_rate), + detail_types=detail_types or [], + asset_usd_value=to_decimal(asset_usd_value), + debt_usd_value=to_decimal(debt_usd_value), + net_usd_value=to_decimal(net_usd_value), + debt_ratio=to_decimal(debt_ratio) if debt_ratio is not None else None, + update_at=(parse_dt(update_at) if update_at is not None else None), ) def append_items(self, items: List[BalanceItem]) -> None: