@@ -1468,3 +1468,115 @@ async def fake_sell(kis, symbol, current_price, avg_price, qty):
14681468 # 재조회 후 ord_psbl_qty(9)가 전달되어야 함
14691469 assert sell_qty_received [0 ] == 9 , \
14701470 f"재조회 후 ord_psbl_qty(9)를 사용해야 하는데 { sell_qty_received [0 ]} 가 전달됨"
1471+
1472+ def test_domestic_automation_refreshes_qty_after_sell_order_cancel (self , monkeypatch ):
1473+ """미체결 매도 주문 취소 후 잔고를 재조회하여 ord_psbl_qty를 갱신해야 함.
1474+
1475+ 실제 버그 시나리오 (APBK0986 에러):
1476+ - 보유 8주, 미체결 매도 3주 → ord_psbl_qty=5
1477+ - 매수 후 잔고 재조회 → ord_psbl_qty=5 (미체결 매도 3주 여전히 존재)
1478+ - 미체결 매도 3주 취소 → 실제로는 ord_psbl_qty=8이 되어야 함
1479+ - 기존 코드: 취소 후 재조회 없이 5주로 매도 시도
1480+ - 수정 후: 취소 후 재조회하여 8주로 매도 시도
1481+ """
1482+ from unittest .mock import AsyncMock , MagicMock , patch
1483+ from app .tasks import kis as kis_tasks
1484+
1485+ class DummyAnalyzer :
1486+ async def analyze_stock_json (self , name ):
1487+ return {"decision" : "hold" , "confidence" : 65 }, "gemini-2.5-pro"
1488+
1489+ async def close (self ):
1490+ return None
1491+
1492+ sell_qty_received = []
1493+
1494+ class DummyKIS :
1495+ def __init__ (self ):
1496+ self .fetch_count = 0
1497+ self .cancel_sell_called = False
1498+
1499+ async def fetch_my_stocks (self ):
1500+ self .fetch_count += 1
1501+ if self .fetch_count <= 2 :
1502+ # 첫 번째/두 번째 호출: 미체결 매도 3주가 있어서 ord_psbl_qty=5
1503+ return [
1504+ {
1505+ "pdno" : "005935" ,
1506+ "prdt_name" : "삼성전자우" ,
1507+ "pchs_avg_pric" : "76300" ,
1508+ "prpr" : "77500" ,
1509+ "hldg_qty" : "8" ,
1510+ "ord_psbl_qty" : "5" , # 미체결 매도 3주가 있는 상태
1511+ }
1512+ ]
1513+ else :
1514+ # 세 번째 호출 (미체결 매도 취소 후): ord_psbl_qty=8
1515+ return [
1516+ {
1517+ "pdno" : "005935" ,
1518+ "prdt_name" : "삼성전자우" ,
1519+ "pchs_avg_pric" : "76300" ,
1520+ "prpr" : "77500" ,
1521+ "hldg_qty" : "8" ,
1522+ "ord_psbl_qty" : "8" , # 미체결 매도 취소 후
1523+ }
1524+ ]
1525+
1526+ async def inquire_korea_orders (self , * args , ** kwargs ):
1527+ # 미체결 매도 주문 3주가 있음
1528+ return [
1529+ {
1530+ "pdno" : "005935" ,
1531+ "sll_buy_dvsn_cd" : "01" , # 매도
1532+ "odno" : "0000001" ,
1533+ "ord_qty" : "3" ,
1534+ "ord_unpr" : "78000" ,
1535+ }
1536+ ]
1537+
1538+ async def cancel_korea_order (self , * args , ** kwargs ):
1539+ self .cancel_sell_called = True
1540+ return {"odno" : "0000001" }
1541+
1542+ class MockManualService :
1543+ def __init__ (self , db ):
1544+ pass
1545+
1546+ async def get_holdings_by_user (self , user_id , market_type ):
1547+ return []
1548+
1549+ async def fake_buy (* _ , ** __ ):
1550+ return {"success" : False , "message" : "1% 매수 조건 미충족" , "orders_placed" : 0 }
1551+
1552+ async def fake_sell (kis , symbol , current_price , avg_price , qty ):
1553+ sell_qty_received .append (qty )
1554+ return {"success" : True , "message" : "매도 완료" , "orders_placed" : 1 }
1555+
1556+ mock_db_session = MagicMock ()
1557+ mock_db_session .__aenter__ = AsyncMock (return_value = MagicMock ())
1558+ mock_db_session .__aexit__ = AsyncMock (return_value = None )
1559+
1560+ with patch ('app.core.db.AsyncSessionLocal' , return_value = mock_db_session ), \
1561+ patch ('app.services.manual_holdings_service.ManualHoldingsService' , MockManualService ):
1562+
1563+ monkeypatch .setattr (kis_tasks , "KISClient" , DummyKIS )
1564+ monkeypatch .setattr (kis_tasks , "KISAnalyzer" , DummyAnalyzer )
1565+ monkeypatch .setattr (kis_tasks , "process_kis_domestic_buy_orders_with_analysis" , fake_buy )
1566+ monkeypatch .setattr (kis_tasks , "process_kis_domestic_sell_orders_with_analysis" , fake_sell )
1567+ monkeypatch .setattr (
1568+ kis_tasks .run_per_domestic_stock_automation ,
1569+ "update_state" ,
1570+ lambda * _ , ** __ : None ,
1571+ raising = False ,
1572+ )
1573+
1574+ result = kis_tasks .run_per_domestic_stock_automation .apply ().result
1575+
1576+ assert result ["status" ] == "completed"
1577+ assert len (sell_qty_received ) == 1
1578+
1579+ # 핵심 검증: 미체결 매도 취소 후 잔고를 재조회하여 ord_psbl_qty(8)가 전달되어야 함
1580+ # 재조회 없이 기존 ord_psbl_qty(5)가 전달되면 버그가 있는 것임
1581+ assert sell_qty_received [0 ] == 8 , \
1582+ f"미체결 매도 취소 후 재조회하여 ord_psbl_qty(8)를 사용해야 하는데 { sell_qty_received [0 ]} 가 전달됨"
0 commit comments