fix: 해외잔고 ord_psbl_qty 우선 적용 및 ghost position SELL 반복 방지 (#235)
Some checks failed
CI / test (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
- _extract_held_codes_from_balance / _extract_held_qty_from_balance: 해외 잔고 수량 필드를 ovrs_cblc_qty(총 보유수량) → ord_psbl_qty(주문가능수량) 우선으로 변경. KIS 공식 문서(VTTS3012R) 확인 결과 ord_psbl_qty가 실제 매도 가능 수량이며, ovrs_cblc_qty는 만료/결제 미완료 포지션을 포함함. MLECW 등 만료된 Warrant는 ovrs_cblc_qty=289456이지만 ord_psbl_qty=0이라 startup sync 대상에서 제외되고 SELL 수량도 0이 됨. - trading_cycle: 해외 SELL이 '잔고내역이 없습니다'로 실패할 때 DB 포지션을 ghost-close SELL 로그로 닫아 무한 재시도 방지. exchange code 불일치 등 예외 상황에서 DB가 계속 open 상태로 남는 문제 해소. - docstring: _extract_held_qty_from_balance 해외 필드 설명 업데이트 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
52
src/main.py
52
src/main.py
@@ -257,7 +257,15 @@ def _extract_held_codes_from_balance(
|
||||
if is_domestic:
|
||||
qty = int(holding.get("ord_psbl_qty") or holding.get("hldg_qty") or 0)
|
||||
else:
|
||||
qty = int(holding.get("ovrs_cblc_qty") or holding.get("hldg_qty") or 0)
|
||||
# ord_psbl_qty (주문가능수량) is the actual sellable quantity.
|
||||
# ovrs_cblc_qty (해외잔고수량) includes unsettled/expired holdings
|
||||
# that cannot actually be sold (e.g. expired warrants).
|
||||
qty = int(
|
||||
holding.get("ord_psbl_qty")
|
||||
or holding.get("ovrs_cblc_qty")
|
||||
or holding.get("hldg_qty")
|
||||
or 0
|
||||
)
|
||||
if qty > 0:
|
||||
codes.append(code)
|
||||
return codes
|
||||
@@ -280,10 +288,12 @@ def _extract_held_qty_from_balance(
|
||||
ord_psbl_qty — 주문가능수량 (preferred: excludes unsettled)
|
||||
hldg_qty — 보유수량 (fallback)
|
||||
|
||||
Overseas fields (output1):
|
||||
Overseas fields (VTTS3012R / TTTS3012R output1):
|
||||
ovrs_pdno — 종목코드
|
||||
ovrs_cblc_qty — 해외잔고수량 (preferred)
|
||||
hldg_qty — 보유수량 (fallback)
|
||||
ord_psbl_qty — 주문가능수량 (preferred: actual sellable qty)
|
||||
ovrs_cblc_qty — 해외잔고수량 (fallback: total holding, may include
|
||||
unsettled or expired positions with ord_psbl_qty=0)
|
||||
hldg_qty — 보유수량 (last-resort fallback)
|
||||
"""
|
||||
output1 = balance_data.get("output1", [])
|
||||
if isinstance(output1, dict):
|
||||
@@ -301,7 +311,12 @@ def _extract_held_qty_from_balance(
|
||||
if is_domestic:
|
||||
qty = int(holding.get("ord_psbl_qty") or holding.get("hldg_qty") or 0)
|
||||
else:
|
||||
qty = int(holding.get("ovrs_cblc_qty") or holding.get("hldg_qty") or 0)
|
||||
qty = int(
|
||||
holding.get("ord_psbl_qty")
|
||||
or holding.get("ovrs_cblc_qty")
|
||||
or holding.get("hldg_qty")
|
||||
or 0
|
||||
)
|
||||
return qty
|
||||
return 0
|
||||
|
||||
@@ -908,6 +923,33 @@ async def trading_cycle(
|
||||
stock_code,
|
||||
_BUY_COOLDOWN_SECONDS,
|
||||
)
|
||||
# Close ghost position when broker has no matching balance.
|
||||
# This prevents infinite SELL retry cycles for positions that
|
||||
# exist in the DB (from startup sync) but are no longer
|
||||
# sellable at the broker (expired warrants, delisted stocks, etc.)
|
||||
if decision.action == "SELL" and "잔고내역이 없습니다" in msg1:
|
||||
logger.warning(
|
||||
"Ghost position detected for %s (%s): broker reports no balance."
|
||||
" Closing DB position to prevent infinite retry.",
|
||||
stock_code,
|
||||
market.exchange_code,
|
||||
)
|
||||
log_trade(
|
||||
conn=db_conn,
|
||||
stock_code=stock_code,
|
||||
action="SELL",
|
||||
confidence=0,
|
||||
rationale=(
|
||||
"[ghost-close] Broker reported no balance;"
|
||||
" position closed without fill"
|
||||
),
|
||||
quantity=0,
|
||||
price=0.0,
|
||||
pnl=0.0,
|
||||
market=market.code,
|
||||
exchange_code=market.exchange_code,
|
||||
mode=settings.MODE if settings else "paper",
|
||||
)
|
||||
logger.info("Order result: %s", result.get("msg1", "OK"))
|
||||
|
||||
# 5.5. Notify trade execution (only on success)
|
||||
|
||||
Reference in New Issue
Block a user