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