Files
cointrader/scripts/verify_prod_api.py
21in7 17742da6af feat: add algo order compatibility and orphan order cleanup
- exchange.py: cancel_all_orders() now cancels both standard and algo orders
- exchange.py: get_open_orders() merges standard + algo orders
- exchange.py: cancel_order() falls back to algo cancel on failure
- bot.py: store SL/TP prices for price-based close_reason re-determination
- bot.py: add _cancel_remaining_orders() for orphan SL/TP cleanup
- bot.py: re-classify MANUAL close_reason as SL/TP via price comparison
- bot.py: cancel orphan orders on startup when no position exists
- tests: fix env setup for testnet config and ML filter mocking
- docs: add backtest market context and algo order fix design specs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 20:46:59 +09:00

231 lines
8.2 KiB
Python

"""실전 API SL/TP 콜백 검증 스크립트.
검증 항목:
1. SL/TP 주문 응답에 orderId vs algoId 확인
2. SL 트리거 시 UDS 콜백의 o, ot 필드 값
3. futures_cancel_order(orderId=...)로 TP 취소 가능 여부
사용법:
1. 바이낸스 앱/웹에서 XRPUSDT 소액 LONG 포지션 수동 진입
2. python scripts/verify_prod_api.py 실행
→ 자동으로 SL/TP 배치 + UDS 리스닝
3. SL이 트리거되면 콜백 로그 확인 + TP 자동 취소 시도
환경변수: BINANCE_API_KEY, BINANCE_API_SECRET (실전 키)
"""
import asyncio
import json
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from dotenv import load_dotenv
from binance import AsyncClient, BinanceSocketManager
from loguru import logger
from src.exchange import BinanceFuturesClient
from src.config import Config
# .env에서 실전 키 로드 (BINANCE_TESTNET이 설정되어 있으면 해제)
os.environ.pop("BINANCE_TESTNET", None)
load_dotenv()
SYMBOL = "XRPUSDT"
async def main():
api_key = os.getenv("BINANCE_API_KEY", "")
api_secret = os.getenv("BINANCE_API_SECRET", "")
if not api_key or not api_secret:
logger.error("BINANCE_API_KEY / BINANCE_API_SECRET 환경변수 필요")
return
# Exchange 클라이언트 (실전)
config = Config()
config.testnet = False
config.api_key = api_key
config.api_secret = api_secret
config.symbol = SYMBOL
exchange = BinanceFuturesClient(config, symbol=SYMBOL)
# ── Step 1: 현재 포지션 확인 ──
position = await exchange.get_position()
if position is None:
logger.error(
f"[{SYMBOL}] 포지션 없음. 먼저 바이낸스 앱/웹에서 소액 포지션을 수동으로 진입하세요."
)
return
pos_amt = float(position["positionAmt"])
entry_price = float(position["entryPrice"])
mark_price = float(position.get("markPrice", entry_price))
side = "LONG" if pos_amt > 0 else "SHORT"
quantity = abs(pos_amt)
logger.info(f"[{SYMBOL}] 포지션 확인: {side} qty={quantity}, entry={entry_price}, mark={mark_price}")
# ── Step 2: 기존 오픈 주문 확인/정리 ──
open_orders = await exchange.get_open_orders()
if open_orders:
logger.info(f"[{SYMBOL}] 기존 오픈 주문 {len(open_orders)}개 — 전체 취소")
await exchange.cancel_all_orders()
await asyncio.sleep(1)
# ── Step 3: SL/TP 주문 배치 (현재가 기준 가까운 값) ──
# SL: 현재가에서 0.15% 떨어진 곳 (빨리 트리거되도록)
# TP: 현재가에서 2% 떨어진 곳 (트리거 안 되도록)
sl_side = "SELL" if side == "LONG" else "BUY"
if side == "LONG":
stop_loss = exchange._round_price(mark_price * 0.9985) # -0.15%
take_profit = exchange._round_price(mark_price * 1.02) # +2%
else:
stop_loss = exchange._round_price(mark_price * 1.0015) # +0.15%
take_profit = exchange._round_price(mark_price * 0.98) # -2%
logger.info(f"[{SYMBOL}] SL/TP 배치 예정: SL={stop_loss}, TP={take_profit}, side={sl_side}")
# SL 배치
sl_result = await exchange.place_order(
side=sl_side,
quantity=quantity,
order_type="STOP_MARKET",
stop_price=stop_loss,
reduce_only=True,
)
logger.success(f"[검증1] SL 주문 응답 전체:\n{json.dumps(sl_result, indent=2)}")
sl_order_id = sl_result.get("orderId")
sl_algo_id = sl_result.get("algoId")
logger.info(f" → orderId={sl_order_id}, algoId={sl_algo_id}")
# TP 배치
tp_result = await exchange.place_order(
side=sl_side,
quantity=quantity,
order_type="TAKE_PROFIT_MARKET",
stop_price=take_profit,
reduce_only=True,
)
logger.success(f"[검증1] TP 주문 응답 전체:\n{json.dumps(tp_result, indent=2)}")
tp_order_id = tp_result.get("orderId")
tp_algo_id = tp_result.get("algoId")
logger.info(f" → orderId={tp_order_id}, algoId={tp_algo_id}")
# ── Step 4: UDS 리스닝 — SL 트리거 대기 ──
logger.info(f"[{SYMBOL}] UDS 리스닝 시작 — SL 트리거 대기 중 (mark={mark_price}, SL={stop_loss})")
logger.info(" SL이 트리거되면 자동으로 TP 취소를 시도합니다.")
logger.info(" Ctrl+C로 중단 가능 (중단 시 잔여 주문 정리)")
sl_triggered = asyncio.Event()
async def on_uds_message(msg: dict):
if msg.get("e") != "ORDER_TRADE_UPDATE":
return
order = msg.get("o", {})
if order.get("s") != SYMBOL:
return
# 모든 이벤트 원본 로깅
logger.info(
f"[검증2] UDS 원본: "
f"s={order.get('s')} "
f"o={order.get('o')} "
f"ot={order.get('ot')} "
f"x={order.get('x')} "
f"X={order.get('X')} "
f"R={order.get('R')} "
f"S={order.get('S')} "
f"i={order.get('i')} "
f"ap={order.get('ap')} "
f"rp={order.get('rp')} "
f"n={order.get('n')}"
)
# FILLED된 SL 감지
if order.get("x") == "TRADE" and order.get("X") == "FILLED":
ot = order.get("ot", "")
if ot == "STOP_MARKET":
logger.success(
f"[검증2] SL FILLED 확인! "
f"o={order.get('o')}, ot={ot}, "
f"orderId={order.get('i')}, "
f"exit_price={order.get('ap')}, rp={order.get('rp')}"
)
sl_triggered.set()
# UDS 연결
client = await AsyncClient.create(
api_key=api_key,
api_secret=api_secret,
)
try:
bm = BinanceSocketManager(client)
async with bm.futures_user_socket() as stream:
logger.info("UDS 연결 완료")
while True:
try:
msg = await asyncio.wait_for(stream.recv(), timeout=1.0)
await on_uds_message(msg)
except asyncio.TimeoutError:
pass
if sl_triggered.is_set():
break
# ── Step 5: TP 취소 검증 ──
cancel_id = tp_order_id or tp_algo_id
logger.info(f"[검증3] TP 취소 시도: futures_cancel_order(orderId={cancel_id})")
try:
cancel_result = await exchange.cancel_order(cancel_id)
logger.success(f"[검증3] TP 취소 성공:\n{json.dumps(cancel_result, indent=2)}")
except Exception as e:
logger.error(f"[검증3] TP 취소 실패: {e}")
# cancel_all_orders 폴백
logger.info("[검증3] cancel_all_orders 폴백 시도")
try:
fallback_result = await exchange.cancel_all_orders()
logger.success(f"[검증3] cancel_all_orders 결과: {fallback_result}")
except Exception as e2:
logger.error(f"[검증3] cancel_all_orders도 실패: {e2}")
# 최종 오픈 주문 확인
remaining = await exchange.get_open_orders()
if remaining:
logger.warning(f"[검증3] 잔여 오픈 주문 {len(remaining)}개:")
for o in remaining:
logger.warning(f" id={o.get('orderId')}, type={o.get('type')}, status={o.get('status')}")
else:
logger.success("[검증3] 잔여 오픈 주문 없음 — 고아주문 없음 확인!")
except KeyboardInterrupt:
logger.info("중단 — 잔여 주문 정리 중...")
try:
await exchange.cancel_all_orders()
logger.info("잔여 주문 전체 취소 완료")
except Exception as e:
logger.warning(f"잔여 주문 취소 실패: {e}")
finally:
await client.close_connection()
# ── 결과 요약 ──
logger.info("=" * 60)
logger.info("검증 결과 요약")
logger.info("=" * 60)
logger.info(f"[1] SL orderId={sl_order_id}, algoId={sl_algo_id}")
logger.info(f"[1] TP orderId={tp_order_id}, algoId={tp_algo_id}")
logger.info(f"[2] SL 트리거 감지: {'YES' if sl_triggered.is_set() else 'NO (타임아웃/중단)'}")
logger.info(f"[3] 위 로그에서 TP 취소 성공 여부 확인")
if __name__ == "__main__":
asyncio.run(main())