Skip to content

Commit dae49f1

Browse files
committed
Handle manual holdings in overseas automation and update tests
- Replace `rt_cd` checks with `odno` for order validation in KIS trading service. - Extend overseas automation to include manual holdings, transform manual balances to KIS format, and trigger Toss recommendations instead of sell steps. - Update tests to ensure correct handling of duplicates, proper inclusion of manual holdings, and validation for Toss notification scenarios.
1 parent 439c947 commit dae49f1

File tree

4 files changed

+536
-83
lines changed

4 files changed

+536
-83
lines changed

app/services/kis_trading_service.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ async def process_kis_domestic_buy_orders_with_analysis(
122122
price=int(price)
123123
)
124124

125-
if res and res.get('rt_cd') == '0':
125+
if res and res.get('odno'):
126126
success_count += 1
127127
ordered_prices.append(price)
128128
ordered_quantities.append(quantity)
@@ -227,7 +227,7 @@ async def process_kis_overseas_buy_orders_with_analysis(
227227
quantity=quantity,
228228
price=price
229229
)
230-
if res and res.get('rt_cd') == '0':
230+
if res and res.get('odno'):
231231
success_count += 1
232232
ordered_prices.append(price)
233233
ordered_quantities.append(quantity)
@@ -288,7 +288,7 @@ async def process_kis_domestic_sell_orders_with_analysis(
288288
quantity=balance_qty,
289289
price=int(current_price)
290290
)
291-
if res and res.get('rt_cd') == '0':
291+
if res and res.get('odno'):
292292
return {
293293
'success': True,
294294
'message': "목표가 도달로 전량 매도",
@@ -314,7 +314,7 @@ async def process_kis_domestic_sell_orders_with_analysis(
314314
quantity=balance_qty,
315315
price=int(target_price)
316316
)
317-
if res and res.get('rt_cd') == '0':
317+
if res and res.get('odno'):
318318
return {
319319
'success': True,
320320
'message': "전량 매도 주문 (분할 불가)",
@@ -346,7 +346,7 @@ async def process_kis_domestic_sell_orders_with_analysis(
346346
quantity=qty,
347347
price=int(price)
348348
)
349-
if res and res.get('rt_cd') == '0':
349+
if res and res.get('odno'):
350350
success_count += 1
351351
remaining_qty -= qty
352352
ordered_prices.append(price)
@@ -431,7 +431,7 @@ async def process_kis_overseas_sell_orders_with_analysis(
431431
quantity=balance_qty,
432432
price=current_price
433433
)
434-
if res and res.get('rt_cd') == '0':
434+
if res and res.get('odno'):
435435
return {
436436
'success': True,
437437
'message': "목표가 도달로 전량 매도",
@@ -457,7 +457,7 @@ async def process_kis_overseas_sell_orders_with_analysis(
457457
quantity=balance_qty,
458458
price=target_price
459459
)
460-
if res and res.get('rt_cd') == '0':
460+
if res and res.get('odno'):
461461
return {
462462
'success': True,
463463
'message': "전량 매도 주문",
@@ -490,7 +490,7 @@ async def process_kis_overseas_sell_orders_with_analysis(
490490
quantity=qty,
491491
price=price
492492
)
493-
if res and res.get('rt_cd') == '0':
493+
if res and res.get('odno'):
494494
success_count += 1
495495
remaining_qty -= qty
496496
ordered_prices.append(price)

app/tasks/kis.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1257,14 +1257,46 @@ async def _cancel_overseas_pending_orders(
12571257
def run_per_overseas_stock_automation(self) -> dict:
12581258
"""해외 주식 종목별 자동 실행 (미체결취소 -> 분석 -> 매수 -> 매도)"""
12591259
async def _run() -> dict:
1260+
from app.core.db import AsyncSessionLocal
1261+
from app.models.manual_holdings import MarketType
1262+
from app.services.manual_holdings_service import ManualHoldingsService
1263+
12601264
kis = KISClient()
12611265
from app.analysis.service_analyzers import YahooAnalyzer
12621266
analyzer = YahooAnalyzer()
12631267

12641268
try:
12651269
self.update_state(state='PROGRESS', meta={'status': STATUS_FETCHING_HOLDINGS, 'current': 0, 'total': 0})
1266-
1270+
1271+
# 1. KIS 보유 종목 조회
12671272
my_stocks = await kis.fetch_my_overseas_stocks()
1273+
1274+
# 2. 수동 잔고(토스 등) 해외 주식 조회
1275+
async with AsyncSessionLocal() as db:
1276+
manual_service = ManualHoldingsService(db)
1277+
user_id = 1 # USER_ID는 현재 1로 고정
1278+
manual_holdings = await manual_service.get_holdings_by_user(user_id=user_id, market_type=MarketType.US)
1279+
1280+
# 3. 수동 잔고 종목을 KIS 형식으로 변환하여 병합
1281+
for holding in manual_holdings:
1282+
ticker = holding.ticker
1283+
# KIS에 이미 있는 종목은 건너뛰기
1284+
if any(s.get('ovrs_pdno') == ticker for s in my_stocks):
1285+
continue
1286+
1287+
# 수동 잔고 종목을 my_stocks에 추가 (KIS 형식으로 변환)
1288+
qty_str = str(holding.quantity)
1289+
my_stocks.append({
1290+
'ovrs_pdno': ticker,
1291+
'ovrs_item_name': holding.display_name or ticker,
1292+
'ovrs_cblc_qty': qty_str,
1293+
'ord_psbl_qty': qty_str, # 수동 잔고는 미체결 없음
1294+
'pchs_avg_pric': str(holding.avg_price),
1295+
'now_pric2': '0', # 현재가는 나중에 API로 조회
1296+
'ovrs_excg_cd': 'NASD', # 기본값 (실제 거래소는 API 조회로 확인)
1297+
'_is_manual': True # 수동 잔고 표시
1298+
})
1299+
12681300
if not my_stocks:
12691301
return {'status': 'completed', 'message': NO_OVERSEAS_STOCKS_MESSAGE, 'results': []}
12701302

@@ -1284,6 +1316,18 @@ async def _run() -> dict:
12841316
# 매도 시 미체결 주문을 제외한 주문 가능 수량(ord_psbl_qty)을 사용
12851317
qty = int(float(stock.get('ord_psbl_qty', stock.get('ovrs_cblc_qty', 0))))
12861318
exchange_code = stock.get('ovrs_excg_cd', 'NASD')
1319+
is_manual = stock.get('_is_manual', False)
1320+
1321+
# 수동 잔고 종목인 경우 현재가를 API로 조회
1322+
if is_manual:
1323+
try:
1324+
price_df = await kis.inquire_overseas_price(symbol)
1325+
if not price_df.empty:
1326+
current_price = float(price_df.iloc[0]['close'])
1327+
logger.info(f"[수동잔고] {name}({symbol}) 현재가 조회: ${current_price:.2f}")
1328+
except Exception as e:
1329+
logger.warning(f"[수동잔고] {name}({symbol}) 현재가 조회 실패, 평단가 사용: {e}")
1330+
current_price = avg_price
12871331

12881332
stock_steps = []
12891333

@@ -1346,6 +1390,24 @@ async def _run() -> dict:
13461390
except Exception as notify_error:
13471391
logger.warning("텔레그램 알림 전송 실패: %s", notify_error)
13481392

1393+
# 수동 잔고(토스 등)는 KIS에서 매도할 수 없으므로 텔레그램 추천 알림만 발송
1394+
if is_manual:
1395+
logger.info(f"[수동잔고] {name}({symbol}) - KIS 매도 불가, 토스 추천 알림 발송")
1396+
try:
1397+
await _send_toss_recommendation_async(
1398+
code=symbol,
1399+
name=name or symbol,
1400+
current_price=current_price,
1401+
toss_quantity=qty,
1402+
toss_avg_price=avg_price,
1403+
)
1404+
stock_steps.append({'step': '매도', 'result': {'success': True, 'message': '수동잔고 - 토스 추천 알림 발송', 'orders_placed': 0}})
1405+
except Exception as e:
1406+
logger.warning(f"[수동잔고] {name}({symbol}) 토스 추천 알림 발송 실패: {e}")
1407+
stock_steps.append({'step': '매도', 'result': {'success': True, 'message': '수동잔고 - 매도 스킵', 'orders_placed': 0}})
1408+
results.append({'name': name, 'symbol': symbol, 'steps': stock_steps})
1409+
continue
1410+
13491411
# 4. 기존 미체결 매도 주문 취소
13501412
self.update_state(state='PROGRESS', meta={'status': f'{symbol} 미체결 매도 주문 취소 중...', 'current': index, 'total': total_count, 'percentage': int((index / total_count) * 100)})
13511413
try:

0 commit comments

Comments
 (0)