Files
upbit-trader/core/order.py
joungmin 6e0c4508fa refactor: MVC 구조 분리 + 미사용 파일 archive 정리
- tick_trader.py를 Controller로 축소, 로직을 3개 모듈로 분리:
  - core/signal.py: 시그널 감지, 지표 계산 (calc_vr, calc_atr, detect_signal)
  - core/order.py: Upbit 주문 실행 (매수/매도/취소/조회)
  - core/position_manager.py: 포지션 관리, DB sync, 복구, 청산 조건
- type hints, Google docstring, 구체적 예외 타입 적용
- 50줄 초과 함수 분리 (process_signal, restore_positions)
- 미사용 파일 58개 archive/ 폴더로 이동
- README.md 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:46:47 +09:00

179 lines
5.1 KiB
Python

"""Upbit 주문 실행 모듈.
주문 제출, 취소, 체결 조회, 시장가 매도 등
Upbit REST API와 직접 통신하는 로직을 담당한다.
"""
from __future__ import annotations
import logging
import math
import time
from typing import Optional, Tuple
import pyupbit
log = logging.getLogger(__name__)
def round_price(price: float) -> float:
"""Upbit 호가 단위로 내림 처리.
Args:
price: 원본 가격.
Returns:
호가 단위에 맞춰 내림된 가격.
"""
if price >= 2_000_000: unit = 1000
elif price >= 1_000_000: unit = 500
elif price >= 100_000: unit = 100
elif price >= 10_000: unit = 10
elif price >= 1_000: unit = 5
elif price >= 100: unit = 1
elif price >= 10: unit = 0.1
else: unit = 0.01
return math.floor(price / unit) * unit
def submit_limit_buy(
client: pyupbit.Upbit,
ticker: str,
price: float,
qty: float,
sim_mode: bool = False,
) -> Optional[str]:
"""지정가 매수 주문 제출.
Returns:
주문 UUID. 실패 시 None.
"""
price = round_price(price)
if sim_mode:
return f"sim-buy-{ticker}"
try:
order = client.buy_limit_order(ticker, price, qty)
if not order or 'error' in str(order):
log.error(f"지정가 매수 실패 {ticker}: {order}")
return None
return order.get('uuid')
except (ConnectionError, TimeoutError, ValueError) as e:
log.error(f"지정가 매수 오류 {ticker}: {e}")
return None
def submit_limit_sell(
client: pyupbit.Upbit,
ticker: str,
qty: float,
price: float,
sim_mode: bool = False,
) -> Optional[str]:
"""지정가 매도 주문 제출.
Returns:
주문 UUID. 실패 시 None.
"""
price = round_price(price)
if sim_mode:
return f"sim-{ticker}"
try:
order = client.sell_limit_order(ticker, price, qty)
if not order or 'error' in str(order):
log.error(f"지정가 매도 실패 {ticker}: price={price} qty={qty} -> {order}")
return None
return order.get('uuid')
except (ConnectionError, TimeoutError, ValueError) as e:
log.error(f"지정가 매도 오류 {ticker}: {e}")
return None
def cancel_order(
client: pyupbit.Upbit,
uuid: Optional[str],
sim_mode: bool = False,
) -> None:
"""주문 취소. sim_mode이거나 uuid가 없으면 무시."""
if sim_mode or not uuid or uuid.startswith('sim-'):
return
try:
client.cancel_order(uuid)
except (ConnectionError, TimeoutError, ValueError) as e:
log.warning(f"주문 취소 실패 {uuid}: {e}")
def check_order_state(
client: pyupbit.Upbit,
uuid: str,
) -> Tuple[Optional[str], Optional[float]]:
"""주문 상태 조회.
Returns:
(state, avg_price) 튜플. state: 'done'|'wait'|'cancel'|None.
"""
try:
detail = client.get_order(uuid)
if not detail:
return None, None
state = detail.get('state')
avg_price = float(detail.get('avg_price') or 0) or None
return state, avg_price
except (ConnectionError, TimeoutError, ValueError) as e:
log.warning(f"주문 조회 실패 {uuid}: {e}")
return None, None
def _avg_price_from_order(
client: pyupbit.Upbit,
uuid: str,
) -> Optional[float]:
"""체결 내역에서 가중평균 체결가를 계산."""
try:
detail = client.get_order(uuid)
if not detail:
return None
trades = detail.get('trades', [])
if trades:
total_funds = sum(float(t['funds']) for t in trades)
total_vol = sum(float(t['volume']) for t in trades)
return total_funds / total_vol if total_vol > 0 else None
avg = detail.get('avg_price')
return float(avg) if avg else None
except (ConnectionError, TimeoutError, ValueError) as e:
log.warning(f"체결가 조회 실패 {uuid}: {e}")
return None
def sell_market(
client: pyupbit.Upbit,
ticker: str,
qty: float,
sim_mode: bool = False,
) -> Optional[float]:
"""시장가 매도. 체결가를 반환.
Args:
client: Upbit 클라이언트.
ticker: 종목 코드.
qty: 매도 수량.
sim_mode: 시뮬레이션 모드.
Returns:
체결 평균가. 실패 시 None.
"""
if sim_mode:
price = pyupbit.get_current_price(ticker)
log.info(f"[SIM 시장가매도] {ticker} {qty:.6f}개 @ {price:,.0f}")
return price
try:
order = client.sell_market_order(ticker, qty)
if not order or 'error' in str(order):
log.error(f"시장가 매도 실패: {order}")
return None
uuid = order.get('uuid')
time.sleep(1.5)
avg_price = _avg_price_from_order(client, uuid) if uuid else None
return avg_price or pyupbit.get_current_price(ticker)
except (ConnectionError, TimeoutError, ValueError) as e:
log.error(f"시장가 매도 오류 {ticker}: {e}")
return None