@@ -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