From 4d6a2dc0acce6da3305fa37379ff722a34245b64 Mon Sep 17 00:00:00 2001 From: khwkim1111 Date: Sat, 9 May 2026 14:10:45 +0900 Subject: [PATCH] commit --- prac.py | 488 ++++++++++++++++++++++---------------------------------- 1 file changed, 191 insertions(+), 297 deletions(-) diff --git a/prac.py b/prac.py index f9690a0..5453108 100644 --- a/prac.py +++ b/prac.py @@ -12,323 +12,217 @@ import socket import json # ~~~~~============== CONFIGURATION ==============~~~~~ -# Replace "REPLACEME" with your team name! team_name = "HanyangFloorFunction" -# ~~~~~============== MAIN LOOP ==============~~~~~ - -# You should put your code here! We provide some starter code as an example, -# but feel free to change/remove/edit/update any of it as you'd like. If you -# have any questions about the starter code, or what to do next, please ask us! -# -# To help you get started, the sample code below tries to buy BOND for a low -# price, and it prints the current prices for VALE every second. The sample -# code is intended to be a working example, but it needs some improvement -# before it will start making good trades! - - -def main(): - args = parse_arguments() - - exchange = ExchangeConnection(args=args) - - # Store and print the "hello" message received from the exchange. This - # contains useful information about your positions. Normally you start with - # all positions at zero, but if you reconnect during a round, you might - # have already bought/sold symbols and have non-zero positions. - hello_message = exchange.read_message() - print("First message from exchange:", hello_message) - - # Send an order for BOND at a good price, but it is low enough that it is - # unlikely it will be traded against. Maybe there is a better price to - # pick? Also, you will need to send more orders over time. - # --- BOND 마켓 메이킹 설정 --- - FAIR_VALUE = 1000 # BOND fair value (고정) - ORDER_SIZE = 30 # 주문당 수량 (크게 설정해서 체결 기회 증가) - MAX_POSITION = 100 # 최대 포지션 한도 - REFRESH_INTERVAL = 5.0 # 주문 갱신 주기 (초) - - position = 0 # 현재 BOND 포지션 - order_id = 0 # 단조 증가하는 주문 ID - active_orders = {} # {order_id: {"dir": ..., "price": ...}} - market_open = False # 시장 open 여부 (open 전에는 주문 불가) - - def next_id(): - nonlocal order_id - order_id += 1 - return order_id - - def cancel_all_bond_orders(): - """활성 BOND 주문 전부 취소""" - for oid in list(active_orders.keys()): - exchange.send_cancel_message(oid) - active_orders.pop(oid, None) - - def place_bond_orders(): - """포지션 한도 안에서 bid/ask 양방향 주문""" - if not market_open: - return - - cancel_all_bond_orders() - - buy_price = FAIR_VALUE - 1 # 999 - sell_price = FAIR_VALUE + 1 # 1001 - - # 포지션에 따라 size 비대칭 조정 - base_size = ORDER_SIZE - adjustment = abs(position) // 5 - - if position < 0: - # 숏 포지션 → 매수를 더 많이 - buy_size = min(base_size + adjustment, MAX_POSITION - position) - sell_size = max(base_size - adjustment, 1) - elif position > 0: - # 롱 포지션 → 매도를 더 많이 - buy_size = max(base_size - adjustment, 1) - sell_size = min(base_size + adjustment, MAX_POSITION + position) - else: - buy_size = base_size - sell_size = base_size - - if buy_size > 0: - bid = next_id() - exchange.send_add_message( - order_id=bid, symbol="BOND", - dir=Dir.BUY, price=buy_price, size=buy_size - ) - active_orders[bid] = {"dir": Dir.BUY, "price": buy_price} - - if sell_size > 0: - ask = next_id() - exchange.send_add_message( - order_id=ask, symbol="BOND", - dir=Dir.SELL, price=sell_price, size=sell_size - ) - active_orders[ask] = {"dir": Dir.SELL, "price": sell_price} - - print(f" BOND 주문 → 매수: {buy_price} x{buy_size}, 매도: {sell_price} x{sell_size}, 포지션: {position}") - - # 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 - # of the VALE market. - vale_bid_price, vale_ask_price = None, None - vale_last_print_time = time.time() - - last_refresh = time.time() - - # Here is the main loop of the program. It will continue to read and - # process messages in a loop until a "close" message is received. You - # should write to code handle more types of messages (and not just print - # the message). Feel free to modify any of the starter code below. - # - # Note: a common mistake people make is to call write_message() at least - # once for every read_message() response. - # - # Every message sent to the exchange generates at least one response - # message. Sending a message in response to every exchange message will - # cause a feedback loop where your bot's messages will quickly be - # rate-limited and ignored. Please, don't do that! - while True: - message = exchange.read_message() - - # Some of the message types below happen infrequently and contain - # important information to help you understand what your bot is doing, - # so they are printed in full. We recommend not always printing every - # message because it can be a lot of information to read. Instead, let - # your code handle the messages and just print the information - # important for you! - if message["type"] == "close": - print("The round has ended") - break - elif message["type"] == "open": - # 시장이 열렸을 때 주문 시작 (open 전에 주문하면 reject됨) - print("Market opened:", message) - market_open = True - place_bond_orders() - elif message["type"] == "error": - print(message) - elif message["type"] == "reject": - print(message) - # 거부된 주문은 active_orders에서 제거 - oid = message.get("order_id") - active_orders.pop(oid, None) - elif message["type"] == "fill": - print(message) - # 체결 시 포지션 업데이트 후 주문 재보충 - qty = message["size"] - if message["dir"] == Dir.BUY: - position += qty - else: - position -= qty - place_bond_orders() - elif message["type"] == "book": - if message["symbol"] == "VALE": - - def best_price(side): - if message[side]: - return message[side][0][0] - - vale_bid_price = best_price("buy") - vale_ask_price = best_price("sell") - - now = time.time() - - if now > vale_last_print_time + 1: - vale_last_print_time = now - print( - { - "vale_bid_price": vale_bid_price, - "vale_ask_price": vale_ask_price, - } - ) - - # 주기적으로 BOND 주문 갱신 (주문 만료 방지) - now = time.time() - if now - last_refresh > REFRESH_INTERVAL: - last_refresh = now - place_bond_orders() - - -# ~~~~~============== PROVIDED CODE ==============~~~~~ - -# You probably don't need to edit anything below this line, but feel free to -# ask if you have any questions about what it is doing or how it works. If you -# do need to change anything below this line, please feel free to - +# ~~~~~============== CLASSES & UTILS ==============~~~~~ 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) + + 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"]) + + # --- 핵심 설정 값 --- + BOND_FAIR = 1000 + BOND_ORDER_SIZE = 30 + BOND_MAX_POS = 100 + + XLF_FEE = 100 + XLF_PROFIT_THRESHOLD = 50 # 보수적 진입 (수수료+슬리피지 방어) + XLF_MAX_POS = 100 + + market_open = False + active_mm_orders = {} + last_refresh = time.time() + REFRESH_INTERVAL = 3.0 + + 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): + 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) + + # 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) + + # --- 메시지 처리 루프 --- + while True: + message = exchange.read_message() + if not message: continue + + 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") + break + + # 주기적 리프레시 (주문 유실 방지) + now = time.time() + if now - last_refresh > REFRESH_INTERVAL: + last_refresh = now + place_bond_orders() + +# ~~~~~============== PROVIDED CONNECTION CODE ==============~~~~~ 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._write_message({"type": "hello", "team": team_name.upper()}) + 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._write({"type": "hello", "team": team_name.upper()}) def read_message(self): - """Read a single message from the exchange""" - message = json.loads(self.reader.readline()) - if "dir" in message: - message["dir"] = Dir(message["dir"]) - return message + line = self.reader.readline() + if not line: return None + msg = json.loads(line) + if "dir" in msg: msg["dir"] = Dir(msg["dir"]) + return msg - def send_add_message( - self, order_id: int, symbol: str, dir: Dir, price: int, size: int - ): - """Add a new order""" - self._write_message( - { - "type": "add", - "order_id": order_id, - "symbol": symbol, - "dir": dir, - "price": price, - "size": size, - "tif": "DAY", # 설명서 필수 필드: DAY or IOC - } - ) + def _write(self, msg): + self.writer.send((json.dumps(msg) + "\n").encode("utf-8")) - def send_convert_message(self, order_id: int, symbol: str, dir: Dir, size: int): - """Convert between related symbols""" - self._write_message( - { - "type": "convert", - "order_id": order_id, - "symbol": symbol, - "dir": dir, - "size": size, - } - ) + 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, order_id: int): - """Cancel an existing order""" - self._write_message({"type": "cancel", "order_id": order_id}) - - def _connect(self, add_socket_timeout): - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - - if add_socket_timeout: - # Automatically raise an exception if no data has been recieved for - # multiple seconds. This should not be enabled on an "empty" test - # exchange. - s.settimeout(5) - s.connect((self.exchange_hostname, self.port)) - return s - - 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" - - 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: - raise Exception("Unable to send data to exchange") - total_sent += sent_this_time - - now = time.time() - self.message_timestamps.append(now) - if len( - self.message_timestamps - ) == self.message_timestamps.maxlen and self.message_timestamps[0] > (now - 1): - print( - "WARNING: You are sending messages too frequently. The exchange will start ignoring your messages. Make sure you are not sending a message in response to every exchange message." - ) + 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}) 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", help="Connect to the production exchange." - ) - exchange_address_group.add_argument( - "--test", - type=str, - choices=test_exchange_port_offsets.keys(), - help="Connect to a test exchange.", - ) - - # Connect to a specific host. This is only intended to be used for debugging. - exchange_address_group.add_argument( - "--specific-address", type=str, metavar="HOST:PORT", help=argparse.SUPPRESS - ) - + parser = argparse.ArgumentParser() + parser.add_argument("--test", type=str, default="prod-like") args = parser.parse_args() - args.add_socket_timeout = True - - if args.production: - args.exchange_hostname = "production" - args.port = 25000 - elif args.test: - args.exchange_hostname = "test-exch-" + team_name - args.port = 22000 + test_exchange_port_offsets[args.test] - if args.test == "empty": - args.add_socket_timeout = False - elif args.specific_address: - args.exchange_hostname, port = args.specific_address.split(":") - args.port = int(port) - + args.exchange_hostname = f"test-exch-{team_name.lower()}" + args.port = 22000 # JSON 프로토콜 포트 return args - if __name__ == "__main__": - # Check that [team_name] has been updated. - assert team_name != "REPLAC" + "EME", ( - "Please put your team name in the variable [team_name]." - ) - main() \ No newline at end of file