diff --git a/prac.py b/prac.py index 20e615f..a51b805 100644 --- a/prac.py +++ b/prac.py @@ -46,36 +46,40 @@ def main(): # pick? Also, you will need to send more orders over time. # --- 설정 --- BOND_FAIR_VALUE = 1000 # BOND fair value (고정) - BOND_ORDER_SIZE = 50 # BOND 주문당 수량 + BOND_ORDER_SIZE = 60 # BOND 주문당 수량 XLF_CONVERSION_FEE = 100 # XLF 변환 비용 - XLF_MIN_PROFIT = 10 # XLF 차익거래 최소 수익 (불확실한 거래 방지) + XLF_MIN_PROFIT = 5 # XLF 차익거래 최소 수익 + XLF_COOLDOWN = 2.0 # XLF 실패 후 재시도 대기 시간 (초) VALE_CONVERSION_FEE = 10 # VALE 변환 비용 - VALE_MIN_PROFIT = 3 # VALE 차익거래 최소 수익 + VALE_MIN_PROFIT = 1 # VALE 차익거래 최소 수익 VALE_ARB_SIZE = 10 # VALE 차익거래 단위 REFRESH_INTERVAL = 5.0 # 주문 갱신 주기 (초) # XLF state machine # IDLE → BUYING_BASKET → CONVERTING → SELLING_XLF → IDLE # IDLE → BUYING_XLF → CONVERTING → SELLING_BASKET → IDLE - xlf_state = "IDLE" - xlf_pending = {} - xlf_direction = None - xlf_arb_size = 0 + xlf_state = "IDLE" + xlf_pending = {} + xlf_direction = None + xlf_arb_size = 0 + xlf_convert_oid = None + xlf_last_fail = 0.0 # 마지막 XLF 실패 시간 - # VALE state machine + # VALE state machine (XLF와 독립적으로 동시 실행 가능) # IDLE → BUYING_VALBZ → CONVERTING → SELLING_VALE → IDLE # IDLE → BUYING_VALE → CONVERTING → SELLING_VALBZ → IDLE - vale_state = "IDLE" - vale_pending = {} - vale_direction = None - vale_arb_size = 0 + vale_state = "IDLE" + vale_pending = {} + vale_direction = None + vale_arb_size = 0 + vale_convert_oid = None state = StateManager() om = OrderManager(exchange) market_open = False active_orders = {} # BOND 전용 {order_id: {"dir": ..., "price": ..., "size": ...}} - last_refresh = time.time() + last_refresh = time.time() # hello 메시지에서 기존 포지션 로드 (재접속 시 필수) for sym_info in hello_message["symbols"]: @@ -89,6 +93,16 @@ def main(): def next_id(): return om.next_order() + def cancel_bond_sell_orders(): + """BOND 매도 주문만 취소 (basket→XLF 시 매수 주문은 유지)""" + for oid in list(active_orders.keys()): + order_info = active_orders[oid] + if order_info["dir"] == Dir.SELL: + size = order_info.get("size", 0) + om.future_positions["BOND"] += size + om.cancel(oid) + active_orders.pop(oid, None) + def cancel_all_bond_orders(): """활성 BOND 주문 전부 취소 + future_positions 롤백""" for oid in list(active_orders.keys()): @@ -151,12 +165,16 @@ def main(): print(f" BOND 주문 → 매수:{buy_price} x{buy_size}, 매도:{sell_price} x{sell_size}, 포지션:{position}") def try_xlf_arb(): - """XLF 차익거래 시도 - IDLE 상태일 때만""" - nonlocal xlf_state, xlf_direction, xlf_pending, xlf_arb_size + """XLF 차익거래 시도 - IDLE + 쿨다운 통과 시에만""" + nonlocal xlf_state, xlf_direction, xlf_pending, xlf_arb_size, xlf_convert_oid if not market_open or xlf_state != "IDLE": return + # 실패 후 쿨다운 체크 + if time.time() - xlf_last_fail < XLF_COOLDOWN: + return + bond_ask = state.ask_prices["BOND"] gs_ask = state.ask_prices["GS"] ms_ask = state.ask_prices["MS"] @@ -175,12 +193,20 @@ def main(): basket_ask = bond_ask*3 + gs_ask*2 + ms_ask*3 + wfc_ask*2 basket_bid = bond_bid*3 + gs_bid*2 + ms_bid*3 + wfc_bid*2 + # 바스켓 심볼 포지션 한도 체크 + def basket_has_room(): + return ( + om.positions["BOND"] + 3 <= 100 and + om.positions["GS"] + 2 <= 100 and + om.positions["MS"] + 3 <= 100 and + om.positions["WFC"] + 2 <= 100 + ) + # 케이스 1: 바스켓 매수 → XLF 변환 → XLF 매도 profit1 = xlf_bid * 10 - basket_ask - XLF_CONVERSION_FEE - if profit1 > XLF_MIN_PROFIT and om.check_pos_limit("XLF"): + if profit1 > XLF_MIN_PROFIT and om.check_pos_limit("XLF") and basket_has_room(): print(f" XLF 차익(바스켓→XLF) 시작, 예상수익:{profit1}") - # BOND 주문 먼저 취소 (XLF용 BOND 매수가 기존 매도에 상쇄되지 않도록) - cancel_all_bond_orders() + cancel_bond_sell_orders() xlf_state = "BUYING_BASKET" xlf_direction = "BASKET_TO_XLF" xlf_arb_size = 10 @@ -206,7 +232,7 @@ def main(): def handle_xlf_fill(order_id, symbol, dir_, qty): """XLF state machine 체결 처리 (부분 체결 추적)""" - nonlocal xlf_state, xlf_pending + nonlocal xlf_state, xlf_pending, xlf_convert_oid if order_id not in xlf_pending: return @@ -218,16 +244,18 @@ def main(): if xlf_state == "BUYING_BASKET" and not xlf_pending: print(" 바스켓 매수 완료 → XLF 변환 시작") xlf_state = "CONVERTING" - exchange.send_convert_message(next_id(), "XLF", Dir.BUY, xlf_arb_size) + xlf_convert_oid = next_id() + exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.BUY, xlf_arb_size) elif xlf_state == "BUYING_XLF" and not xlf_pending: print(" XLF 매수 완료 → 바스켓 변환 시작") xlf_state = "CONVERTING" - exchange.send_convert_message(next_id(), "XLF", Dir.SELL, xlf_arb_size) + xlf_convert_oid = next_id() + exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.SELL, xlf_arb_size) def try_vale_arb(): - """VALE/VALBZ 차익거래 시도 - IDLE 상태일 때만""" - nonlocal vale_state, vale_direction, vale_pending, vale_arb_size + """VALE/VALBZ 차익거래 시도 - XLF와 독립적으로 동시 실행""" + nonlocal vale_state, vale_direction, vale_pending, vale_arb_size, vale_convert_oid if not market_open or vale_state != "IDLE": return @@ -272,7 +300,7 @@ def main(): def handle_vale_fill(order_id, symbol, dir_, qty): """VALE state machine 체결 처리 (부분 체결 추적)""" - nonlocal vale_state, vale_pending + nonlocal vale_state, vale_pending, vale_convert_oid if order_id not in vale_pending: return @@ -284,12 +312,14 @@ def main(): if vale_state == "BUYING_VALBZ" and not vale_pending: print(" VALBZ 매수 완료 → VALE 변환 시작") vale_state = "CONVERTING" - exchange.send_convert_message(next_id(), "VALE", Dir.BUY, vale_arb_size) + vale_convert_oid = next_id() + exchange.send_convert_message(vale_convert_oid, "VALE", Dir.BUY, vale_arb_size) elif vale_state == "BUYING_VALE" and not vale_pending: print(" VALE 매수 완료 → VALBZ 변환 시작") vale_state = "CONVERTING" - exchange.send_convert_message(next_id(), "VALE", Dir.SELL, vale_arb_size) + vale_convert_oid = next_id() + exchange.send_convert_message(vale_convert_oid, "VALE", Dir.SELL, vale_arb_size) # Set up some variables to track the bid and ask price of a symbol. Right # now this doesn't track much information, but it's enough to get a sense @@ -336,19 +366,22 @@ def main(): active_orders.pop(oid, None) if oid in xlf_pending: print(" XLF 주문 reject → IDLE 복귀") - xlf_state = "IDLE" + xlf_state = "IDLE" xlf_pending.clear() xlf_direction = None - place_bond_orders() + xlf_last_fail = time.time() # 쿨다운 시작 + place_bond_orders() # try_xlf_arb() 호출 제거 if oid in vale_pending: print(" VALE 주문 reject → IDLE 복귀") - vale_state = "IDLE" + vale_state = "IDLE" vale_pending.clear() vale_direction = None elif message["type"] == "ack": - # XLF 변환 ack 처리 - if xlf_state == "CONVERTING": + ack_oid = message.get("order_id") + + # XLF 변환 ack 처리 (order_id로 구분) + if xlf_state == "CONVERTING" and ack_oid == xlf_convert_oid: print(" XLF 변환 완료 → 매도 시작") if xlf_direction == "BASKET_TO_XLF": # 변환: BOND -3, GS -2, MS -3, WFC -2, XLF +10 @@ -388,8 +421,8 @@ def main(): ) xlf_pending[oid] = qty - # VALE 변환 ack 처리 - elif vale_state == "CONVERTING": + # VALE 변환 ack 처리 (order_id로 구분) + elif vale_state == "CONVERTING" and ack_oid == vale_convert_oid: print(" VALE 변환 완료 → 매도 시작") if vale_direction == "VALBZ_TO_VALE": # 변환: VALBZ -size, VALE +size @@ -423,6 +456,7 @@ def main(): dir_ = message["dir"] oid = message["order_id"] + # 포지션 업데이트 if dir_ == Dir.BUY: om.update_position(sym, oid, qty) else: @@ -430,22 +464,27 @@ def main(): print(f" 포지션 → {om.positions}") + # BOND 체결 시 무조건 재주문 if sym == "BOND" and oid in active_orders: active_orders.pop(oid, None) place_bond_orders() + # XLF state machine 체결 처리 handle_xlf_fill(oid, sym, dir_, qty) if xlf_state in ("SELLING_XLF", "SELLING_BASKET") and not xlf_pending: print(" XLF 차익거래 완료 → IDLE 복귀") xlf_state = "IDLE" xlf_direction = None place_bond_orders() + # try_xlf_arb() 제거 → book 메시지에서 자동 시도 + # VALE state machine 체결 처리 handle_vale_fill(oid, sym, dir_, qty) if vale_state in ("SELLING_VALE", "SELLING_VALBZ") and not vale_pending: print(" VALE 차익거래 완료 → IDLE 복귀") vale_state = "IDLE" vale_direction = None + # try_vale_arb() 제거 → book 메시지에서 자동 시도 elif message["type"] == "book": sym = message["symbol"] @@ -621,4 +660,4 @@ if __name__ == "__main__": "Please put your team name in the variable [team_name]." ) - main() \ No newline at end of file + main()