Skip to content

Commit 439c947

Browse files
committed
Add overseas stock price retrieval and expand test coverage
- Implement `_fetch_missing_prices` update to query overseas stock prices using the KIS API. - Add tests for various scenarios: successful retrieval, skipping already priced stocks, handling API errors, and multiple stock updates. - Ensure robust logging and graceful error handling for API failures.
1 parent fca0c05 commit 439c947

File tree

2 files changed

+127
-2
lines changed

2 files changed

+127
-2
lines changed

app/services/merged_portfolio_service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,10 @@ async def _fetch_missing_prices(
279279
if not df.empty:
280280
merged[ticker].current_price = float(df.iloc[0]["close"])
281281
else:
282-
# 해외주식의 경우 별도 API 필요 (추후 구현)
283-
pass
282+
# 해외주식 현재가 조회
283+
df = await kis_client.inquire_overseas_price(ticker)
284+
if not df.empty:
285+
merged[ticker].current_price = float(df.iloc[0]["close"])
284286
except Exception as exc:
285287
logger.warning(
286288
"Failed to fetch price for %s: %s", ticker, exc

tests/test_merged_portfolio_service.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,3 +642,126 @@ def test_to_dict_with_all_fields(self):
642642
assert result["evaluation"] == 11670000.0
643643
assert result["analysis_id"] == 123
644644
assert len(result["holdings"]) == 1
645+
646+
647+
class TestFetchMissingPricesOverseas:
648+
"""해외주식 _fetch_missing_prices 테스트 - TOSS 전용 해외 종목 현재가 조회"""
649+
650+
@pytest.mark.asyncio
651+
async def test_fetch_overseas_price_for_toss_only_stock(
652+
self, merged_portfolio_service, mock_kis_client
653+
):
654+
"""TOSS만 보유한 해외 종목의 현재가 조회"""
655+
# 현재가가 0인 TOSS 전용 해외 종목
656+
merged = {
657+
"CONY": MergedHolding(
658+
ticker="CONY",
659+
name="CONY",
660+
market_type="US",
661+
current_price=0.0,
662+
total_quantity=20,
663+
toss_quantity=20,
664+
toss_avg_price=17.18,
665+
holdings=[HoldingInfo(broker="toss", quantity=20, avg_price=17.18)],
666+
)
667+
}
668+
669+
# KIS API 해외주식 현재가 응답 Mock
670+
price_df = pd.DataFrame([{"close": 18.50}])
671+
mock_kis_client.inquire_overseas_price = AsyncMock(return_value=price_df)
672+
673+
await merged_portfolio_service._fetch_missing_prices(
674+
merged, MarketType.US, mock_kis_client
675+
)
676+
677+
assert merged["CONY"].current_price == 18.50
678+
mock_kis_client.inquire_overseas_price.assert_called_once_with("CONY")
679+
680+
@pytest.mark.asyncio
681+
async def test_fetch_overseas_price_multiple_stocks(
682+
self, merged_portfolio_service, mock_kis_client
683+
):
684+
"""여러 TOSS 전용 해외 종목의 현재가 조회"""
685+
merged = {
686+
"CONY": MergedHolding(
687+
ticker="CONY",
688+
name="CONY",
689+
market_type="US",
690+
current_price=0.0,
691+
total_quantity=20,
692+
),
693+
"BRK-B": MergedHolding(
694+
ticker="BRK-B",
695+
name="버크셔 해서웨이 B",
696+
market_type="US",
697+
current_price=0.0,
698+
total_quantity=5,
699+
),
700+
}
701+
702+
# 각 종목별 현재가 응답
703+
def mock_inquire_overseas_price(ticker):
704+
prices = {"CONY": 18.50, "BRK-B": 474.17}
705+
return pd.DataFrame([{"close": prices[ticker]}])
706+
707+
mock_kis_client.inquire_overseas_price = AsyncMock(
708+
side_effect=mock_inquire_overseas_price
709+
)
710+
711+
await merged_portfolio_service._fetch_missing_prices(
712+
merged, MarketType.US, mock_kis_client
713+
)
714+
715+
assert merged["CONY"].current_price == 18.50
716+
assert merged["BRK-B"].current_price == 474.17
717+
assert mock_kis_client.inquire_overseas_price.call_count == 2
718+
719+
@pytest.mark.asyncio
720+
async def test_skip_overseas_stocks_with_price(
721+
self, merged_portfolio_service, mock_kis_client
722+
):
723+
"""현재가가 이미 있는 해외 종목은 조회하지 않음"""
724+
merged = {
725+
"TSLA": MergedHolding(
726+
ticker="TSLA",
727+
name="Tesla",
728+
market_type="US",
729+
current_price=250.0, # 이미 현재가 있음
730+
total_quantity=3,
731+
)
732+
}
733+
734+
mock_kis_client.inquire_overseas_price = AsyncMock()
735+
736+
await merged_portfolio_service._fetch_missing_prices(
737+
merged, MarketType.US, mock_kis_client
738+
)
739+
740+
mock_kis_client.inquire_overseas_price.assert_not_called()
741+
742+
@pytest.mark.asyncio
743+
async def test_handle_overseas_api_error_gracefully(
744+
self, merged_portfolio_service, mock_kis_client
745+
):
746+
"""해외주식 API 오류 시 로그만 남기고 계속 진행"""
747+
merged = {
748+
"INVALID": MergedHolding(
749+
ticker="INVALID",
750+
name="존재하지 않는 종목",
751+
market_type="US",
752+
current_price=0.0,
753+
total_quantity=10,
754+
)
755+
}
756+
757+
mock_kis_client.inquire_overseas_price = AsyncMock(
758+
side_effect=Exception("API Error: 해당종목정보가 없습니다")
759+
)
760+
761+
# 예외 발생해도 에러 없이 진행
762+
await merged_portfolio_service._fetch_missing_prices(
763+
merged, MarketType.US, mock_kis_client
764+
)
765+
766+
# 현재가는 0으로 유지
767+
assert merged["INVALID"].current_price == 0.0

0 commit comments

Comments
 (0)