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>
This commit is contained in:
178
core/order.py
Normal file
178
core/order.py
Normal file
@@ -0,0 +1,178 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user