Skip to content

Commit 4f18cce

Browse files
committed
Add ord_psbl_qty field for manual holdings and expand test coverage
- Update `kis.py` to set `ord_psbl_qty` equal to `hldg_qty` for manual holdings as they have no pending orders. - Add integration test to validate accurate `ord_psbl_qty` assignment and proper handling in sell tasks. - Include mocks and assertions to ensure correct automation behavior and quantity consistency.
1 parent d78c9c7 commit 4f18cce

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

app/tasks/kis.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,10 +448,13 @@ async def _run() -> dict:
448448
continue
449449

450450
# 수동 잔고 종목을 my_stocks에 추가 (한투 형식으로 변환)
451+
# 수동 잔고는 미체결 주문이 없으므로 ord_psbl_qty = hldg_qty
452+
qty_str = str(holding.quantity)
451453
my_stocks.append({
452454
'pdno': ticker,
453455
'prdt_name': holding.display_name or ticker,
454-
'hldg_qty': str(holding.quantity),
456+
'hldg_qty': qty_str,
457+
'ord_psbl_qty': qty_str, # 수동 잔고는 미체결 없음
455458
'pchs_avg_pric': str(holding.avg_price),
456459
'prpr': str(holding.avg_price), # 현재가는 나중에 API로 조회
457460
'_is_manual': True # 수동 잔고 표시

tests/test_kis_manual_holdings_integration.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,3 +363,81 @@ async def fake_sell(kis, symbol, current_price, avg_price, qty):
363363
# qty가 정수 타입이어야 함
364364
assert sell_calls[0]["qty_type"] == "int", f"수량은 int 타입이어야 하는데 {sell_calls[0]['qty_type']} 타입임"
365365
assert sell_calls[0]["qty"] == 2, f"수량은 2여야 하는데 {sell_calls[0]['qty']}임"
366+
367+
def test_manual_holdings_has_orderable_qty(self, monkeypatch):
368+
"""수동 잔고에 ord_psbl_qty 필드가 올바르게 설정되는지 확인"""
369+
from app.tasks import kis as kis_tasks
370+
from decimal import Decimal
371+
372+
class DummyAnalyzer:
373+
async def analyze_stock_json(self, name):
374+
return {"decision": "hold", "confidence": 65}, "gemini-2.5-pro"
375+
376+
async def close(self):
377+
return None
378+
379+
class DummyKIS:
380+
async def fetch_my_stocks(self):
381+
return [] # KIS에는 보유 종목 없음
382+
383+
async def inquire_korea_orders(self, *args, **kwargs):
384+
return []
385+
386+
async def cancel_korea_order(self, *args, **kwargs):
387+
return {"odno": "0000001"}
388+
389+
async def fetch_fundamental_info(self, code):
390+
return {"종목명": "삼성전자", "현재가": 72000}
391+
392+
# Mock manual holding
393+
manual_holding = MagicMock()
394+
manual_holding.ticker = "005930"
395+
manual_holding.display_name = "삼성전자"
396+
manual_holding.quantity = Decimal("5")
397+
manual_holding.avg_price = Decimal("70000")
398+
399+
class MockManualService:
400+
def __init__(self, db):
401+
pass
402+
403+
async def get_holdings_by_user(self, user_id, market_type):
404+
return [manual_holding]
405+
406+
sell_calls = []
407+
408+
async def fake_buy(kis, symbol, current_price, avg_price):
409+
return {"success": False, "message": "조건 미충족", "orders_placed": 0}
410+
411+
async def fake_sell(kis, symbol, current_price, avg_price, qty):
412+
sell_calls.append({"symbol": symbol, "qty": qty})
413+
return {"success": False, "message": "조건 미충족", "orders_placed": 0}
414+
415+
# Mock DB session
416+
mock_db_session = MagicMock()
417+
mock_db_session.__aenter__ = AsyncMock(return_value=MagicMock())
418+
mock_db_session.__aexit__ = AsyncMock(return_value=None)
419+
420+
with patch('app.core.db.AsyncSessionLocal', return_value=mock_db_session), \
421+
patch('app.services.manual_holdings_service.ManualHoldingsService', MockManualService):
422+
423+
monkeypatch.setattr(kis_tasks, "KISClient", DummyKIS)
424+
monkeypatch.setattr(kis_tasks, "KISAnalyzer", DummyAnalyzer)
425+
monkeypatch.setattr(kis_tasks, "process_kis_domestic_buy_orders_with_analysis", fake_buy)
426+
monkeypatch.setattr(kis_tasks, "process_kis_domestic_sell_orders_with_analysis", fake_sell)
427+
monkeypatch.setattr(
428+
kis_tasks.run_per_domestic_stock_automation,
429+
"update_state",
430+
lambda *_, **__: None,
431+
raising=False,
432+
)
433+
434+
result = kis_tasks.run_per_domestic_stock_automation.apply().result
435+
436+
# 태스크가 성공적으로 완료되어야 함
437+
assert result["status"] == "completed"
438+
439+
# 매도 함수가 호출되어야 함
440+
assert len(sell_calls) == 1, "매도 함수가 호출되어야 함"
441+
442+
# qty가 정확히 5여야 함 (ord_psbl_qty가 올바르게 설정됨)
443+
assert sell_calls[0]["qty"] == 5, f"수량은 5여야 하는데 {sell_calls[0]['qty']}임"

0 commit comments

Comments
 (0)