Skip to content

Commit d78c9c7

Browse files
committed
Handle Decimal conversion for manual holdings quantity and add test coverage
- Update `kis.py` to ensure `ord_psbl_qty` or `hldg_qty` values are converted to integers after passing through float to handle Decimal cases. - Add new tests for validating proper Decimal-to-int conversion in manual holdings integration scenarios. - Include mocks and assertions to verify correct quantity type and value during sell automation tasks.
1 parent 9e5ef5e commit d78c9c7

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

app/tasks/kis.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,8 @@ async def _run() -> dict:
475475
current_price = float(stock.get('prpr', 0))
476476
# 매도 시 미체결 주문을 제외한 주문 가능 수량(ord_psbl_qty)을 사용
477477
# ord_psbl_qty가 없으면 hldg_qty를 fallback으로 사용
478-
qty = int(stock.get('ord_psbl_qty', stock.get('hldg_qty', 0)))
478+
# 수동 잔고의 경우 Decimal이 str로 변환되어 소수점이 있을 수 있으므로 float을 거쳐 int로 변환
479+
qty = int(float(stock.get('ord_psbl_qty', stock.get('hldg_qty', 0))))
479480
is_manual = stock.get('_is_manual', False)
480481

481482
# 수동 잔고 종목인 경우 현재가를 API로 조회

tests/test_kis_manual_holdings_integration.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,3 +283,83 @@ async def fake_sell(kis, symbol, current_price, avg_price, qty):
283283

284284
# 현재가 조회 API가 호출되어야 함
285285
assert "005930" in price_fetch_calls, "수동 잔고 종목의 현재가를 조회해야 함"
286+
287+
def test_manual_holdings_decimal_conversion(self, monkeypatch):
288+
"""Decimal 타입의 수량/가격이 올바르게 변환되는지 확인"""
289+
from app.tasks import kis as kis_tasks
290+
from decimal import Decimal
291+
292+
class DummyAnalyzer:
293+
async def analyze_stock_json(self, name):
294+
return {"decision": "hold", "confidence": 65}, "gemini-2.5-pro"
295+
296+
async def close(self):
297+
return None
298+
299+
class DummyKIS:
300+
async def fetch_my_stocks(self):
301+
return [] # KIS에는 보유 종목 없음
302+
303+
async def inquire_korea_orders(self, *args, **kwargs):
304+
return []
305+
306+
async def cancel_korea_order(self, *args, **kwargs):
307+
return {"odno": "0000001"}
308+
309+
async def fetch_fundamental_info(self, code):
310+
return {"종목명": "삼성전자", "현재가": 72000}
311+
312+
# Mock manual holding with Decimal values that have many decimal places
313+
manual_holding = MagicMock()
314+
manual_holding.ticker = "005930"
315+
manual_holding.display_name = "삼성전자"
316+
manual_holding.quantity = Decimal("2.00000000") # 소수점 많은 Decimal
317+
manual_holding.avg_price = Decimal("70000.00000000") # 소수점 많은 Decimal
318+
319+
class MockManualService:
320+
def __init__(self, db):
321+
pass
322+
323+
async def get_holdings_by_user(self, user_id, market_type):
324+
return [manual_holding]
325+
326+
sell_calls = []
327+
328+
async def fake_buy(kis, symbol, current_price, avg_price):
329+
return {"success": False, "message": "조건 미충족", "orders_placed": 0}
330+
331+
async def fake_sell(kis, symbol, current_price, avg_price, qty):
332+
# qty가 올바르게 정수로 전달되는지 확인
333+
sell_calls.append({"symbol": symbol, "qty": qty, "qty_type": type(qty).__name__})
334+
return {"success": False, "message": "조건 미충족", "orders_placed": 0}
335+
336+
# Mock DB session
337+
mock_db_session = MagicMock()
338+
mock_db_session.__aenter__ = AsyncMock(return_value=MagicMock())
339+
mock_db_session.__aexit__ = AsyncMock(return_value=None)
340+
341+
with patch('app.core.db.AsyncSessionLocal', return_value=mock_db_session), \
342+
patch('app.services.manual_holdings_service.ManualHoldingsService', MockManualService):
343+
344+
monkeypatch.setattr(kis_tasks, "KISClient", DummyKIS)
345+
monkeypatch.setattr(kis_tasks, "KISAnalyzer", DummyAnalyzer)
346+
monkeypatch.setattr(kis_tasks, "process_kis_domestic_buy_orders_with_analysis", fake_buy)
347+
monkeypatch.setattr(kis_tasks, "process_kis_domestic_sell_orders_with_analysis", fake_sell)
348+
monkeypatch.setattr(
349+
kis_tasks.run_per_domestic_stock_automation,
350+
"update_state",
351+
lambda *_, **__: None,
352+
raising=False,
353+
)
354+
355+
result = kis_tasks.run_per_domestic_stock_automation.apply().result
356+
357+
# 태스크가 성공적으로 완료되어야 함
358+
assert result["status"] == "completed"
359+
360+
# 매도 함수가 호출되어야 함
361+
assert len(sell_calls) == 1, "매도 함수가 호출되어야 함"
362+
363+
# qty가 정수 타입이어야 함
364+
assert sell_calls[0]["qty_type"] == "int", f"수량은 int 타입이어야 하는데 {sell_calls[0]['qty_type']} 타입임"
365+
assert sell_calls[0]["qty"] == 2, f"수량은 2여야 하는데 {sell_calls[0]['qty']}임"

0 commit comments

Comments
 (0)