diff --git a/prac.py b/prac.py index 00b5319..f9690a0 100644 --- a/prac.py +++ b/prac.py @@ -1,136 +1,334 @@ #!/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 +from enum import Enum import time import socket import json # ~~~~~============== CONFIGURATION ==============~~~~~ +# Replace "REPLACEME" with your team name! team_name = "HanyangFloorFunction" -# --- 상태 관리 및 유틸리티 --- -class Dir: +# ~~~~~============== 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 + + +class Dir(str, Enum): BUY = "BUY" SELL = "SELL" -class StateManager: - def __init__(self): - self.positions = {} - self.bid_prices = {} - self.ask_prices = {} - 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: - def __init__(self): - self.order_id_counter = 0 - def next_order(self): - self.order_id_counter += 1 - return self.order_id_counter - -# --- 연결 클래스 --- class ExchangeConnection: def __init__(self, args): + self.message_timestamps = deque(maxlen=500) self.exchange_hostname = args.exchange_hostname self.port = args.port - # 타임아웃 설정으로 무한 대기 방지 - self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.s.settimeout(5) - self.s.connect((self.exchange_hostname, self.port)) - self.reader = self.s.makefile("r", 1) - # 초기 Handshake + 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): - line = self.reader.readline() - if not line: return None - return json.loads(line) + """Read a single message from the exchange""" + message = json.loads(self.reader.readline()) + if "dir" in message: + message["dir"] = Dir(message["dir"]) + return message + + 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 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_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): - # 모든 메시지 끝에는 줄바꿈(\n)이 필수입니다 - self.s.send((json.dumps(message) + "\n").encode("utf-8")) + what_to_write = json.dumps(message) + if not what_to_write.endswith("\n"): + what_to_write = what_to_write + "\n" - def send_add_message(self, order_id, symbol, direction, price, size): - self._write_message({ - "type": "add", "order_id": order_id, "symbol": symbol, - "dir": direction, "price": price, "size": size, "tif": "DAY" - }) + 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 -# --- 메인 봇 로직 --- -def main(): - args = parse_arguments() - try: - exchange = ExchangeConnection(args) - except Exception as e: - print(f"연결 실패: {e}") - return + 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." + ) - state = StateManager() - om = OrderManager() - market_open = False - - # 첫 메시지 처리 (Handshake Hello) - hello_msg = exchange.read_message() - if hello_msg and "symbols" in hello_msg: - for sym_info in hello_msg["symbols"]: - state.update_position(sym_info["symbol"], sym_info["position"]) - - print("봇이 시작되었습니다. P/L 복구 모드 가동.") - - while True: - message = exchange.read_message() - if not message: continue - - msg_type = message.get("type") - - if msg_type == "open": - market_open = True - print("시장이 열렸습니다.") - - 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) - - # BOND 로직: 1000원 고정 가치를 활용한 안전 수익 - if sym == "BOND" and market_open: - pos = state.get_position("BOND") - if pos < 100: - exchange.send_add_message(om.next_order(), "BOND", Dir.BUY, 999, 100 - pos) - if pos > -100: - exchange.send_add_message(om.next_order(), "BOND", Dir.SELL, 1001, 100 + pos) - - elif msg_type == "fill": - # 체결 시 포지션 동기화 - sym, qty, direction = message["symbol"], message["size"], message["dir"] - delta = qty if direction == Dir.BUY else -qty - state.update_position(sym, delta) - print(f"[FILL] {sym} {direction} {qty} | 포지션: {state.positions.get(sym)}") - - elif msg_type == "close": - market_open = False - print("시장이 닫혔습니다.") - - elif msg_type == "error": - print(f"서버 에러: {message.get('error')}") def parse_arguments(): - parser = argparse.ArgumentParser() - parser.add_argument("--test", type=str, default="prod-like", choices=["prod-like", "slower", "empty"]) + 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 + ) + args = parser.parse_args() - # 팀 이름에 맞게 호스트네임 설정 - args.exchange_hostname = f"test-exch-{team_name.lower()}" - args.port = 22000 + (0 if args.test == "prod-like" else 1 if args.test == "slower" else 2) + 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) + 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