diff --git a/bot bond sell.py b/bot bond sell.py index d21cf14..0339e7a 100644 --- a/bot bond sell.py +++ b/bot bond sell.py @@ -25,86 +25,240 @@ def main(): hello_message = exchange.read_message() print("First message from exchange:", hello_message) - # --- 설정 --- + # ★ 재연결 시 기존 포지션 복원 + state_init = StateManager() + for sym_info in hello_message.get("symbols", []): + sym = sym_info["symbol"] + posn = sym_info["position"] + if posn != 0: + state_init.update_position(sym, posn) + print(f" [재연결] 기존 포지션 복원: {sym} = {posn}") + + # ==================== 설정 ==================== BOND_FAIR_VALUE = 1000 - BOND_SPREAD = 2 # ±1 → ±2로 확대해서 수익성 개선 (테스트 후 조정) + BOND_SPREAD = 2 # BOND 마켓메이킹 스프레드 ±2 BOND_ORDER_SIZE = 30 BOND_MAX_POSITION = 100 + + # GS/MS/WFC 마켓메이킹 설정 + STOCK_SPREAD = 3 # 개별주 스프레드 ±3 (변동성이 BOND보다 크므로) + STOCK_ORDER_SIZE = 10 # 개별주 주문 수량 (보수적으로 시작) + STOCK_MAX_POSITION = 100 + STOCK_REORDER_DELAY = 0.3 # 개별주 재주문 throttle (초) + XLF_CONVERSION_FEE = 100 XLF_MAX_POSITION = 100 VALE_CONVERSION_FEE = 10 VALE_MAX_POSITION = 10 - REFRESH_INTERVAL = 5.0 + + REFRESH_INTERVAL = 5.0 # 전체 주문 갱신 주기 + + # ==================== 상태 변수 ==================== # XLF state machine xlf_state = "IDLE" - xlf_pending = {} # order_id -> remaining qty + xlf_pending = {} # order_id -> remaining qty xlf_direction = None - xlf_convert_oid = None # 변환 주문 ID (ack 구분용) + xlf_convert_oid = None # VALE state machine vale_state = "IDLE" vale_pending = {} vale_direction = None - vale_convert_oid = None # 변환 주문 ID (ack 구분용) + vale_convert_oid = None - state = StateManager() - om = OrderManager() - market_open = False - active_orders = {} # BOND 전용: order_id -> {"dir": ..., "price": ...} + state = state_init + om = OrderManager() + market_open = False - last_refresh = time.time() - last_bond_fill_time = 0.0 # BOND 체결 시 재주문 throttle용 - BOND_REORDER_DELAY = 0.2 # 0.2초 이내 중복 재주문 방지 + # 마켓메이킹 주문 추적: order_id -> {"sym": ..., "dir": ..., "price": ...} + active_mm_orders = {} + + last_refresh = time.time() + last_mm_fill_time = {} # sym -> 마지막 체결 시각 (throttle용) + BOND_REORDER_DELAY = 0.2 + + # 개별주 implied fair value (book 업데이트마다 갱신) + implied_fairs = {"GS": None, "MS": None, "WFC": None} def next_id(): return om.next_order() - def cancel_all_bond_orders(): - for oid in list(active_orders.keys()): - exchange.send_cancel_message(oid) - active_orders.clear() + # ==================== fair value 추정 ==================== - def place_bond_orders(): - """포지션 한도 안에서 bid/ask 양방향 주문""" + def update_implied_fairs(): + """ + XLF 바스켓 구성으로 GS/MS/WFC implied fair value 추정. + XLF_mid * 10 = BOND*3 + GS*2 + MS*3 + WFC*2 + BOND = 1000(고정) 이므로 잔여분을 현재 mid 비율로 배분. + """ + xlf_bid = state.bid_prices.get("XLF") + xlf_ask = state.ask_prices.get("XLF") + gs_bid = state.bid_prices.get("GS") + gs_ask = state.ask_prices.get("GS") + ms_bid = state.bid_prices.get("MS") + ms_ask = state.ask_prices.get("MS") + wfc_bid = state.bid_prices.get("WFC") + wfc_ask = state.ask_prices.get("WFC") + + if None in [xlf_bid, xlf_ask, gs_bid, gs_ask, ms_bid, ms_ask, wfc_bid, wfc_ask]: + return + + xlf_mid = (xlf_bid + xlf_ask) / 2 + gs_mid = (gs_bid + gs_ask) / 2 + ms_mid = (ms_bid + ms_ask) / 2 + wfc_mid = (wfc_bid + wfc_ask) / 2 + + residual = xlf_mid * 10 - 3000 # BOND 기여분(3x1000) 제외 + total = gs_mid*2 + ms_mid*3 + wfc_mid*2 + + if total <= 0: + return + + implied_fairs["GS"] = round(residual * (gs_mid * 2 / total) / 2) + implied_fairs["MS"] = round(residual * (ms_mid * 3 / total) / 3) + implied_fairs["WFC"] = round(residual * (wfc_mid * 2 / total) / 2) + + # ==================== 잔여 포지션 청산 ==================== + + def cleanup_residual_positions(): + """재연결 시 잔여 포지션 청산""" if not market_open: return - cancel_all_bond_orders() + xlf_pos = state.get_position("XLF") + if xlf_pos != 0: + lots = abs(xlf_pos) // 10 + remainder = abs(xlf_pos) % 10 + direction = Dir.SELL if xlf_pos > 0 else Dir.BUY + print(f" [청산] XLF 포지션={xlf_pos}, 변환 lots={lots}, 잔여={remainder}") + if lots > 0: + exchange.send_convert_message(next_id(), "XLF", direction, lots * 10) + if remainder > 0: + oid = next_id() + if xlf_pos > 0: + price = max((state.bid_prices.get("XLF") or 1) - 5, 1) + exchange.send_add_message_ioc(oid, "XLF", Dir.SELL, price, remainder) + else: + price = (state.ask_prices.get("XLF") or 99999) + 5 + exchange.send_add_message_ioc(oid, "XLF", Dir.BUY, price, remainder) - buy_price = BOND_FAIR_VALUE - BOND_SPREAD - sell_price = BOND_FAIR_VALUE + BOND_SPREAD - position = state.get_position("BOND") + for sym in ["GS", "MS", "WFC", "VALE", "VALBZ"]: + pos = state.get_position(sym) + if pos == 0: + continue + print(f" [청산] {sym} 포지션={pos}") + oid = next_id() + if pos > 0: + price = max((state.bid_prices.get(sym) or 1) - 5, 1) + exchange.send_add_message_ioc(oid, sym, Dir.SELL, price, abs(pos)) + else: + price = (state.ask_prices.get(sym) or 99999) + 5 + exchange.send_add_message_ioc(oid, sym, Dir.BUY, price, abs(pos)) - base_size = BOND_ORDER_SIZE - adjustment = abs(position) // 5 + # ==================== 마켓메이킹 주문 ==================== + + def cancel_mm_orders_for(sym): + to_cancel = [oid for oid, info in active_mm_orders.items() if info["sym"] == sym] + for oid in to_cancel: + exchange.send_cancel_message(oid) + active_mm_orders.pop(oid, None) + + def place_bond_orders(): + if not market_open: + return + cancel_mm_orders_for("BOND") + + position = state.get_position("BOND") + base_size = BOND_ORDER_SIZE + adj = abs(position) // 5 if position < 0: - buy_size = min(base_size + adjustment, BOND_MAX_POSITION - position) - sell_size = max(base_size - adjustment, 1) + buy_size = min(base_size + adj, BOND_MAX_POSITION - position) + sell_size = max(base_size - adj, 1) elif position > 0: - buy_size = max(base_size - adjustment, 1) - sell_size = min(base_size + adjustment, BOND_MAX_POSITION + position) + buy_size = max(base_size - adj, 1) + sell_size = min(base_size + adj, BOND_MAX_POSITION + position) else: buy_size = base_size sell_size = base_size - # 포지션 한도 초과 방지 buy_size = min(buy_size, BOND_MAX_POSITION - max(position, 0)) sell_size = min(sell_size, BOND_MAX_POSITION + min(position, 0)) + buy_price = BOND_FAIR_VALUE - BOND_SPREAD + sell_price = BOND_FAIR_VALUE + BOND_SPREAD + if buy_size > 0: - bid = next_id() - exchange.send_add_message(bid, "BOND", Dir.BUY, buy_price, buy_size) - active_orders[bid] = {"dir": Dir.BUY, "price": buy_price} + oid = next_id() + exchange.send_add_message(oid, "BOND", Dir.BUY, buy_price, buy_size) + active_mm_orders[oid] = {"sym": "BOND", "dir": Dir.BUY, "price": buy_price} if sell_size > 0: - ask = next_id() - exchange.send_add_message(ask, "BOND", Dir.SELL, sell_price, sell_size) - active_orders[ask] = {"dir": Dir.SELL, "price": sell_price} + oid = next_id() + exchange.send_add_message(oid, "BOND", Dir.SELL, sell_price, sell_size) + active_mm_orders[oid] = {"sym": "BOND", "dir": Dir.SELL, "price": sell_price} print(f" BOND 주문 → 매수:{buy_price}x{buy_size}, 매도:{sell_price}x{sell_size}, 포지션:{position}") + def place_stock_orders(sym): + """ + GS/MS/WFC 마켓메이킹. + implied fair value 주변 ±STOCK_SPREAD에서 양방향 DAY 주문. + """ + if not market_open: + return + + fair = implied_fairs.get(sym) + if fair is None or fair <= 0: + return + + cancel_mm_orders_for(sym) + + position = state.get_position(sym) + base_size = STOCK_ORDER_SIZE + adj = abs(position) // 10 + + if position < 0: + buy_size = min(base_size + adj, STOCK_MAX_POSITION - position) + sell_size = max(base_size - adj, 1) + elif position > 0: + buy_size = max(base_size - adj, 1) + sell_size = min(base_size + adj, STOCK_MAX_POSITION + position) + else: + buy_size = base_size + sell_size = base_size + + buy_size = min(buy_size, STOCK_MAX_POSITION - max(position, 0)) + sell_size = min(sell_size, STOCK_MAX_POSITION + min(position, 0)) + + buy_price = int(fair) - STOCK_SPREAD + sell_price = int(fair) + STOCK_SPREAD + + if buy_price <= 0: + return + + if buy_size > 0: + oid = next_id() + exchange.send_add_message(oid, sym, Dir.BUY, buy_price, buy_size) + active_mm_orders[oid] = {"sym": sym, "dir": Dir.BUY, "price": buy_price} + + if sell_size > 0: + oid = next_id() + exchange.send_add_message(oid, sym, Dir.SELL, sell_price, sell_size) + active_mm_orders[oid] = {"sym": sym, "dir": Dir.SELL, "price": sell_price} + + print(f" {sym} 주문 → fair:{fair}, 매수:{buy_price}x{buy_size}, 매도:{sell_price}x{sell_size}, 포지션:{position}") + + def refresh_all_mm(): + """모든 마켓메이킹 주문 갱신""" + place_bond_orders() + update_implied_fairs() + for sym in ["GS", "MS", "WFC"]: + place_stock_orders(sym) + + # ==================== XLF 차익거래 ==================== + def try_xlf_arb(): nonlocal xlf_state, xlf_direction, xlf_pending, xlf_convert_oid @@ -128,16 +282,15 @@ 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 + pos_xlf = state.get_position("XLF") # 케이스 1: 바스켓 매수 → XLF 변환 → XLF 매도 profit1 = xlf_bid * 10 - basket_ask - XLF_CONVERSION_FEE - pos_xlf = state.get_position("XLF") if profit1 > 0 and pos_xlf < XLF_MAX_POSITION: - print(f" XLF 차익(바스켓→XLF) 시작, 예상수익:{profit1}") + print(f" XLF 차익(바스켓->XLF) 시작, 예상수익:{profit1}") xlf_state = "BUYING_BASKET" xlf_direction = "BASKET_TO_XLF" xlf_pending.clear() - # IOC 주문: 즉시 체결 안 되면 자동 취소 for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]: oid = next_id() exchange.send_add_message_ioc(oid, sym, Dir.BUY, state.ask_prices[sym], qty) @@ -147,7 +300,7 @@ def main(): # 케이스 2: XLF 매수 → 바스켓 변환 → 각 종목 매도 profit2 = basket_bid - xlf_ask * 10 - XLF_CONVERSION_FEE if profit2 > 0 and pos_xlf > -XLF_MAX_POSITION: - print(f" XLF 차익(XLF→바스켓) 시작, 예상수익:{profit2}") + print(f" XLF 차익(XLF->바스켓) 시작, 예상수익:{profit2}") xlf_state = "BUYING_XLF" xlf_direction = "XLF_TO_BASKET" xlf_pending.clear() @@ -160,39 +313,34 @@ def main(): if order_id not in xlf_pending: return - xlf_pending[order_id] = max(0, xlf_pending[order_id] - qty) if xlf_pending[order_id] == 0: del xlf_pending[order_id] if xlf_state == "BUYING_BASKET" and not xlf_pending: - print(" 바스켓 매수 완료 → XLF 변환 시작") - xlf_state = "CONVERTING" + print(" 바스켓 매수 완료 -> XLF 변환 시작") + xlf_state = "CONVERTING" xlf_convert_oid = next_id() exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.BUY, 10) elif xlf_state == "BUYING_XLF" and not xlf_pending: - print(" XLF 매수 완료 → 바스켓 변환 시작") - xlf_state = "CONVERTING" + print(" XLF 매수 완료 -> 바스켓 변환 시작") + xlf_state = "CONVERTING" xlf_convert_oid = next_id() exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.SELL, 10) def handle_xlf_out(order_id): - """out 메시지 처리 - IOC 주문이 체결 안 됐거나 BOND 주문 만료""" nonlocal xlf_state, xlf_pending, xlf_direction if order_id not in xlf_pending: return - remaining = xlf_pending.pop(order_id, 0) - # 아직 살아있는 pending이 없으면 state 정리 - if not xlf_pending: - if xlf_state in ("BUYING_BASKET", "BUYING_XLF"): - # 부분 체결이나 미체결로 인해 arb 포기 → IDLE 복귀 - # 이미 체결된 포지션은 다음 arb에서 자연 해소됨 - print(f" XLF 주문 out (미체결/부분체결) → IDLE 복귀. 남은:{remaining}") - xlf_state = "IDLE" - xlf_direction = None + if not xlf_pending and xlf_state in ("BUYING_BASKET", "BUYING_XLF"): + print(f" XLF 주문 out (미체결/부분체결) -> IDLE 복귀. 남은:{remaining}") + xlf_state = "IDLE" + xlf_direction = None + + # ==================== VALE 차익거래 ==================== def try_vale_arb(): nonlocal vale_state, vale_direction, vale_pending, vale_convert_oid @@ -208,24 +356,22 @@ def main(): if None in [vale_bid, vale_ask, valbz_bid, valbz_ask]: return - # 케이스 1: VALBZ 매수 → VALE 변환 → VALE 매도 profit1 = vale_bid - valbz_ask - VALE_CONVERSION_FEE if profit1 > 0 and state.get_position("VALBZ") < VALE_MAX_POSITION: - print(f" VALE 차익(VALBZ→VALE) 시작, 예상수익:{profit1}") - vale_state = "BUYING_VALBZ" - vale_direction = "VALBZ_TO_VALE" + print(f" VALE 차익(VALBZ->VALE) 시작, 예상수익:{profit1}") + vale_state = "BUYING_VALBZ" + vale_direction = "VALBZ_TO_VALE" vale_pending.clear() oid = next_id() exchange.send_add_message_ioc(oid, "VALBZ", Dir.BUY, valbz_ask, 1) vale_pending[oid] = 1 return - # 케이스 2: VALE 매수 → VALBZ 변환 → VALBZ 매도 profit2 = valbz_bid - vale_ask - VALE_CONVERSION_FEE if profit2 > 0 and state.get_position("VALE") < VALE_MAX_POSITION: - print(f" VALE 차익(VALE→VALBZ) 시작, 예상수익:{profit2}") - vale_state = "BUYING_VALE" - vale_direction = "VALE_TO_VALBZ" + print(f" VALE 차익(VALE->VALBZ) 시작, 예상수익:{profit2}") + vale_state = "BUYING_VALE" + vale_direction = "VALE_TO_VALBZ" vale_pending.clear() oid = next_id() exchange.send_add_message_ioc(oid, "VALE", Dir.BUY, vale_ask, 1) @@ -236,20 +382,19 @@ def main(): if order_id not in vale_pending: return - vale_pending[order_id] = max(0, vale_pending[order_id] - qty) if vale_pending[order_id] == 0: del vale_pending[order_id] if vale_state == "BUYING_VALBZ" and not vale_pending: - print(" VALBZ 매수 완료 → VALE 변환 시작") - vale_state = "CONVERTING" + print(" VALBZ 매수 완료 -> VALE 변환 시작") + vale_state = "CONVERTING" vale_convert_oid = next_id() exchange.send_convert_message(vale_convert_oid, "VALE", Dir.BUY, 1) elif vale_state == "BUYING_VALE" and not vale_pending: - print(" VALE 매수 완료 → VALBZ 변환 시작") - vale_state = "CONVERTING" + print(" VALE 매수 완료 -> VALBZ 변환 시작") + vale_state = "CONVERTING" vale_convert_oid = next_id() exchange.send_convert_message(vale_convert_oid, "VALE", Dir.SELL, 1) @@ -258,13 +403,13 @@ def main(): if order_id not in vale_pending: return - remaining = vale_pending.pop(order_id, 0) - if not vale_pending: - if vale_state in ("BUYING_VALBZ", "BUYING_VALE"): - print(f" VALE 주문 out (미체결) → IDLE 복귀. 남은:{remaining}") - vale_state = "IDLE" - vale_direction = None + if not vale_pending and vale_state in ("BUYING_VALBZ", "BUYING_VALE"): + print(f" VALE 주문 out (미체결) -> IDLE 복귀. 남은:{remaining}") + vale_state = "IDLE" + vale_direction = None + + # ==================== 메인 루프 ==================== vale_last_print_time = time.time() @@ -278,7 +423,8 @@ def main(): elif message["type"] == "open": print("Market opened:", message) market_open = True - place_bond_orders() + cleanup_residual_positions() + refresh_all_mm() elif message["type"] == "error": print("ERROR:", message) @@ -286,94 +432,90 @@ def main(): elif message["type"] == "reject": print("REJECT:", message) oid = message.get("order_id") - active_orders.pop(oid, None) + active_mm_orders.pop(oid, None) if oid in xlf_pending: - print(" XLF 주문 reject → IDLE 복귀") - xlf_state = "IDLE" + print(" XLF 주문 reject -> IDLE 복귀") + xlf_state = "IDLE" xlf_pending.clear() - xlf_direction = None + xlf_direction = None xlf_convert_oid = None if oid in vale_pending: - print(" VALE 주문 reject → IDLE 복귀") - vale_state = "IDLE" + print(" VALE 주문 reject -> IDLE 복귀") + vale_state = "IDLE" vale_pending.clear() - vale_direction = None + vale_direction = None vale_convert_oid = None elif message["type"] == "ack": oid = message.get("order_id") - # ★ 핵심 수정: 변환 주문의 ack인지 명확히 구분 if xlf_state == "CONVERTING" and oid == xlf_convert_oid: - print(" XLF 변환 완료 → 매도 시작") + print(" XLF 변환 완료 -> 매도 시작") if xlf_direction == "BASKET_TO_XLF": xlf_state = "SELLING_XLF" - sell_oid = next_id() - xlf_bid_now = state.bid_prices.get("XLF") - if xlf_bid_now: - exchange.send_add_message_ioc(sell_oid, "XLF", Dir.SELL, xlf_bid_now, 10) + bid_now = state.bid_prices.get("XLF") + if bid_now: + sell_oid = next_id() + exchange.send_add_message_ioc(sell_oid, "XLF", Dir.SELL, bid_now, 10) xlf_pending[sell_oid] = 10 else: - print(" XLF bid 없음 → IDLE 복귀") - xlf_state = "IDLE" + xlf_state = "IDLE" xlf_direction = None elif xlf_direction == "XLF_TO_BASKET": xlf_state = "SELLING_BASKET" for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]: - sell_oid = next_id() bid_now = state.bid_prices.get(sym) if bid_now: + sell_oid = next_id() exchange.send_add_message_ioc(sell_oid, sym, Dir.SELL, bid_now, qty) xlf_pending[sell_oid] = qty if not xlf_pending: - xlf_state = "IDLE" + xlf_state = "IDLE" xlf_direction = None elif vale_state == "CONVERTING" and oid == vale_convert_oid: - print(" VALE 변환 완료 → 매도 시작") + print(" VALE 변환 완료 -> 매도 시작") if vale_direction == "VALBZ_TO_VALE": vale_state = "SELLING_VALE" - sell_oid = next_id() - vale_bid_now = state.bid_prices.get("VALE") - if vale_bid_now: - exchange.send_add_message_ioc(sell_oid, "VALE", Dir.SELL, vale_bid_now, 1) + bid_now = state.bid_prices.get("VALE") + if bid_now: + sell_oid = next_id() + exchange.send_add_message_ioc(sell_oid, "VALE", Dir.SELL, bid_now, 1) vale_pending[sell_oid] = 1 else: - vale_state = "IDLE" + vale_state = "IDLE" vale_direction = None elif vale_direction == "VALE_TO_VALBZ": vale_state = "SELLING_VALBZ" - sell_oid = next_id() - valbz_bid_now = state.bid_prices.get("VALBZ") - if valbz_bid_now: - exchange.send_add_message_ioc(sell_oid, "VALBZ", Dir.SELL, valbz_bid_now, 1) + bid_now = state.bid_prices.get("VALBZ") + if bid_now: + sell_oid = next_id() + exchange.send_add_message_ioc(sell_oid, "VALBZ", Dir.SELL, bid_now, 1) vale_pending[sell_oid] = 1 else: - vale_state = "IDLE" + vale_state = "IDLE" vale_direction = None elif message["type"] == "out": - # ★ 핵심 수정: out 메시지 처리 oid = message.get("order_id") - active_orders.pop(oid, None) + active_mm_orders.pop(oid, None) handle_xlf_out(oid) handle_vale_out(oid) - # 매도 완료 체크 if xlf_state in ("SELLING_XLF", "SELLING_BASKET") and not xlf_pending: - print(" XLF 차익거래 완료 → IDLE 복귀") - xlf_state = "IDLE" - xlf_direction = None + print(" XLF 차익거래 완료 -> IDLE 복귀") + xlf_state = "IDLE" + xlf_direction = None xlf_convert_oid = None if vale_state in ("SELLING_VALE", "SELLING_VALBZ") and not vale_pending: - print(" VALE 차익거래 완료 → IDLE 복귀") - vale_state = "IDLE" - vale_direction = None + print(" VALE 차익거래 완료 -> IDLE 복귀") + vale_state = "IDLE" + vale_direction = None vale_convert_oid = None elif message["type"] == "fill": @@ -389,26 +531,33 @@ def main(): print(f" FILL {sym} {dir_} x{qty} @ {message['price']} | 포지션:{state.positions}") - # ★ 핵심 수정: BOND 체결 시 throttle - 너무 자주 재주문 방지 - if sym == "BOND" and oid in active_orders: - active_orders.pop(oid, None) - now = time.time() - if now - last_bond_fill_time > BOND_REORDER_DELAY: - last_bond_fill_time = now - place_bond_orders() + # 마켓메이킹 주문 체결 -> 해당 심볼 재주문 (throttle 적용) + if oid in active_mm_orders: + filled_sym = active_mm_orders.pop(oid)["sym"] + now = time.time() + last_t = last_mm_fill_time.get(filled_sym, 0.0) + delay = BOND_REORDER_DELAY if filled_sym == "BOND" else STOCK_REORDER_DELAY + + if now - last_t > delay: + last_mm_fill_time[filled_sym] = now + if filled_sym == "BOND": + place_bond_orders() + elif filled_sym in ("GS", "MS", "WFC"): + update_implied_fairs() + place_stock_orders(filled_sym) 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 + print(" XLF 차익거래 완료 -> IDLE 복귀") + xlf_state = "IDLE" + xlf_direction = None xlf_convert_oid = None 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 + print(" VALE 차익거래 완료 -> IDLE 복귀") + vale_state = "IDLE" + vale_direction = None vale_convert_oid = None elif message["type"] == "book": @@ -424,22 +573,33 @@ def main(): if now > vale_last_print_time + 1: vale_last_print_time = now print({ - "vale_bid": state.bid_prices.get("VALE"), - "vale_ask": state.ask_prices.get("VALE"), + "vale_bid": state.bid_prices.get("VALE"), + "vale_ask": state.ask_prices.get("VALE"), "valbz_bid": state.bid_prices.get("VALBZ"), "valbz_ask": state.ask_prices.get("VALBZ"), }) if sym in ["BOND", "GS", "MS", "WFC", "XLF"]: + update_implied_fairs() try_xlf_arb() + # fair value가 크게 움직이면 해당 종목 주문 즉시 갱신 + if sym in ["GS", "MS", "WFC"] and implied_fairs.get(sym) is not None: + existing = [(oid, info) for oid, info in active_mm_orders.items() + if info["sym"] == sym] + if existing: + fair = implied_fairs[sym] + prices = [info["price"] for _, info in existing] + if any(abs(p - fair) > STOCK_SPREAD * 2 for p in prices): + place_stock_orders(sym) + if sym in ["VALE", "VALBZ"]: try_vale_arb() now = time.time() if now - last_refresh > REFRESH_INTERVAL: last_refresh = now - place_bond_orders() + refresh_all_mm() # ~~~~~============== PROVIDED CODE ==============~~~~~ @@ -452,12 +612,11 @@ class Dir(str, Enum): class ExchangeConnection: def __init__(self, args): self.message_timestamps = deque(maxlen=500) - self.exchange_hostname = args.exchange_hostname - self.port = args.port - exchange_socket = self._connect(add_socket_timeout=args.add_socket_timeout) - self.reader = exchange_socket.makefile("r", 1) - self.writer = exchange_socket - + self.exchange_hostname = args.exchange_hostname + self.port = args.port + exchange_socket = self._connect(add_socket_timeout=args.add_socket_timeout) + self.reader = exchange_socket.makefile("r", 1) + self.writer = exchange_socket self._write_message({"type": "hello", "team": team_name.upper()}) def read_message(self): @@ -467,36 +626,23 @@ class ExchangeConnection: return message def send_add_message(self, order_id: int, symbol: str, dir: Dir, price: int, size: int): - """DAY 주문 (장 마감까지 유지)""" + """DAY 주문 - 마켓메이킹용""" self._write_message({ - "type": "add", - "order_id": order_id, - "symbol": symbol, - "dir": dir, - "price": price, - "size": size, - "tif": "DAY", + "type": "add", "order_id": order_id, "symbol": symbol, + "dir": dir, "price": price, "size": size, "tif": "DAY", }) def send_add_message_ioc(self, order_id: int, symbol: str, dir: Dir, price: int, size: int): - """IOC 주문 (즉시 체결 안 되면 자동 취소) - 차익거래에 사용""" + """IOC 주문 - 차익거래/청산용""" self._write_message({ - "type": "add", - "order_id": order_id, - "symbol": symbol, - "dir": dir, - "price": price, - "size": size, - "tif": "IOC", + "type": "add", "order_id": order_id, "symbol": symbol, + "dir": dir, "price": price, "size": size, "tif": "IOC", }) def send_convert_message(self, order_id: int, symbol: str, dir: Dir, size: int): self._write_message({ - "type": "convert", - "order_id": order_id, - "symbol": symbol, - "dir": dir, - "size": size, + "type": "convert", "order_id": order_id, + "symbol": symbol, "dir": dir, "size": size, }) def send_cancel_message(self, order_id: int): @@ -512,17 +658,14 @@ class ExchangeConnection: def _write_message(self, message): what_to_write = json.dumps(message) if not what_to_write.endswith("\n"): - what_to_write = what_to_write + "\n" - + what_to_write += "\n" length_to_send = len(what_to_write) total_sent = 0 while total_sent < length_to_send: - sent_this_time = self.writer.send( - what_to_write[total_sent:].encode("utf-8") - ) - if sent_this_time == 0: + sent = self.writer.send(what_to_write[total_sent:].encode("utf-8")) + if sent == 0: raise Exception("Unable to send data to exchange") - total_sent += sent_this_time + total_sent += sent now = time.time() self.message_timestamps.append(now) @@ -535,14 +678,10 @@ def parse_arguments(): test_exchange_port_offsets = {"prod-like": 0, "slower": 1, "empty": 2} parser = argparse.ArgumentParser(description="Trade on an ETC exchange!") - exchange_address_group = parser.add_mutually_exclusive_group(required=True) - exchange_address_group.add_argument("--production", action="store_true") - exchange_address_group.add_argument( - "--test", type=str, choices=test_exchange_port_offsets.keys() - ) - exchange_address_group.add_argument( - "--specific-address", type=str, metavar="HOST:PORT", help=argparse.SUPPRESS - ) + grp = parser.add_mutually_exclusive_group(required=True) + grp.add_argument("--production", action="store_true") + grp.add_argument("--test", type=str, choices=test_exchange_port_offsets.keys()) + grp.add_argument("--specific-address", type=str, metavar="HOST:PORT", help=argparse.SUPPRESS) args = parser.parse_args() args.add_socket_timeout = True @@ -566,4 +705,4 @@ if __name__ == "__main__": assert team_name != "REPLAC" + "EME", ( "Please put your team name in the variable [team_name]." ) - main() \ No newline at end of file + main()