228 lines
8.6 KiB
Python
228 lines
8.6 KiB
Python
#!/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 ==============~~~~~
|
|
team_name = "HanyangFloorFunction"
|
|
|
|
# ~~~~~============== 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.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):
|
|
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 _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})
|
|
|
|
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 프로토콜 포트
|
|
return args
|
|
|
|
if __name__ == "__main__":
|
|
main() |