From 807918c3118349b336d8721ad7b7b639d2d10368 Mon Sep 17 00:00:00 2001 From: khwkim1111 Date: Sat, 9 May 2026 14:22:13 +0900 Subject: [PATCH] commit --- prac.py | 366 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 190 insertions(+), 176 deletions(-) diff --git a/prac.py b/prac.py index 5453108..b1a27fb 100644 --- a/prac.py +++ b/prac.py @@ -1,8 +1,4 @@ #!/usr/bin/env python3 -# ~~~~~============== HOW TO RUN ==============~~~~~ -# 1) Configure things in CONFIGURATION section -# 2) Change permissions: chmod +x bot.py -# 3) Run in loop: while true; do ./bot.py --test prod-like; sleep 1; done import argparse from collections import deque @@ -10,218 +6,236 @@ from enum import Enum import time import socket import json +from state import StateManager +from order import OrderManager -# ~~~~~============== CONFIGURATION ==============~~~~~ team_name = "HanyangFloorFunction" -# ~~~~~============== CLASSES & UTILS ==============~~~~~ +# ==================== 설정 ==================== +BOND_FAIR_VALUE = 1000 +BOND_SPREAD = 2 +BOND_ORDER_SIZE = 20 +BOND_MAX_POSITION = 100 +STOCK_SPREAD = 6 +STOCK_ORDER_SIZE = 5 +STOCK_MAX_POSITION = 100 +STOCK_REORDER_DELAY = 0.2 + +XLF_CONVERSION_FEE = 100 +VALE_CONVERSION_FEE = 10 + +REFRESH_INTERVAL = 1.0 +MIN_PROFIT_BUFFER = 10 +MAX_POSITION_SOFT = 50 + +# ==================== 방향 ==================== class Dir(str, Enum): BUY = "BUY" SELL = "SELL" -class StateManager: - """모든 종목의 포지션과 현재 호가 정보를 추적합니다.""" - def __init__(self): - self.positions = {s: 0 for s in ["BOND", "GS", "MS", "WFC", "XLF", "VALE", "VALBZ"]} - self.bid_prices = {s: None for s in ["BOND", "GS", "MS", "WFC", "XLF", "VALE", "VALBZ"]} - self.ask_prices = {s: None for s in ["BOND", "GS", "MS", "WFC", "XLF", "VALE", "VALBZ"]} - - def update_position(self, symbol, delta): - self.positions[symbol] = self.positions.get(symbol, 0) + delta - - def get_position(self, symbol): - return self.positions.get(symbol, 0) - - def update_bid_ask_price(self, symbol, bid, ask): - self.bid_prices[symbol] = bid - self.ask_prices[symbol] = ask - -class OrderManager: - """단조 증가하는 주문 ID를 관리합니다.""" - def __init__(self): - self.order_id_counter = 0 - def next_order(self): - self.order_id_counter += 1 - return self.order_id_counter - -# ~~~~~============== MAIN BOT LOGIC ==============~~~~~ - +# ==================== 메인 ==================== def main(): args = parse_arguments() exchange = ExchangeConnection(args=args) - # 초기 메시지 처리 및 포지션 복원 - hello_message = exchange.read_message() - print("First message from exchange:", hello_message) - + hello = exchange.read_message() state = StateManager() - om = OrderManager() - - if hello_message and "symbols" in hello_message: - for sym_info in hello_message["symbols"]: - state.update_position(sym_info["symbol"], sym_info["position"]) + om = OrderManager() - # --- 핵심 설정 값 --- - BOND_FAIR = 1000 - BOND_ORDER_SIZE = 30 - BOND_MAX_POS = 100 - - XLF_FEE = 100 - XLF_PROFIT_THRESHOLD = 50 # 보수적 진입 (수수료+슬리피지 방어) - XLF_MAX_POS = 100 + for sym_info in hello.get("symbols", []): + state.update_position(sym_info["symbol"], sym_info["position"]) - market_open = False - active_mm_orders = {} - last_refresh = time.time() - REFRESH_INTERVAL = 3.0 + implied_fairs = {"GS": None, "MS": None, "WFC": None} + active_orders = {} - def next_id(): return om.next_order() + def next_id(): + return om.next_order() - # --- 전략 1: BOND 마켓 메이킹 (사용자 기존 로직) --- - def place_bond_orders(): - if not market_open: return - - # 기존 BOND 주문 관리 (Active Order 추적) - pos = state.get_position("BOND") - adjustment = abs(pos) // 5 - - # 포지션 한도 내 수량 결정 - if pos < 0: - buy_size = min(BOND_ORDER_SIZE + adjustment, BOND_MAX_POS - pos) - sell_size = max(BOND_ORDER_SIZE - adjustment, 1) - elif pos > 0: - buy_size = max(BOND_ORDER_SIZE - adjustment, 1) - sell_size = min(BOND_ORDER_SIZE + adjustment, BOND_MAX_POS + pos) - else: - buy_size, sell_size = BOND_ORDER_SIZE, BOND_ORDER_SIZE - - # 999 매수 / 1001 매도 전략 - if buy_size > 0: - oid = next_id() - exchange.send_add_message(oid, "BOND", Dir.BUY, 999, buy_size) - active_mm_orders[oid] = {"sym": "BOND", "dir": Dir.BUY} - if sell_size > 0: - oid = next_id() - exchange.send_add_message(oid, "BOND", Dir.SELL, 1001, sell_size) - active_mm_orders[oid] = {"sym": "BOND", "dir": Dir.SELL} - - # --- 전략 2: XLF 차익거래 (ETF 바스켓 가치 계산) --- - def try_xlf_arb(): - """XLF와 바스켓(3:2:3:2) 간의 가격 괴리 이용""" - if not market_open: return - - # 필요한 호가 정보 확인 - needed = ["BOND", "GS", "MS", "WFC", "XLF"] - if any(state.bid_prices[s] is None or state.ask_prices[s] is None for s in needed): + # ==================== FAIR VALUE ==================== + def update_fair(): + try: + xlf_mid = (state.bid_prices["XLF"] + state.ask_prices["XLF"]) / 2 + gs_mid = (state.bid_prices["GS"] + state.ask_prices["GS"]) / 2 + ms_mid = (state.bid_prices["MS"] + state.ask_prices["MS"]) / 2 + wfc_mid = (state.bid_prices["WFC"] + state.ask_prices["WFC"]) / 2 + except: return - # 1. 바스켓 매수 가치 계산 (우리가 사야 할 가격의 합) - # 10 XLF = 3 BOND + 2 GS + 3 MS + 2 WFC - basket_buy_cost = (state.ask_prices["BOND"] * 3 + - state.ask_prices["GS"] * 2 + - state.ask_prices["MS"] * 3 + - state.ask_prices["WFC"] * 2) - xlf_sell_revenue = state.bid_prices["XLF"] * 10 - - # 예상 수익 = (XLF 매도금액 - 바스켓 매수비용 - 수수료) - if xlf_sell_revenue - (basket_buy_cost + XLF_FEE) > XLF_PROFIT_THRESHOLD: - if state.get_position("XLF") > -XLF_MAX_POS: - print(f" [XLF ARB] SELL XLF Opportunity! Profit: {xlf_sell_revenue - basket_buy_cost - 100}") - exchange.send_add_message(next_id(), "XLF", Dir.SELL, state.bid_prices["XLF"], 10) + residual = xlf_mid * 10 - 3000 + total = gs_mid*2 + ms_mid*3 + wfc_mid*2 + if total <= 0: + return - # 2. 바스켓 매도 가치 계산 (우리가 팔 수 있는 가격의 합) - xlf_buy_cost = state.ask_prices["XLF"] * 10 - basket_sell_revenue = (state.bid_prices["BOND"] * 3 + - state.bid_prices["GS"] * 2 + - state.bid_prices["MS"] * 3 + - state.bid_prices["WFC"] * 2) - - if basket_sell_revenue - (xlf_buy_cost + XLF_FEE) > XLF_PROFIT_THRESHOLD: - if state.get_position("XLF") < XLF_MAX_POS: - print(f" [XLF ARB] BUY XLF Opportunity! Profit: {basket_sell_revenue - xlf_buy_cost - 100}") - exchange.send_add_message(next_id(), "XLF", Dir.BUY, state.ask_prices["XLF"], 10) + raw_gs = residual * (gs_mid*2 / total) / 2 + raw_ms = residual * (ms_mid*3 / total) / 3 + raw_wfc = residual * (wfc_mid*2 / total) / 2 - # --- 메시지 처리 루프 --- + implied_fairs["GS"] = int(0.7 * gs_mid + 0.3 * raw_gs) + implied_fairs["MS"] = int(0.7 * ms_mid + 0.3 * raw_ms) + implied_fairs["WFC"] = int(0.7 * wfc_mid + 0.3 * raw_wfc) + + # ==================== BOND ARB ==================== + def bond_arb(): + bid = state.bid_prices.get("BOND") + ask = state.ask_prices.get("BOND") + + if ask and ask < 1000: + exchange.send_add_message_ioc(next_id(), "BOND", Dir.BUY, ask, 10) + + if bid and bid > 1000: + exchange.send_add_message_ioc(next_id(), "BOND", Dir.SELL, bid, 10) + + # ==================== STOCK MM ==================== + def market_make(sym): + bid = state.bid_prices.get(sym) + ask = state.ask_prices.get(sym) + fair = implied_fairs.get(sym) + + if None in [bid, ask] or fair is None: + return + + pos = state.get_position(sym) + + buy_price = min(fair - STOCK_SPREAD, bid + 1) + sell_price = max(fair + STOCK_SPREAD, ask - 1) + + if pos > 0: + buy_size, sell_size = 1, STOCK_ORDER_SIZE + 3 + elif pos < 0: + buy_size, sell_size = STOCK_ORDER_SIZE + 3, 1 + else: + buy_size = sell_size = STOCK_ORDER_SIZE + + if buy_size > 0: + exchange.send_add_message(next_id(), sym, Dir.BUY, int(buy_price), buy_size) + + if sell_size > 0: + exchange.send_add_message(next_id(), sym, Dir.SELL, int(sell_price), sell_size) + + # ==================== XLF ARB ==================== + def xlf_arb(): + try: + basket_ask = ( + state.ask_prices["BOND"]*3 + + state.ask_prices["GS"]*2 + + state.ask_prices["MS"]*3 + + state.ask_prices["WFC"]*2 + ) + xlf_bid = state.bid_prices["XLF"] + except: + return + + profit = xlf_bid*10 - basket_ask - XLF_CONVERSION_FEE + + if profit > MIN_PROFIT_BUFFER: + exchange.send_add_message_ioc(next_id(), "XLF", Dir.SELL, xlf_bid, 10) + + # ==================== VALE ARB ==================== + def vale_arb(): + vale_bid = state.bid_prices.get("VALE") + valbz_ask = state.ask_prices.get("VALBZ") + + if None in [vale_bid, valbz_ask]: + return + + if vale_bid - valbz_ask - VALE_CONVERSION_FEE > 5: + exchange.send_add_message_ioc(next_id(), "VALBZ", Dir.BUY, valbz_ask, 1) + + # ==================== 리스크 관리 ==================== + def risk(): + for sym in ["GS", "MS", "WFC"]: + pos = state.get_position(sym) + + if abs(pos) > MAX_POSITION_SOFT: + if pos > 0: + exchange.send_add_message_ioc( + next_id(), sym, Dir.SELL, + state.bid_prices.get(sym, 1), abs(pos) + ) + else: + exchange.send_add_message_ioc( + next_id(), sym, Dir.BUY, + state.ask_prices.get(sym, 99999), abs(pos) + ) + + last_refresh = time.time() + + # ==================== LOOP ==================== while True: - message = exchange.read_message() - if not message: continue + msg = exchange.read_message() - msg_type = message["type"] - - if msg_type == "open": - market_open = True - print("Market is open. Starting trades...") - place_bond_orders() - - elif msg_type == "book": - sym = message["symbol"] - bid = message["buy"][0][0] if message["buy"] else None - ask = message["sell"][0][0] if message["sell"] else None - state.update_bid_ask_price(sym, bid, ask) - - # 가격 업데이트 시마다 전략 체크 - if sym == "BOND": - place_bond_orders() - elif sym in ["GS", "MS", "WFC", "XLF"]: - try_xlf_arb() - - elif msg_type == "fill": - sym, qty, direction = message["symbol"], message["size"], message["dir"] - state.update_position(sym, qty if direction == Dir.BUY else -qty) - print(f" [FILL] {sym} {direction} x{qty} | New Pos: {state.get_position(sym)}") - if sym == "BOND": place_bond_orders() - - elif msg_type == "reject": - print(f" [REJECT] Order {message.get('order_id')}: {message.get('error')}") - active_mm_orders.pop(message.get("order_id"), None) - - elif msg_type == "close": - print("The round has ended") + if msg["type"] == "close": break - # 주기적 리프레시 (주문 유실 방지) - now = time.time() - if now - last_refresh > REFRESH_INTERVAL: - last_refresh = now - place_bond_orders() + elif msg["type"] == "book": + sym = msg["symbol"] -# ~~~~~============== PROVIDED CONNECTION CODE ==============~~~~~ + state.update_bid_ask_price( + sym, + msg["buy"][0][0] if msg["buy"] else None, + msg["sell"][0][0] if msg["sell"] else None + ) + update_fair() + + bond_arb() + xlf_arb() + vale_arb() + risk() + + if sym in ["GS", "MS", "WFC"]: + market_make(sym) + + if time.time() - last_refresh > REFRESH_INTERVAL: + last_refresh = time.time() + for s in ["GS", "MS", "WFC"]: + market_make(s) + + elif msg["type"] == "fill": + qty = msg["size"] + sym = msg["symbol"] + + if msg["dir"] == Dir.BUY: + state.update_position(sym, qty) + else: + state.update_position(sym, -qty) + +# ==================== 연결 코드 ==================== class ExchangeConnection: def __init__(self, args): - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.settimeout(5) - self.s.connect((args.exchange_hostname, args.port)) - self.reader = self.s.makefile("r", 1) - self.writer = self.s + self.exchange_hostname = args.exchange_hostname + self.port = args.port + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.connect((self.exchange_hostname, self.port)) + self.reader = s.makefile("r", 1) + self.writer = s self._write({"type": "hello", "team": team_name.upper()}) def read_message(self): - line = self.reader.readline() - if not line: return None - msg = json.loads(line) - if "dir" in msg: msg["dir"] = Dir(msg["dir"]) + msg = json.loads(self.reader.readline()) + if "dir" in msg: + msg["dir"] = Dir(msg["dir"]) return msg + def send_add_message(self, oid, sym, dir, price, size): + self._write({"type":"add","order_id":oid,"symbol":sym,"dir":dir,"price":price,"size":size,"tif":"DAY"}) + + def send_add_message_ioc(self, oid, sym, dir, price, size): + self._write({"type":"add","order_id":oid,"symbol":sym,"dir":dir,"price":price,"size":size,"tif":"IOC"}) + def _write(self, msg): - self.writer.send((json.dumps(msg) + "\n").encode("utf-8")) - - def send_add_message(self, oid, sym, direction, price, size): - self._write({"type": "add", "order_id": oid, "symbol": sym, "dir": direction, "price": price, "size": size, "tif": "DAY"}) - - def send_cancel_message(self, oid): - self._write({"type": "cancel", "order_id": oid}) - - def send_convert_message(self, oid, sym, direction, size): - self._write({"type": "convert", "order_id": oid, "symbol": sym, "dir": direction, "size": size}) + data = json.dumps(msg) + "\n" + self.writer.send(data.encode()) +# ==================== args ==================== def parse_arguments(): parser = argparse.ArgumentParser() parser.add_argument("--test", type=str, default="prod-like") args = parser.parse_args() - args.exchange_hostname = f"test-exch-{team_name.lower()}" - args.port = 22000 # JSON 프로토콜 포트 + + args.exchange_hostname = "test-exch-" + team_name + args.port = 22000 return args if __name__ == "__main__":