Compare commits

...

105 Commits

Author SHA1 Message Date
5f7e000de3 last 2026-05-10 16:08:46 +09:00
8a85cc7248 x22 2026-05-09 17:14:04 +09:00
a9ed42bbba commit 2026-05-09 17:11:56 +09:00
e12ae87be6 x2 2026-05-09 17:08:34 +09:00
01cd34058d fa dw 2026-05-09 17:02:05 +09:00
a8ad37d35c commit 2026-05-09 17:01:57 +09:00
97a11f8c5b commit 2026-05-09 16:57:32 +09:00
5d6e1103b3 addg 2026-05-09 16:54:46 +09:00
12ccb4f2fd commit 2026-05-09 16:53:44 +09:00
c16ce43a7e add team_name 2026-05-09 16:51:45 +09:00
23ee31c18f commit 2026-05-09 16:51:42 +09:00
c9f4ad93ee down arb 2026-05-09 16:49:59 +09:00
05f6f007e1 fix 2026-05-09 16:49:02 +09:00
2160cc0e2a fix 2026-05-09 16:47:19 +09:00
b2aa766f25 commit 2026-05-09 16:46:34 +09:00
9c33cae2ce commit 2026-05-09 16:41:09 +09:00
7742a759f7 add bot_x 2026-05-09 16:39:44 +09:00
c81c344560 commit 2026-05-09 16:35:42 +09:00
30e63c9fc8 sad 2026-05-09 16:27:09 +09:00
7a8704c1d4 add cross + imb 2026-05-09 16:25:08 +09:00
5bd1588e09 asd 2026-05-09 16:23:10 +09:00
d161c0b6f1 va 2 2 2026-05-09 16:22:18 +09:00
d5c06c6b1a val 22 e2 2026-05-09 16:21:56 +09:00
e8886cebc4 commit 2026-05-09 16:18:49 +09:00
a411a31feb valsd2 12 2026-05-09 16:16:45 +09:00
e179de53ab add pos_limit 2026-05-09 16:16:07 +09:00
44325c398a value 1232131 2026-05-09 16:15:24 +09:00
d075831a55 add new 2026-05-09 16:12:25 +09:00
718cafb0ff add hello 2026-05-09 16:12:25 +09:00
7668287f5d value 22123 2026-05-09 16:11:27 +09:00
52f66ddba7 value 31 2026-05-09 16:06:44 +09:00
cef21e8bbe add: value 2026-05-09 15:59:01 +09:00
1b35f00923 commit 2026-05-09 15:58:33 +09:00
13159014fe hot fix 2026-05-09 15:56:05 +09:00
e40f457ecf botx 2026-05-09 15:54:41 +09:00
8b2444e418 bot split real 1 2026-05-09 15:54:26 +09:00
e826defc15 commit 2026-05-09 15:53:04 +09:00
ad49153cfc fix 2026-05-09 15:50:21 +09:00
99f9832390 debug frag 2026-05-09 15:49:54 +09:00
19096268f7 sex54 2026-05-09 15:49:23 +09:00
7b2824219f commit 2026-05-09 15:48:55 +09:00
90a024fa83 commit 2026-05-09 15:48:55 +09:00
46255a8c05 fix convert 2026-05-09 15:47:06 +09:00
f4672d522c sex3 2026-05-09 15:45:54 +09:00
0bddbf13c5 new sex 2026-05-09 15:44:01 +09:00
c0e437eb34 add: manager 2026-05-09 15:43:17 +09:00
2ef37ffa3c correct size 2026-05-09 15:42:13 +09:00
3507723794 add dbg 2026-05-09 15:40:10 +09:00
e5a10567d2 fix fair 2026-05-09 15:36:57 +09:00
5685520f1f many change 2026-05-09 15:35:13 +09:00
887873a141 commit 2026-05-09 15:34:56 +09:00
766666cf39 md 2026-05-09 15:34:22 +09:00
d3b6e7cbbb bot split sex 2026-05-09 15:32:14 +09:00
0534e31d32 commit 2026-05-09 15:29:32 +09:00
57b27791ff some change 2026-05-09 15:25:55 +09:00
174df5c728 commit 2026-05-09 15:25:17 +09:00
8a195184e8 commit 2026-05-09 15:24:42 +09:00
52a6fd6a10 commit 2026-05-09 15:18:42 +09:00
1269ca645b commit 2026-05-09 15:15:46 +09:00
a3f1756ac9 commit 2026-05-09 15:12:35 +09:00
428291b7d4 commit 2026-05-09 15:04:52 +09:00
bc33e4f8b2 bidir 2 2026-05-09 15:01:25 +09:00
e3cc9fc4d8 bidir 2026-05-09 14:59:25 +09:00
fec41b18dd fix convert 2026-05-09 14:57:57 +09:00
d5ae98a19f commit 2026-05-09 14:56:55 +09:00
f480e23717 bot.py order.py 2026-05-09 14:56:20 +09:00
0d098403f1 포지션 완화 2026-05-09 14:52:02 +09:00
79d99ab799 commit 2026-05-09 14:46:09 +09:00
b1b851dd8d rm dbg 2026-05-09 14:45:21 +09:00
f46aeee181 fix convert 2026-05-09 14:43:28 +09:00
d3d6ddeeb4 commit 2026-05-09 14:40:33 +09:00
9b8a8bbee8 tif 2026-05-09 14:36:58 +09:00
861f54357e remove sleep 2026-05-09 14:35:22 +09:00
7dcb0538e3 add pennying 2026-05-09 14:35:05 +09:00
4e736c6080 hello 2026-05-09 14:33:53 +09:00
9aecd96cee dbg2 2026-05-09 14:32:10 +09:00
5c5775338a commit 2026-05-09 14:31:48 +09:00
d8758fafe9 add debg2 2026-05-09 14:27:53 +09:00
6c31e6d74e commit 2026-05-09 14:27:16 +09:00
8f514d9136 add debg 2026-05-09 14:26:36 +09:00
193afa3e01 commit 2026-05-09 14:24:40 +09:00
5ead7cd982 add dbg 2026-05-09 14:24:24 +09:00
807918c311 commit 2026-05-09 14:22:47 +09:00
16d2649f86 add debug 3 2026-05-09 14:19:25 +09:00
f40d6e8308 add debug 2 2026-05-09 14:18:35 +09:00
15200e1115 add debug 2026-05-09 14:16:20 +09:00
5a931485f1 fix critical 2026-05-09 14:12:59 +09:00
4d6a2dc0ac commit 2026-05-09 14:10:55 +09:00
c557936e0f add debug 2026-05-09 14:10:13 +09:00
8db85d0c0e some change in p_ 2026-05-09 14:06:47 +09:00
d31c04c1ff some change 2026-05-09 14:05:35 +09:00
f015d73e87 add many change 2026-05-09 14:04:16 +09:00
d17815f727 commit 2026-05-09 14:00:18 +09:00
207d927329 commit 2026-05-09 13:57:06 +09:00
d4cf9d29dc commit 2026-05-09 13:55:48 +09:00
2455dc33dc commit 2026-05-09 13:51:02 +09:00
b29f70b667 commit 2026-05-09 13:41:54 +09:00
7d63d020bf commit 2026-05-09 13:33:20 +09:00
a004d498ab commit 2026-05-09 13:22:16 +09:00
97ca53ba7c commit 2026-05-09 13:11:45 +09:00
ac3d1d0db9 hotfix state 2026-05-09 13:03:12 +09:00
d9dca41f18 modify state 2026-05-09 13:02:32 +09:00
f51b22c3f2 add: bot bond selling 2026-05-09 12:49:47 +09:00
21ebb1258b modify: add missing library 2026-05-09 12:49:13 +09:00
9873605c02 add state 2026-05-09 12:48:26 +09:00
22 changed files with 4422 additions and 180 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -10,6 +10,8 @@ from enum import Enum
import time import time
import socket import socket
import json import json
from state import StateManager
from order import OrderManager
# ~~~~~============== CONFIGURATION ==============~~~~~ # ~~~~~============== CONFIGURATION ==============~~~~~
# Replace "REPLACEME" with your team name! # Replace "REPLACEME" with your team name!
@@ -42,26 +44,44 @@ def main():
# Send an order for BOND at a good price, but it is low enough that it is # 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 # unlikely it will be traded against. Maybe there is a better price to
# pick? Also, you will need to send more orders over time. # pick? Also, you will need to send more orders over time.
# --- BOND 마켓 메이킹 설정 --- # --- 설정 ---
FAIR_VALUE = 1000 # BOND fair value (고정) BOND_FAIR_VALUE = 1000 # BOND fair value (고정)
ORDER_SIZE = 10 # 주문당 기본 수량 BOND_ORDER_SIZE = 50 # BOND 주문당 수량
MAX_POSITION = 100 # 최대 포지션 한도 XLF_CONVERSION_FEE = 100 # XLF 변환 비용
VALE_CONVERSION_FEE = 10 # VALE 변환 비용
VALE_ARB_SIZE = 10 # VALE 차익거래 단위
REFRESH_INTERVAL = 5.0 # 주문 갱신 주기 (초) REFRESH_INTERVAL = 5.0 # 주문 갱신 주기 (초)
position = 0 # 현재 BOND 포지션 # XLF state machine
order_id = 0 # 단조 증가하는 주문 ID # IDLE → BUYING_BASKET → CONVERTING → SELLING_XLF → IDLE
active_orders = {} # {order_id: {"dir": ..., "price": ...}} # IDLE → BUYING_XLF → CONVERTING → SELLING_BASKET → IDLE
market_open = False # 시장 open 여부 (open 전에는 주문 불가) xlf_state = "IDLE"
xlf_pending = {}
xlf_direction = None
xlf_arb_size = 0
# VALE state machine
# IDLE → BUYING_VALBZ → CONVERTING → SELLING_VALE → IDLE
# IDLE → BUYING_VALE → CONVERTING → SELLING_VALBZ → IDLE
vale_state = "IDLE"
vale_pending = {}
vale_direction = None
vale_arb_size = 0
state = StateManager()
om = OrderManager(exchange)
market_open = False
active_orders = {} # BOND 전용 {order_id: {"dir": ..., "price": ...}}
last_refresh = time.time()
def next_id(): def next_id():
nonlocal order_id return om.next_order()
order_id += 1
return order_id
def cancel_all_bond_orders(): def cancel_all_bond_orders():
"""활성 BOND 주문 전부 취소""" """활성 BOND 주문 전부 취소"""
for oid in list(active_orders.keys()): for oid in list(active_orders.keys()):
exchange.send_cancel_message(oid) om.cancel(oid)
active_orders.pop(oid, None) active_orders.pop(oid, None)
def place_bond_orders(): def place_bond_orders():
@@ -71,26 +91,28 @@ def main():
cancel_all_bond_orders() cancel_all_bond_orders()
buy_price = FAIR_VALUE - 1 # 999 buy_price = BOND_FAIR_VALUE - 1 # 999
sell_price = FAIR_VALUE + 1 # 1001 sell_price = BOND_FAIR_VALUE + 1 # 1001
position = om.positions["BOND"]
# 포지션이 음수일수록 매수 size 크게, 매도 size 작게 # 포지션에 따라 size 비대칭 조정
# 포지션이 양수일수록 매도 size 크게, 매수 size 작게 base_size = BOND_ORDER_SIZE
base_size = ORDER_SIZE adjustment = abs(position) // 5
adjustment = abs(position) // 5 # 포지션 5마다 1씩 조정
if position < 0: if position < 0:
# 숏 포지션 → 매수를 더 많이 buy_size = min(base_size + adjustment, 100 - position)
buy_size = min(base_size + adjustment, MAX_POSITION - position)
sell_size = max(base_size - adjustment, 1) sell_size = max(base_size - adjustment, 1)
elif position > 0: elif position > 0:
# 롱 포지션 → 매도를 더 많이
buy_size = max(base_size - adjustment, 1) buy_size = max(base_size - adjustment, 1)
sell_size = min(base_size + adjustment, MAX_POSITION + position) sell_size = min(base_size + adjustment, 100 + position)
else: else:
buy_size = base_size buy_size = base_size
sell_size = base_size sell_size = base_size
# 포지션 한도 초과 방지
buy_size = max(0, min(buy_size, 100 - position))
sell_size = max(0, min(sell_size, 100 + position))
if buy_size > 0: if buy_size > 0:
bid = next_id() bid = next_id()
exchange.send_add_message( exchange.send_add_message(
@@ -109,14 +131,152 @@ def main():
print(f" BOND 주문 → 매수:{buy_price} x{buy_size}, 매도:{sell_price} x{sell_size}, 포지션:{position}") print(f" BOND 주문 → 매수:{buy_price} x{buy_size}, 매도:{sell_price} x{sell_size}, 포지션:{position}")
def try_xlf_arb():
"""XLF 차익거래 시도 - IDLE 상태일 때만"""
nonlocal xlf_state, xlf_direction, xlf_pending, xlf_arb_size
if not market_open or xlf_state != "IDLE":
return
bond_ask = state.ask_prices["BOND"]
gs_ask = state.ask_prices["GS"]
ms_ask = state.ask_prices["MS"]
wfc_ask = state.ask_prices["WFC"]
xlf_bid = state.bid_prices["XLF"]
bond_bid = state.bid_prices["BOND"]
gs_bid = state.bid_prices["GS"]
ms_bid = state.bid_prices["MS"]
wfc_bid = state.bid_prices["WFC"]
xlf_ask = state.ask_prices["XLF"]
if None in [bond_ask, gs_ask, ms_ask, wfc_ask, xlf_bid,
bond_bid, gs_bid, ms_bid, wfc_bid, xlf_ask]:
return
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
# 케이스 1: 바스켓 매수 → XLF 변환 → XLF 매도
profit1 = xlf_bid * 10 - basket_ask - XLF_CONVERSION_FEE
if profit1 > 0 and om.check_pos_limit("XLF"):
print(f" XLF 차익(바스켓→XLF) 시작, 예상수익:{profit1}")
# BOND 주문 먼저 취소 (XLF용 BOND 매수가 기존 매도에 상쇄되지 않도록)
cancel_all_bond_orders()
xlf_state = "BUYING_BASKET"
xlf_direction = "BASKET_TO_XLF"
xlf_arb_size = 10
xlf_pending.clear()
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(oid, sym, Dir.BUY, state.ask_prices[sym], qty)
xlf_pending[oid] = qty
return
# 케이스 2: XLF 매수 → 바스켓 변환 → 각 종목 매도
profit2 = basket_bid - xlf_ask * 10 - XLF_CONVERSION_FEE
if profit2 > 0 and om.check_pos_limit("XLF"):
print(f" XLF 차익(XLF→바스켓) 시작, 예상수익:{profit2}")
cancel_all_bond_orders()
xlf_state = "BUYING_XLF"
xlf_direction = "XLF_TO_BASKET"
xlf_arb_size = 10
xlf_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "XLF", Dir.BUY, xlf_ask, 10)
xlf_pending[oid] = 10
def handle_xlf_fill(order_id, symbol, dir_, qty):
"""XLF state machine 체결 처리 (부분 체결 추적)"""
nonlocal xlf_state, xlf_pending
if order_id not in xlf_pending:
return
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"
exchange.send_convert_message(next_id(), "XLF", Dir.BUY, xlf_arb_size)
elif xlf_state == "BUYING_XLF" and not xlf_pending:
print(" XLF 매수 완료 → 바스켓 변환 시작")
xlf_state = "CONVERTING"
exchange.send_convert_message(next_id(), "XLF", Dir.SELL, xlf_arb_size)
def try_vale_arb():
"""VALE/VALBZ 차익거래 시도 - IDLE 상태일 때만"""
nonlocal vale_state, vale_direction, vale_pending, vale_arb_size
if not market_open or vale_state != "IDLE":
return
vale_bid = state.bid_prices["VALE"]
vale_ask = state.ask_prices["VALE"]
valbz_bid = state.bid_prices["VALBZ"]
valbz_ask = state.ask_prices["VALBZ"]
if None in [vale_bid, vale_ask, valbz_bid, valbz_ask]:
return
valbz_pos = om.positions["VALBZ"]
vale_pos = om.positions["VALE"]
# 케이스 1: VALE가 비쌀 때 → VALBZ 매수 → VALE 변환 → VALE 매도
profit1 = vale_bid - valbz_ask - VALE_CONVERSION_FEE
arb_size1 = min(VALE_ARB_SIZE, 10 - valbz_pos)
if profit1 > 0 and arb_size1 > 0:
print(f" VALE 차익(VALBZ→VALE) 시작, 예상수익:{profit1 * arb_size1}, size:{arb_size1}")
vale_state = "BUYING_VALBZ"
vale_direction = "VALBZ_TO_VALE"
vale_arb_size = arb_size1
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALBZ", Dir.BUY, valbz_ask, arb_size1)
vale_pending[oid] = arb_size1
return
# 케이스 2: VALBZ가 비쌀 때 → VALE 매수 → VALBZ 변환 → VALBZ 매도
profit2 = valbz_bid - vale_ask - VALE_CONVERSION_FEE
arb_size2 = min(VALE_ARB_SIZE, 10 - vale_pos)
if profit2 > 0 and arb_size2 > 0:
print(f" VALE 차익(VALE→VALBZ) 시작, 예상수익:{profit2 * arb_size2}, size:{arb_size2}")
vale_state = "BUYING_VALE"
vale_direction = "VALE_TO_VALBZ"
vale_arb_size = arb_size2
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALE", Dir.BUY, vale_ask, arb_size2)
vale_pending[oid] = arb_size2
def handle_vale_fill(order_id, symbol, dir_, qty):
"""VALE state machine 체결 처리 (부분 체결 추적)"""
nonlocal vale_state, vale_pending
if order_id not in vale_pending:
return
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"
exchange.send_convert_message(next_id(), "VALE", Dir.BUY, vale_arb_size)
elif vale_state == "BUYING_VALE" and not vale_pending:
print(" VALE 매수 완료 → VALBZ 변환 시작")
vale_state = "CONVERTING"
exchange.send_convert_message(next_id(), "VALE", Dir.SELL, vale_arb_size)
# Set up some variables to track the bid and ask price of a symbol. Right # 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 # now this doesn't track much information, but it's enough to get a sense
# of the VALE market. # of the VALE market.
vale_bid_price, vale_ask_price = None, None
vale_last_print_time = time.time() vale_last_print_time = time.time()
last_refresh = time.time()
# Here is the main loop of the program. It will continue to read and # 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 # 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 # should write to code handle more types of messages (and not just print
@@ -141,47 +301,148 @@ def main():
if message["type"] == "close": if message["type"] == "close":
print("The round has ended") print("The round has ended")
break break
elif message["type"] == "open": elif message["type"] == "open":
# 시장이 열렸을 때 주문 시작 (open 전에 주문하면 reject됨) # 시장이 열렸을 때 주문 시작 (open 전에 주문하면 reject됨)
print("Market opened:", message) print("Market opened:", message)
market_open = True market_open = True
place_bond_orders() place_bond_orders()
elif message["type"] == "error": elif message["type"] == "error":
print(message) print(message)
elif message["type"] == "reject": elif message["type"] == "reject":
print(message) print(message)
# 거부된 주문은 active_orders에서 제거
oid = message.get("order_id") oid = message.get("order_id")
active_orders.pop(oid, None) active_orders.pop(oid, None)
if oid in xlf_pending:
print(" XLF 주문 reject → IDLE 복귀")
xlf_state = "IDLE"
xlf_pending.clear()
xlf_direction = None
place_bond_orders()
if oid in vale_pending:
print(" VALE 주문 reject → IDLE 복귀")
vale_state = "IDLE"
vale_pending.clear()
vale_direction = None
elif message["type"] == "ack":
# XLF 변환 ack 처리
if xlf_state == "CONVERTING":
print(" XLF 변환 완료 → 매도 시작")
if xlf_direction == "BASKET_TO_XLF":
# 변환: BOND -3, GS -2, MS -3, WFC -2, XLF +10
om.positions["BOND"] -= 3
om.positions["GS"] -= 2
om.positions["MS"] -= 3
om.positions["WFC"] -= 2
om.positions["XLF"] += xlf_arb_size
xlf_state = "SELLING_XLF"
oid = next_id()
exchange.send_add_message(
oid, "XLF", Dir.SELL, state.bid_prices["XLF"], xlf_arb_size
)
xlf_pending[oid] = xlf_arb_size
elif xlf_direction == "XLF_TO_BASKET":
# 변환: XLF -10, BOND +3, GS +2, MS +3, WFC +2
om.positions["XLF"] -= xlf_arb_size
om.positions["BOND"] += 3
om.positions["GS"] += 2
om.positions["MS"] += 3
om.positions["WFC"] += 2
xlf_state = "SELLING_BASKET"
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(
oid, sym, Dir.SELL, state.bid_prices[sym], qty
)
xlf_pending[oid] = qty
# VALE 변환 ack 처리
elif vale_state == "CONVERTING":
print(" VALE 변환 완료 → 매도 시작")
if vale_direction == "VALBZ_TO_VALE":
# 변환: VALBZ -size, VALE +size
om.positions["VALBZ"] -= vale_arb_size
om.positions["VALE"] += vale_arb_size
vale_state = "SELLING_VALE"
oid = next_id()
exchange.send_add_message(
oid, "VALE", Dir.SELL, state.bid_prices["VALE"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif vale_direction == "VALE_TO_VALBZ":
# 변환: VALE -size, VALBZ +size
om.positions["VALE"] -= vale_arb_size
om.positions["VALBZ"] += vale_arb_size
vale_state = "SELLING_VALBZ"
oid = next_id()
exchange.send_add_message(
oid, "VALBZ", Dir.SELL, state.bid_prices["VALBZ"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif message["type"] == "fill": elif message["type"] == "fill":
print(message) print(message)
# 체결 시 포지션 업데이트 후 주문 재보충
qty = message["size"] qty = message["size"]
if message["dir"] == Dir.BUY: sym = message["symbol"]
position += qty dir_ = message["dir"]
oid = message["order_id"]
# 포지션 업데이트
if dir_ == Dir.BUY:
om.update_position(sym, oid, qty)
else: else:
position -= qty om.update_position(sym, oid, -qty)
print(f" 포지션 → {om.positions}")
# BOND 체결 시 무조건 재주문
if sym == "BOND" and oid in active_orders:
active_orders.pop(oid, None)
place_bond_orders() place_bond_orders()
# XLF state machine 체결 처리
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
# XLF 완료 후 BOND 주문 재배치
place_bond_orders()
# VALE state machine 체결 처리
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
elif message["type"] == "book": elif message["type"] == "book":
if message["symbol"] == "VALE": sym = message["symbol"]
state.update_bid_ask_price(
def best_price(side): sym,
if message[side]: message["buy"][0][0] if message["buy"] else None,
return message[side][0][0] message["sell"][0][0] if message["sell"] else None
)
vale_bid_price = best_price("buy")
vale_ask_price = best_price("sell")
if sym == "VALE":
now = time.time() now = time.time()
if now > vale_last_print_time + 1: if now > vale_last_print_time + 1:
vale_last_print_time = now vale_last_print_time = now
print( print({
{ "vale_bid_price": state.bid_prices["VALE"],
"vale_bid_price": vale_bid_price, "vale_ask_price": state.ask_prices["VALE"],
"vale_ask_price": vale_ask_price, })
}
) # XLF 관련 심볼 호가 업데이트마다 차익거래 시도
if sym in ["BOND", "GS", "MS", "WFC", "XLF"]:
try_xlf_arb()
# VALE/VALBZ 호가 업데이트마다 차익거래 시도
if sym in ["VALE", "VALBZ"]:
try_vale_arb()
# 주기적으로 BOND 주문 갱신 (주문 만료 방지) # 주기적으로 BOND 주문 갱신 (주문 만료 방지)
now = time.time() now = time.time()

View File

@@ -1,15 +1,16 @@
import argparse
from collections import deque from collections import deque
from enum import Enum from enum import Enum
import time
import socket import socket
import json import json
import order
team_name = "HanyangFloorFunction" team_name = "HanyangFloorFunction"
class Dir(str, Enum): class Dir(str, Enum):
BUY = "BUY" BUY = "BUY"
SELL = "SELL" SELL = "SELL"
class ExchangeConnection: class ExchangeConnection:
def __init__(self, args): def __init__(self, args):
self.message_timestamps = deque(maxlen=500) self.message_timestamps = deque(maxlen=500)
@@ -40,6 +41,7 @@ class ExchangeConnection:
"dir": dir, "dir": dir,
"price": price, "price": price,
"size": size, "size": size,
"tif": "DAY",
} }
) )
@@ -94,3 +96,40 @@ class ExchangeConnection:
"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." "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 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
)
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)
return args

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

129
bot split/bot sb.py Normal file
View File

@@ -0,0 +1,129 @@
#!/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
from state import StateManager
# ~~~~~============== 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()
state = StateManager()
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.
exchange.send_add_message(order_id=1, symbol="BOND", dir=Dir.BUY, price=990, size=1)
# 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()
# 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"] == "error":
print(message)
elif message["type"] == "reject":
print(message)
elif message["type"] == "fill":
on_fill(message)
elif message["type"] == "book":
on_book(message)
def on_fill(message):
;
def on_book(message):
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,
}
)
elif message["symbol"] == "VALBZ":
# ~~~~~============== 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
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()

View File

@@ -5,9 +5,8 @@
# 3) Run in loop: while true; do ./bot.py --test prod-like; sleep 1; done # 3) Run in loop: while true; do ./bot.py --test prod-like; sleep 1; done
import argparse import argparse
import time import time
from ETC import ExchangeConnection from ETC import ExchangeConnection, Dir, team_name
from ETC import Dir from manager import Manager
from ETC import team_name
# ~~~~~============== CONFIGURATION ==============~~~~~ # ~~~~~============== CONFIGURATION ==============~~~~~
# Replace "REPLACEME" with your team name! # Replace "REPLACEME" with your team name!
@@ -23,51 +22,27 @@ from ETC import team_name
# code is intended to be a working example, but it needs some improvement # code is intended to be a working example, but it needs some improvement
# before it will start making good trades! # before it will start making good trades!
last_print_time = 0
def main(): def main():
global last_print_time
args = parse_arguments() args = parse_arguments()
exchange = ExchangeConnection(args=args) exchange = ExchangeConnection(args=args)
man = Manager(exchange)
# 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() hello_message = exchange.read_message()
print("First message from exchange:", hello_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.
exchange.send_add_message(order_id=1, symbol="BOND", dir=Dir.BUY, price=990, size=1)
# 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()
# 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 man.orderMan.buy("BOND", 999, 99)
# once for every read_message() response. man.orderMan.sell("BOND", 1001, 99)
#
# Every message sent to the exchange generates at least one response last_print_time = time.time()
# 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: while True:
message = exchange.read_message() 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": if message["type"] == "close":
print("The round has ended") print("The round has ended")
break break
@@ -76,27 +51,73 @@ def main():
elif message["type"] == "reject": elif message["type"] == "reject":
print(message) print(message)
elif message["type"] == "fill": elif message["type"] == "fill":
print(message) on_fill(message, man)
elif message["type"] == "book": elif message["type"] == "book":
if message["symbol"] == "VALE": on_book(message, man)
def on_fill(message, man: Manager):
if (message["dir"] == Dir.BUY):
man.positionMan.update_position(message["symbol"], message["size"])
elif (message["dir"] == Dir.SELL):
man.positionMan.update_position(message["symbol"], -message["size"])
def on_book(message, man: Manager):
global last_print_time
if (message["buy"]):
man.valueMan.set_bid(message["symbol"], message["buy"][0][0])
if (message["sell"]):
man.valueMan.set_ask(message["symbol"], message["sell"][0][0])
if message["symbol"] == "VALE" or message["symbol"] == "VALBZ":
def best_price(side): def best_price(side):
if message[side]: if message[side]:
return message[side][0][0] return message[side][0][0]
# best_price("buy")
vale_bid_price = best_price("buy") # best_price("sell")
vale_ask_price = best_price("sell")
now = time.time() now = time.time()
if now > vale_last_print_time + 1: if now >= last_print_time+1:
vale_last_print_time = now last_print_time = now
print(
{ valePos = man.positionMan.get_position("VALE")
"vale_bid_price": vale_bid_price, valeBid = man.valueMan.get_bid("VALE")
"vale_ask_price": vale_ask_price, valeAsk = man.valueMan.get_ask("VALE")
} valbzPos = man.positionMan.get_position("VALBZ")
) valbzBid = man.valueMan.get_bid("VALBZ")
valbzAsk = man.valueMan.get_ask("VALBZ")
FEE = 10
if ((valeBid > 0 and valbzBid) and
valePos * valeBid + FEE <
valbzPos * valbzBid):
man.orderMan.convert("VALE", Dir.SELL, valePos)
if ((valeBid > 0 and valbzBid) and
valePos * valeBid >
valbzPos * valbzBid + FEE):
man.orderMan.convert("VALBZ", Dir.SELL, valbzPos)
elif message["symbol"] == "MS" or message["symbol"] == "GS":
now = time.time()
if now >= last_print_time+1:
last_print_time = now
if (man.valueMan.get_bid(message["symbol"])+10 < man.valueMan.get_ask(message["symbol"])):
man.orderMan.buy(message["symbol"], man.valueMan.get_bid(message["symbol"])-1, 10)
else:
man.orderMan.sell(message["symbol"], man.valueMan.get_ask(message["symbol"])+1, 10)
elif message["symbol"] == "WLF":
now = time.time()
if now >= last_print_time+1:
last_print_time = now
if (man.positionMan.get_position("BOND") > 3 and
man.positionMan.get_position("GS") > 2 and
man.positionMan.get_position("MS") > 3 and
man.positionMan.get_position("WFC") > 2):
# ~~~~~============== PROVIDED CODE ==============~~~~~ # ~~~~~============== PROVIDED CODE ==============~~~~~

10
bot split/manager.py Normal file
View File

@@ -0,0 +1,10 @@
from ETC import ExchangeConnection
from order import OrderManager
from position import PositionManager
from value import ValueManager
class Manager:
def __init__(self, exchange: ExchangeConnection):
self.positionMan = PositionManager()
self.orderMan = OrderManager(exchange, self.positionMan)
self.valueMan = ValueManager()

View File

@@ -1,52 +1,52 @@
import time import time
from collections import deque from collections import deque
from ETC import ExchangeConnection, Dir from ETC import ExchangeConnection, Dir
from position import PositionManager
class OrderManager: class OrderManager:
def __init__(self, exchange: ExchangeConnection): def __init__(self, exchange: ExchangeConnection, positionMan: PositionManager):
self.exchange = exchange self.exchange = exchange
self._order_size = 0 self._order_size = 0
# 최근 500개의 전송 시간만 기록하는 데크
self._send_timestamps = deque(maxlen=500) self._send_timestamps = deque(maxlen=500)
def next_order(self): self.positionMan = positionMan
def _next_order(self):
self._order_size += 1 self._order_size += 1
return self._order_size return self._order_size
def _attempt_send(self, action_type: str, **kwargs) -> bool: def _attempt_send(self, action_type: str, order_id: int, symbol: str, dir: Dir, price: int, size: int) -> bool:
""" """
주문 전송을 시도합니다. 주문 전송을 시도합니다.
제한에 걸려 무시되면 False를, 정상 전송되면 True를 반환합니다. 제한에 걸려 무시되면 False를, 정상 전송되면 True를 반환합니다.
""" """
now = time.time() now = time.time()
# 최근 500개를 이미 보냈다면, 가장 오래된 주문(0번 인덱스)의 시간을 확인 # if len(self._send_timestamps) == 500:
if len(self._send_timestamps) == 500: # elapsed = now - self._send_timestamps[0]
elapsed = now - self._send_timestamps[0] # if elapsed < 1.01:
if elapsed < 1.01: # return False
# 1.01초 내에 500개를 이미 보냈으므로 이 요청은 무시합니다.
# (print문은 필요시 주석 해제하여 로깅 용도로 사용하세요)
# print("전송 제한 초과! 주문이 무시되었습니다.")
return False
# 전송 조건 통과 시 실제 통신 수행 # 전송 조건 통과 시 실제 통신 수행
if action_type == "add": if action_type == "add":
self.exchange.send_add_message(**kwargs) self.exchange.send_add_message(symbol=symbol, order_id=order_id, dir=dir, price=price, size=size)
elif action_type == "cancel": elif action_type == "cancel":
self.exchange.send_cancel_message(**kwargs) self.exchange.send_cancel_message(order_id=order_id)
elif action_type == "convert": elif action_type == "convert":
self.exchange.send_convert_message(**kwargs) self.exchange.send_convert_message(symbol=symbol, order_id=order_id, dir=dir, size=size)
# 전송한 시간 기록
self._send_timestamps.append(now) self._send_timestamps.append(now)
return True return True
def sell(self, symbol: str, dir: Dir, price: int, size: int): def sell(self, symbol: str, price: int, size: int):
return self._attempt_send("add", order_id=self.next_order(), symbol=symbol, dir=dir, price=price, size=size) return self._attempt_send("add", order_id=self._next_order(), symbol=symbol, dir=Dir.SELL, price=price, size=size)
def buy(self, symbol: str, dir: Dir, price: int, size: int): def buy(self, symbol: str, price: int, size: int):
return self._attempt_send("add", order_id=self.next_order(), symbol=symbol, dir=dir, price=price, size=size) return self._attempt_send("add", order_id=self._next_order(), symbol=symbol, dir=Dir.BUY, price=price, size=size)
def convert(self, symbol: str, dir: Dir, size: int):
return self._attempt_send("convert", order_id=self._next_order(), symbol=symbol, dir=dir, size=size)
def cancel(self, order_id: int): def cancel(self, order_id: int):
return self._attempt_send("cancel", order_id=order_id) return self._attempt_send("cancel", order_id=order_id)

28
bot split/position.py Normal file
View File

@@ -0,0 +1,28 @@
class PositionManager:
def __init__(self):
self.POSITION_LIMIT = {
"BOND": 100,
"GS": 10,
"MS": 10,
"VALBZ": 100,
"VALE": 100,
"WFC": 100,
"XLF": 100
}
self.position = {
"BOND": 0,
"GS": 0,
"MS": 0,
"VALBZ": 0,
"VALE": 0,
"WFC": 0,
"XLF": 0
}
def update_position(self, symbol: str, quantity: int):
self.position[symbol] += quantity
def get_position(self, symbol: str):
return self.position[symbol]

28
bot split/value.py Normal file
View File

@@ -0,0 +1,28 @@
class ValueManager:
def __init__(self):
self.value = {
# symbol: [bid, ask]
"BOND": [0, 0],
"GS": [0, 0],
"MS": [0, 0],
"VALBZ": [0, 0],
"VALE": [0, 0],
"WFC": [0, 0],
"XLF": [0, 0]
}
def set_value(self, symbol: str, bid: int, ask: int):
self.set_bid(symbol, bid)
self.set_ask(symbol, ask)
def set_bid(self, symbol: str, bid: int):
self.value[symbol][0] = bid
def set_ask(self, symbol: str, ask: int):
self.value[symbol][1] = ask
def get_bid(self, symbol: str):
return self.value[symbol][0]
def get_ask(self, symbol: str):
return self.value[symbol][1]

114
bot.py
View File

@@ -11,6 +11,9 @@ import time
import socket import socket
import json import json
from state import StateManager
from order import OrderManager
# ~~~~~============== CONFIGURATION ==============~~~~~ # ~~~~~============== CONFIGURATION ==============~~~~~
# Replace "REPLACEME" with your team name! # Replace "REPLACEME" with your team name!
team_name = "HanyangFloorFunction" team_name = "HanyangFloorFunction"
@@ -29,48 +32,30 @@ team_name = "HanyangFloorFunction"
def main(): def main():
args = parse_arguments() args = parse_arguments()
state = StateManager()
exchange = ExchangeConnection(args=args) exchange = ExchangeConnection(args=args)
orderman = OrderManager(exchange)
# 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() hello_message = exchange.read_message()
print("First message from exchange:", hello_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 if "symbols" in hello_message:
# unlikely it will be traded against. Maybe there is a better price to for sym_data in hello_message["symbols"]:
# pick? Also, you will need to send more orders over time. sym = sym_data["symbol"]
exchange.send_add_message(order_id=1, symbol="BOND", dir=Dir.BUY, price=990, size=1) pos = sym_data["position"]
if sym in orderman.positions:
orderman.positions[sym] = pos
orderman.future_positions[sym] = pos
# Set up some variables to track the bid and ask price of a symbol. Right bond_pos = orderman.positions["BOND"]
# now this doesn't track much information, but it's enough to get a sense if bond_pos > 0:
# of the VALE market. orderman.sell("BOND", 1001, bond_pos)
vale_bid_price, vale_ask_price = None, None elif bond_pos < 0:
vale_last_print_time = time.time() orderman.buy("BOND", 999, bond_pos)
# 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: while True:
message = exchange.read_message() 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": if message["type"] == "close":
print("The round has ended") print("The round has ended")
break break
@@ -78,34 +63,66 @@ def main():
print(message) print(message)
elif message["type"] == "reject": elif message["type"] == "reject":
print(message) print(message)
elif message["type"] == "fill":
print(message)
elif message["type"] == "book": elif message["type"] == "book":
if message["symbol"] == "VALE": on_book(message, state)
elif message["type"] == "trade":
on_trade(message, orderman, state)
elif message["type"] == "fill":
on_fill(message, orderman, state)
def best_price(side):
def on_book(message: dict, state: StateManager):
symbol = message["symbol"]
def best_price(side) -> int | None:
if message[side]: if message[side]:
return message[side][0][0] return message[side][0][0]
else:
return None
vale_bid_price = best_price("buy") state.update_bid_ask_price(symbol, best_price("buy"), best_price("sell"))
vale_ask_price = best_price("sell")
now = time.time()
if now > vale_last_print_time + 1: def on_fill(message: dict, orderman: OrderManager, state: StateManager):
vale_last_print_time = now symbol = message["symbol"]
print( dir = message["dir"]
{ size = message["size"]
"vale_bid_price": vale_bid_price,
"vale_ask_price": vale_ask_price,
}
)
quantity = size if dir == Dir.BUY else -size
orderman.update_position(symbol, message["order_id"], quantity)
def on_trade(message: dict, orderman: OrderManager, state: StateManager):
symbol = message["symbol"]
price = message["price"]
state.set_last_price(symbol, price)
if orderman.positions["VALE"] - orderman.future_positions["VALE"] == 0 and orderman.positions["VALBZ"] - orderman.future_positions["VALBZ"] == 0:
execute_arb(orderman, state)
def execute_arb(orderman: OrderManager, state: StateManager):
bid_valbz, ask_valbz = state.get_best_bid_ask("VALBZ")
bid_vale, ask_vale = state.get_best_bid_ask("VALE")
if bid_valbz is None or ask_valbz is None or bid_vale is None or ask_vale is None:
return
vale_to_valbz = bid_valbz - ask_vale - 2
valbz_to_vale = bid_vale - ask_valbz - 2
if vale_to_valbz > 10:
flag = orderman.buy("VALE", bid_vale + 1, 10) and orderman.convert("VALE", Dir.SELL, 10) and orderman.sell("VALBZ", ask_valbz - 1, 10)
print(f"VALE -> VALBZ: {vale_to_valbz}, {flag}")
elif valbz_to_vale > 10:
flag = orderman.buy("VALBZ", bid_valbz + 1, 10) and orderman.convert("VALE", Dir.BUY, 10) and orderman.sell("VALE", ask_vale - 1, 10)
print(f"VALBZ -> VALE: {valbz_to_vale}, {flag}")
# ~~~~~============== PROVIDED CODE ==============~~~~~ # ~~~~~============== PROVIDED CODE ==============~~~~~
# You probably don't need to edit anything below this line, but feel free to # 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 # ask if you have any questi채푸ons about what it is doing or how it works. If you
# do need to change anything below this line, please feel free to # do need to change anything below this line, please feel free to
@@ -144,6 +161,7 @@ class ExchangeConnection:
"dir": dir, "dir": dir,
"price": price, "price": price,
"size": size, "size": size,
"tif": "DAY",
} }
) )

433
bot_new.py Normal file
View File

@@ -0,0 +1,433 @@
#!/usr/bin/env python3
import argparse
from collections import deque
from enum import Enum
import time
import socket
import json
from itertools import chain
team_name = "HanyangFloorFunction"
class Dir(str, Enum):
BUY = "BUY"
SELL = "SELL"
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()})
def read_message(self):
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):
self._write_message(
{
"type": "add",
"order_id": order_id,
"symbol": symbol,
"dir": dir,
"price": price,
"size": size,
"tif": "DAY",
}
)
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,
}
)
def send_cancel_message(self, order_id: int):
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:
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.")
class Order:
def __init__(self, symbol, size, price, dir):
self.symbol = symbol
self.size = size
self.price = price
self.dir = dir
class StateManager:
def __init__(self, exchange):
self.exchange = exchange
self.order_id_counter = -1
self.positions_by_symbol = {}
self.unacked_orders = {}
self.open_orders = {}
self.pending_cancels = set()
self.average_vale_price = []
self.average_valbz_price = []
self.average_xlf_price = []
self.CONVERSION_FEE = 10
self.ETF_COMPONENTS = {"XLF": {"BOND": 3, "GS": 2, "MS": 3, "WFC": 2}}
self.ETF_SHARES = {"XLF": 10}
self.POSITION_LIMIT = {
"BOND": 100,
"VALBZ": 10,
"VALE": 10,
"GS": 100,
"MS": 100,
"WFC": 100,
"XLF": 100,
}
self.last_strategy_time = 0
self.strategy_interval = 0.1
self.ema_fast_period = 5
self.ema_slow_period = 20
self.ema_prices = {sym: [] for sym in ["VALBZ", "VALE", "XLF"]}
self.ema_fast = {sym: None for sym in ["VALBZ", "VALE", "XLF"]}
self.ema_slow = {sym: None for sym in ["VALBZ", "VALE", "XLF"]}
self.prev_ema_fast = {sym: None for sym in ["VALBZ", "VALE", "XLF"]}
self.position_signal = {sym: 0 for sym in ["VALBZ", "VALE", "XLF"]}
self.bid_prices = {sym: None for sym in ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]}
self.ask_prices = {sym: None for sym in ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]}
self.bid_depths = {sym: 0 for sym in ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]}
self.ask_depths = {sym: 0 for sym in ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]}
def position_for_symbol(self, symbol):
return self.positions_by_symbol.get(symbol, 0)
def next_order_id(self):
self.order_id_counter += 1
return self.order_id_counter
def on_ack(self, message):
order_id = message["order_id"]
if order_id in self.unacked_orders:
self.open_orders[order_id] = self.unacked_orders.pop(order_id)
def on_fill(self, message):
order_id = message["order_id"]
symbol = message["symbol"]
dir = message["dir"]
raw_size = message["size"]
size_multiplier = 1 if dir == Dir.BUY.value else -1
size = raw_size * size_multiplier
if order_id in self.open_orders:
self.open_orders[order_id].size -= raw_size
self.positions_by_symbol[symbol] = self.positions_by_symbol.get(symbol, 0) + size
def on_out(self, message):
order_id = int(message["order_id"])
if order_id in self.open_orders:
del self.open_orders[order_id]
self.pending_cancels.discard(order_id)
def on_hello(self, message):
symbol_positions = message["symbols"]
for symbol_position in symbol_positions:
symbol = symbol_position["symbol"]
position = symbol_position["position"]
self.positions_by_symbol[symbol] = position
def on_reject(self, message):
order_id = message["order_id"]
if order_id in self.unacked_orders:
del self.unacked_orders[order_id]
def send_order(self, symbol, dir, price, size):
order_id = self.next_order_id()
order = Order(symbol, size, price, Dir(dir))
self.unacked_orders[order_id] = order
self.exchange.send_add_message(order_id, symbol, dir, price, size)
return order_id
def cancel_order(self, order_id):
self.pending_cancels.add(order_id)
self.exchange.send_cancel_message(order_id)
def open_and_pending_orders_in_symbol_and_direction_by_price_level(self, symbol, dir):
output = {}
for order_id, order in chain(self.open_orders.items(), self.unacked_orders.items()):
if order.symbol == symbol and order.dir == dir and order_id not in self.pending_cancels:
price_level = order.price
if price_level not in output:
output[price_level] = {}
output[price_level][order_id] = order
return output
def set_orders_in_symbol_for_direction(self, symbol, dir, size_by_price_level):
current_orders = self.open_and_pending_orders_in_symbol_and_direction_by_price_level(symbol, dir)
for price_level in set(size_by_price_level.keys()) | set(current_orders.keys()):
current_orders_by_order_id = current_orders.get(price_level, {})
current_size_at_price_level = sum(order.size for order in current_orders_by_order_id.values())
desired_size_for_price_level = size_by_price_level.get(price_level, 0)
if current_size_at_price_level == desired_size_for_price_level:
pass
elif current_size_at_price_level < desired_size_for_price_level:
self.send_order(symbol, dir, price_level, desired_size_for_price_level - current_size_at_price_level)
else:
for order_id in current_orders_by_order_id:
if order_id not in self.pending_cancels:
self.cancel_order(order_id)
if desired_size_for_price_level != 0:
self.send_order(symbol, dir, price_level, desired_size_for_price_level)
def get_average_price(self, average_list):
if not average_list:
return 0
return int(sum(average_list) / len(average_list))
def convert(self, symbol, dir, size):
order_id = self.next_order_id()
self.exchange.send_convert_message(order_id, symbol, dir, size)
def calculate_ema(self, prices, period):
if len(prices) < period:
return None
alpha = 2 / (period + 1)
ema = prices[0]
for price in prices[1:]:
ema = alpha * price + (1 - alpha) * ema
return ema
def update_ema_prices(self, trade_message):
symbol = trade_message["symbol"]
price = trade_message["price"]
if symbol not in ["VALBZ", "VALE", "XLF"]:
return
self.ema_prices[symbol].append(price)
max_len = self.ema_slow_period
if len(self.ema_prices[symbol]) > max_len:
self.ema_prices[symbol].pop(0)
self.prev_ema_fast[symbol] = self.ema_fast[symbol]
self.ema_fast[symbol] = self.calculate_ema(self.ema_prices[symbol], self.ema_fast_period)
self.ema_slow[symbol] = self.calculate_ema(self.ema_prices[symbol], self.ema_slow_period)
def get_cross_signal(self, symbol):
if self.ema_fast[symbol] is None or self.ema_slow[symbol] is None:
return 0
if self.prev_ema_fast[symbol] is None:
return 0
prev_fast = self.prev_ema_fast[symbol]
prev_slow = self.ema_slow[symbol]
curr_fast = self.ema_fast[symbol]
curr_slow = self.ema_slow[symbol]
if prev_fast <= prev_slow and curr_fast > curr_slow:
return 1
elif prev_fast >= prev_slow and curr_fast < curr_slow:
return -1
return 0
def update_book(self, book_message):
symbol = book_message["symbol"]
if symbol not in self.bid_prices:
return
buy = book_message.get("buy", [])
sell = book_message.get("sell", [])
if buy:
self.bid_prices[symbol] = buy[0][0]
self.bid_depths[symbol] = sum(depth for _, depth in buy[:3])
if sell:
self.ask_prices[symbol] = sell[0][0]
self.ask_depths[symbol] = sum(depth for _, depth in sell[:3])
def get_book_imbalance(self, symbol):
bid = self.bid_depths.get(symbol, 0)
ask = self.ask_depths.get(symbol, 0)
if bid + ask == 0:
return 0
return (bid - ask) / (bid + ask)
def get_fair_value_from_book(self, symbol):
bid = self.bid_prices.get(symbol)
ask = self.ask_prices.get(symbol)
if bid is not None and ask is not None:
return (bid + ask) // 2
return None
def can_add_position(self, symbol, size, is_buy):
current = self.position_for_symbol(symbol)
if is_buy:
return current + size <= self.POSITION_LIMIT.get(symbol, 100)
else:
return current - size >= -self.POSITION_LIMIT.get(symbol, 100)
def execute_strategies(self):
now = time.time()
if now - self.last_strategy_time < self.strategy_interval:
return
self.last_strategy_time = now
for sym in ["VALBZ", "VALE", "XLF"]:
ema_signal = self.get_cross_signal(sym)
imbalance = self.get_book_imbalance(sym)
fair = self.get_fair_value_from_book(sym)
if fair is None:
continue
signal_strength = abs(ema_signal) + (1 if abs(imbalance) > 0.3 else 0)
if signal_strength == 0:
continue
if ema_signal == 1 and self.position_signal[sym] <= 0:
size = 1 if self.can_add_position(sym, 1, True) else 0
if size > 0:
price = fair + 1
self.set_orders_in_symbol_for_direction(sym, "BUY", {price: size})
self.position_signal[sym] = 1
elif ema_signal == -1 and self.position_signal[sym] >= 0:
size = 1 if self.can_add_position(sym, 1, False) else 0
if size > 0:
price = fair - 1
self.set_orders_in_symbol_for_direction(sym, "SELL", {price: size})
self.position_signal[sym] = -1
self.execute_arb()
def execute_arb(self):
vale_pos = self.position_for_symbol("VALE")
valbz_pos = self.position_for_symbol("VALBZ")
xlf_pos = self.position_for_symbol("XLF")
if len(self.average_valbz_price) >= 5 and len(self.average_vale_price) >= 5:
avg_valbz = self.get_average_price(self.average_valbz_price)
avg_vale = self.get_average_price(self.average_vale_price)
spread = avg_valbz - avg_vale
if spread > self.CONVERSION_FEE:
if self.can_add_position("VALE", 1, True) and self.can_add_position("VALBZ", 1, False):
self.send_order("VALE", "BUY", avg_vale, 1)
self.convert("VALE", Dir.SELL, 1)
self.send_order("VALBZ", "SELL", avg_valbz, 1)
elif spread < -self.CONVERSION_FEE:
if self.can_add_position("VALE", 1, False) and self.can_add_position("VALBZ", 1, True):
self.send_order("VALE", "SELL", avg_vale, 1)
self.convert("VALE", Dir.BUY, 1)
self.send_order("VALBZ", "BUY", avg_valbz, 1)
def on_startup(self):
bond_pos = self.position_for_symbol("BOND")
if bond_pos > 0:
self.send_order("BOND", "SELL", 1001, min(bond_pos, self.POSITION_LIMIT["BOND"]))
elif bond_pos < 0:
self.send_order("BOND", "BUY", 999, min(-bond_pos, self.POSITION_LIMIT["BOND"]))
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.")
exchange_address_group.add_argument("--specific-address", type=str, metavar="HOST:PORT", help=argparse.SUPPRESS)
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)
return args
def main():
args = parse_arguments()
exchange = ExchangeConnection(args=args)
state_manager = StateManager(exchange)
hello_message = exchange.read_message()
print("First message from exchange:", hello_message)
state_manager.on_hello(hello_message)
state_manager.on_startup()
while True:
message = exchange.read_message()
if message["type"] == "close":
print("The round has ended")
break
elif message["type"] == "error":
print(message)
elif message["type"] == "reject":
print(message)
state_manager.on_reject(message)
elif message["type"] == "fill":
print(message)
state_manager.on_fill(message)
elif message["type"] == "trade":
state_manager.update_ema_prices(message)
state_manager.execute_strategies()
elif message["type"] == "ack":
state_manager.on_ack(message)
elif message["type"] == "out":
state_manager.on_out(message)
elif message["type"] == "book":
state_manager.update_book(message)
if __name__ == "__main__":
assert team_name != "REPLACEME", "Please put your team name in the variable [team_name]."
main()

918
bot_x.py Normal file
View File

@@ -0,0 +1,918 @@
#!/usr/bin/env python3
import argparse
from collections import deque
from enum import Enum
import time
import socket
import json
from state import StateManager
from order import OrderManager
class PriceHistory:
def __init__(self, maxlen=100):
self.prices = deque(maxlen=maxlen)
def add(self, price):
if price is not None:
self.prices.append(price)
def get_all(self):
return list(self.prices)
def __len__(self):
return len(self.prices)
class TechnicalAnalyzer:
@staticmethod
def calculate_ema(prices, period):
if len(prices) < period:
return None
alpha = 2 / (period + 1)
ema = prices[0]
for price in prices[1:]:
ema = alpha * price + (1 - alpha) * ema
return ema
@staticmethod
def calculate_rsi(prices, period=14):
if len(prices) < period + 1:
return None
gains = []
losses = []
for i in range(1, len(prices)):
diff = prices[i] - prices[i - 1]
if diff > 0:
gains.append(diff)
losses.append(0)
else:
gains.append(0)
losses.append(abs(diff))
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
return 100 - (100 / (1 + rs))
@staticmethod
def calculate_sma(prices, period):
if len(prices) < period:
return None
return sum(prices[-period:]) / period
@staticmethod
def calculate_std(prices, period):
if len(prices) < period:
return None
sma = TechnicalAnalyzer.calculate_sma(prices, period)
variance = sum((p - sma) ** 2 for p in prices[-period:]) / period
return variance ** 0.5
@staticmethod
def calculate_zscore(prices, period=20):
if len(prices) < period:
return None
sma = TechnicalAnalyzer.calculate_sma(prices, period)
std = TechnicalAnalyzer.calculate_std(prices, period)
if std is None or std == 0:
return None
return (prices[-1] - sma) / std
@staticmethod
def calculate_vwap(prices, volumes=None):
if not prices:
return None
if volumes is None:
volumes = [1] * len(prices)
total_pv = sum(p * v for p, v in zip(prices, volumes))
return total_pv / sum(volumes)
class CrossEMAStrategy:
def __init__(self, fast_period=10, slow_period=30, rsi_period=14,
oversold=30, overbought=70, size=5):
self.fast_period = fast_period
self.slow_period = slow_period
self.rsi_period = rsi_period
self.oversold = oversold
self.overbought = overbought
self.size = size
self.price_histories = {}
self.last_signal = {}
def register_symbol(self, symbol):
if symbol not in self.price_histories:
maxlen = max(self.slow_period, self.rsi_period) + 10
self.price_histories[symbol] = PriceHistory(maxlen=maxlen)
self.last_signal[symbol] = None
def update(self, symbol, price):
self.register_symbol(symbol)
self.price_histories[symbol].add(price)
def get_signal(self, symbol):
history = self.price_histories[symbol]
prices = history.get_all()
if len(prices) < self.slow_period:
return None, None, None
fast_ema = TechnicalAnalyzer.calculate_ema(prices, self.fast_period)
slow_ema = TechnicalAnalyzer.calculate_ema(prices, self.slow_period)
rsi = TechnicalAnalyzer.calculate_rsi(prices, self.rsi_period)
if fast_ema is None or slow_ema is None:
return None, None, None
prev_fast = TechnicalAnalyzer.calculate_ema(prices[:-1], self.fast_period)
prev_slow = TechnicalAnalyzer.calculate_ema(prices[:-1], self.slow_period)
if prev_fast is None or prev_slow is None:
return None, None, None
signal = None
if prev_fast <= prev_slow and fast_ema > slow_ema:
signal = "LONG"
elif prev_fast >= prev_slow and fast_ema < slow_ema:
signal = "SHORT"
return signal, rsi, (fast_ema, slow_ema)
def should_trade(self, symbol, rsi):
if rsi is None:
return True
if self.last_signal.get(symbol) == "LONG" and rsi > self.overbought:
return True
if self.last_signal.get(symbol) == "SHORT" and rsi < self.oversold:
return True
return False
class MeanReversionStrategy:
def __init__(self, lookback_period=20, zscore_threshold=2.0,
size=5, exit_threshold=0.5):
self.lookback_period = lookback_period
self.zscore_threshold = zscore_threshold
self.size = size
self.exit_threshold = exit_threshold
self.price_histories = {}
self.position = {}
self.entry_zscore = {}
def register_symbol(self, symbol):
if symbol not in self.price_histories:
maxlen = self.lookback_period + 10
self.price_histories[symbol] = PriceHistory(maxlen=maxlen)
self.position[symbol] = None
self.entry_zscore[symbol] = None
def update(self, symbol, price):
self.register_symbol(symbol)
self.price_histories[symbol].add(price)
def get_signal(self, symbol):
history = self.price_histories[symbol]
prices = history.get_all()
if len(prices) < self.lookback_period:
return None, None
zscore = TechnicalAnalyzer.calculate_zscore(prices, self.lookback_period)
if zscore is None:
return None, None
current_pos = self.position[symbol]
if current_pos is None:
if zscore > self.zscore_threshold:
return "SHORT", zscore
elif zscore < -self.zscore_threshold:
return "LONG", zscore
else:
if current_pos == "LONG" and zscore >= -self.exit_threshold:
return "CLOSE_LONG", zscore
elif current_pos == "SHORT" and zscore <= self.exit_threshold:
return "CLOSE_SHORT", zscore
return None, zscore
def set_position(self, symbol, pos):
self.position[symbol] = pos
team_name = "HanyangFloorFunction"
class OrderRateLimiter:
def __init__(self, max_per_second=500):
self.max_per_second = max_per_second
self.timestamps = deque(maxlen=max_per_second + 100)
def can_send(self):
now = time.time()
self.timestamps.append(now)
recent = [t for t in self.timestamps if now - t < 1.0]
if len(recent) >= self.max_per_second:
return False
return True
def reset_if_needed(self):
now = time.time()
while self.timestamps and now - self.timestamps[0] >= 1.0:
self.timestamps.popleft()
def main():
args = parse_arguments()
exchange = ExchangeConnection(args=args)
hello_message = exchange.read_message()
print("First message from exchange:", hello_message)
BOND_FAIR_VALUE = 1000
BOND_ORDER_SIZE = 50
XLF_CONVERSION_FEE = 100
VALE_CONVERSION_FEE = 10
VALE_ARB_SIZE = 10
REFRESH_INTERVAL = 5.0
FAST_EMA_PERIOD = 10
SLOW_EMA_PERIOD = 30
EMA_SIZE = 5
MEAN_REV_LOOKBACK = 20
MEAN_REV_ZSCORE_THRESH = 2.0
MEAN_REV_SIZE = 5
MEAN_REV_EXIT_THRESH = 0.5
xlf_state = "IDLE"
xlf_pending = {}
xlf_direction = None
xlf_arb_size = 0
vale_state = "IDLE"
vale_pending = {}
vale_direction = None
vale_arb_size = 0
state = StateManager()
om = OrderManager(exchange)
rate_limiter = OrderRateLimiter(max_per_second=500)
market_open = False
active_orders = {}
last_refresh = time.time()
cross_ema = CrossEMAStrategy(
fast_period=FAST_EMA_PERIOD,
slow_period=SLOW_EMA_PERIOD,
size=EMA_SIZE
)
mean_rev = MeanReversionStrategy(
lookback_period=MEAN_REV_LOOKBACK,
zscore_threshold=MEAN_REV_ZSCORE_THRESH,
size=MEAN_REV_SIZE,
exit_threshold=MEAN_REV_EXIT_THRESH
)
symbols_for_ema = ["VALE", "VALBZ", "GS", "MS", "WFC", "XLF"]
symbols_for_mr = ["VALE", "VALBZ", "GS", "MS", "WFC", "XLF"]
for sym in symbols_for_ema:
cross_ema.register_symbol(sym)
mean_rev.register_symbol(sym)
active_ema_orders = {}
active_mr_orders = {}
def next_id():
return om.next_order()
def cancel_all_bond_orders():
for oid in list(active_orders.keys()):
om.cancel(oid)
active_orders.pop(oid, None)
def cancel_ema_orders_for_symbol(symbol):
to_cancel = [oid for oid, info in active_ema_orders.items() if info["symbol"] == symbol]
for oid in to_cancel:
om.cancel(oid)
active_ema_orders.pop(oid, None)
def cancel_mr_orders_for_symbol(symbol):
to_cancel = [oid for oid, info in active_mr_orders.items() if info["symbol"] == symbol]
for oid in to_cancel:
om.cancel(oid)
active_mr_orders.pop(oid, None)
def place_bond_orders():
if not market_open:
return
cancel_all_bond_orders()
buy_price = BOND_FAIR_VALUE - 1
sell_price = BOND_FAIR_VALUE + 1
position = om.positions["BOND"]
base_size = BOND_ORDER_SIZE
adjustment = abs(position) // 5
if position < 0:
buy_size = min(base_size + adjustment, 100 - position)
sell_size = max(base_size - adjustment, 1)
elif position > 0:
buy_size = max(base_size - adjustment, 1)
sell_size = min(base_size + adjustment, 100 + position)
else:
buy_size = base_size
sell_size = base_size
buy_size = max(0, min(buy_size, 100 - position))
sell_size = max(0, min(sell_size, 100 + position))
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}")
def try_xlf_arb():
nonlocal xlf_state, xlf_direction, xlf_pending, xlf_arb_size
if not market_open or xlf_state != "IDLE":
return
if any(mean_rev.position.get(s) is not None for s in symbols_for_mr):
return
if any(cross_ema.last_signal.get(s) is not None for s in symbols_for_ema):
return
rate_limiter.reset_if_needed()
if not rate_limiter.can_send():
return
bond_ask = state.ask_prices["BOND"]
gs_ask = state.ask_prices["GS"]
ms_ask = state.ask_prices["MS"]
wfc_ask = state.ask_prices["WFC"]
xlf_bid = state.bid_prices["XLF"]
bond_bid = state.bid_prices["BOND"]
gs_bid = state.bid_prices["GS"]
ms_bid = state.bid_prices["MS"]
wfc_bid = state.bid_prices["WFC"]
xlf_ask = state.ask_prices["XLF"]
if None in [bond_ask, gs_ask, ms_ask, wfc_ask, xlf_bid,
bond_bid, gs_bid, ms_bid, wfc_bid, xlf_ask]:
return
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
profit1 = xlf_bid * 10 - basket_ask - XLF_CONVERSION_FEE
if profit1 > 0 and om.check_pos_limit("XLF"):
print(f" XLF 차익(바스켓→XLF) 시작, 예상수익:{profit1}")
cancel_all_bond_orders()
xlf_state = "BUYING_BASKET"
xlf_direction = "BASKET_TO_XLF"
xlf_arb_size = 10
xlf_pending.clear()
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(oid, sym, Dir.BUY, state.ask_prices[sym], qty)
xlf_pending[oid] = qty
return
profit2 = basket_bid - xlf_ask * 10 - XLF_CONVERSION_FEE
if profit2 > 0 and om.check_pos_limit("XLF"):
print(f" XLF 차익(XLF→바스켓) 시작, 예상수익:{profit2}")
cancel_all_bond_orders()
xlf_state = "BUYING_XLF"
xlf_direction = "XLF_TO_BASKET"
xlf_arb_size = 10
xlf_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "XLF", Dir.BUY, xlf_ask, 10)
xlf_pending[oid] = 10
def handle_xlf_fill(order_id, symbol, dir_, qty):
nonlocal xlf_state, xlf_pending
if order_id not in xlf_pending:
return
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"
exchange.send_convert_message(next_id(), "XLF", Dir.BUY, xlf_arb_size)
elif xlf_state == "BUYING_XLF" and not xlf_pending:
print(" XLF 매수 완료 → 바스켓 변환 시작")
xlf_state = "CONVERTING"
exchange.send_convert_message(next_id(), "XLF", Dir.SELL, xlf_arb_size)
def try_vale_arb():
nonlocal vale_state, vale_direction, vale_pending, vale_arb_size
if not market_open or vale_state != "IDLE":
return
if any(mean_rev.position.get(s) is not None for s in symbols_for_mr):
return
if any(cross_ema.last_signal.get(s) is not None for s in symbols_for_ema):
return
rate_limiter.reset_if_needed()
if not rate_limiter.can_send():
return
vale_bid = state.bid_prices["VALE"]
vale_ask = state.ask_prices["VALE"]
valbz_bid = state.bid_prices["VALBZ"]
valbz_ask = state.ask_prices["VALBZ"]
if None in [vale_bid, vale_ask, valbz_bid, valbz_ask]:
return
valbz_pos = om.positions["VALBZ"]
vale_pos = om.positions["VALE"]
profit1 = vale_bid - valbz_ask - VALE_CONVERSION_FEE
arb_size1 = min(VALE_ARB_SIZE, 10 - valbz_pos)
if profit1 > 0 and arb_size1 > 0:
print(f" VALE 차익(VALBZ→VALE) 시작, 예상수익:{profit1 * arb_size1}, size:{arb_size1}")
vale_state = "BUYING_VALBZ"
vale_direction = "VALBZ_TO_VALE"
vale_arb_size = arb_size1
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALBZ", Dir.BUY, valbz_ask, arb_size1)
vale_pending[oid] = arb_size1
return
profit2 = valbz_bid - vale_ask - VALE_CONVERSION_FEE
arb_size2 = min(VALE_ARB_SIZE, 10 - vale_pos)
if profit2 > 0 and arb_size2 > 0:
print(f" VALE 차익(VALE→VALBZ) 시작, 예상수익:{profit2 * arb_size2}, size:{arb_size2}")
vale_state = "BUYING_VALE"
vale_direction = "VALE_TO_VALBZ"
vale_arb_size = arb_size2
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALE", Dir.BUY, vale_ask, arb_size2)
vale_pending[oid] = arb_size2
def handle_vale_fill(order_id, symbol, dir_, qty):
nonlocal vale_state, vale_pending
if order_id not in vale_pending:
return
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"
exchange.send_convert_message(next_id(), "VALE", Dir.BUY, vale_arb_size)
elif vale_state == "BUYING_VALE" and not vale_pending:
print(" VALE 매수 완료 → VALBZ 변환 시작")
vale_state = "CONVERTING"
exchange.send_convert_message(next_id(), "VALE", Dir.SELL, vale_arb_size)
def try_ema_trade():
if not market_open:
return
if xlf_state != "IDLE" or vale_state != "IDLE":
return
if any(mean_rev.position.get(s) is not None for s in symbols_for_mr):
return
rate_limiter.reset_if_needed()
if not rate_limiter.can_send():
return
for symbol in symbols_for_ema:
mid_price = state.get_mid_price(symbol)
if mid_price is None:
continue
cross_ema.update(symbol, mid_price)
signal, rsi, ema_values = cross_ema.get_signal(symbol)
if signal is None:
continue
current_pos = om.positions[symbol]
limit = om.POSITIONS_LIMIT.get(symbol, 100)
remaining_capacity = limit - current_pos if signal == "LONG" else limit + current_pos
if remaining_capacity <= 0:
continue
size = min(cross_ema.size, remaining_capacity)
cancel_ema_orders_for_symbol(symbol)
if signal == "LONG":
bid_price = state.bid_prices[symbol]
if bid_price is not None and om.check_pos_limit(symbol):
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.BUY, bid_price + 1, size)
active_ema_orders[oid] = {"symbol": symbol, "dir": "LONG"}
cross_ema.last_signal[symbol] = "LONG"
print(f" CrossEMA: {symbol} LONG (bid:{bid_price}, rsi:{rsi:.1f}, ema:{ema_values})")
elif signal == "SHORT":
ask_price = state.ask_prices[symbol]
if ask_price is not None and om.check_pos_limit(symbol):
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.SELL, ask_price - 1, size)
active_ema_orders[oid] = {"symbol": symbol, "dir": "SHORT"}
cross_ema.last_signal[symbol] = "SHORT"
print(f" CrossEMA: {symbol} SHORT (ask:{ask_price}, rsi:{rsi:.1f}, ema:{ema_values})")
def try_mean_reversion_trade():
if not market_open:
return
if xlf_state != "IDLE" or vale_state != "IDLE":
return
rate_limiter.reset_if_needed()
if not rate_limiter.can_send():
return
for symbol in symbols_for_mr:
mid_price = state.get_mid_price(symbol)
if mid_price is None:
continue
mean_rev.update(symbol, mid_price)
signal, zscore = mean_rev.get_signal(symbol)
if signal is None:
continue
current_pos = om.positions[symbol]
limit = om.POSITIONS_LIMIT.get(symbol, 100)
if signal == "LONG":
remaining_capacity = limit - current_pos
if remaining_capacity <= 0:
continue
size = min(mean_rev.size, remaining_capacity)
cancel_mr_orders_for_symbol(symbol)
bid_price = state.bid_prices[symbol]
if bid_price is not None and om.check_pos_limit(symbol):
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.BUY, bid_price + 1, size)
active_mr_orders[oid] = {"symbol": symbol, "dir": "LONG"}
mean_rev.set_position(symbol, "LONG")
mean_rev.entry_zscore[symbol] = zscore
print(f" MeanRev: {symbol} LONG (zscore:{zscore:.2f})")
elif signal == "SHORT":
remaining_capacity = limit + current_pos
if remaining_capacity <= 0:
continue
size = min(mean_rev.size, remaining_capacity)
cancel_mr_orders_for_symbol(symbol)
ask_price = state.ask_prices[symbol]
if ask_price is not None and om.check_pos_limit(symbol):
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.SELL, ask_price - 1, size)
active_mr_orders[oid] = {"symbol": symbol, "dir": "SHORT"}
mean_rev.set_position(symbol, "SHORT")
mean_rev.entry_zscore[symbol] = zscore
print(f" MeanRev: {symbol} SHORT (zscore:{zscore:.2f})")
elif signal == "CLOSE_LONG":
if current_pos <= 0:
mean_rev.set_position(symbol, None)
continue
size = min(current_pos, mean_rev.size)
cancel_mr_orders_for_symbol(symbol)
ask_price = state.ask_prices[symbol]
if ask_price is not None:
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.SELL, ask_price - 1, size)
active_mr_orders[oid] = {"symbol": symbol, "dir": "CLOSE_LONG"}
print(f" MeanRev: {symbol} CLOSE_LONG (zscore:{zscore:.2f})")
elif signal == "CLOSE_SHORT":
if current_pos >= 0:
mean_rev.set_position(symbol, None)
continue
size = min(abs(current_pos), mean_rev.size)
cancel_mr_orders_for_symbol(symbol)
bid_price = state.bid_prices[symbol]
if bid_price is not None:
oid = next_id()
exchange.send_add_message(oid, symbol, Dir.BUY, bid_price + 1, size)
active_mr_orders[oid] = {"symbol": symbol, "dir": "CLOSE_SHORT"}
print(f" MeanRev: {symbol} CLOSE_SHORT (zscore:{zscore:.2f})")
vale_last_print_time = time.time()
while True:
message = exchange.read_message()
if message["type"] == "close":
print("The round has ended")
break
elif message["type"] == "open":
print("Market opened:", message)
market_open = True
place_bond_orders()
elif message["type"] == "error":
print(message)
elif message["type"] == "reject":
print(message)
oid = message.get("order_id")
active_orders.pop(oid, None)
active_ema_orders.pop(oid, None)
active_mr_orders.pop(oid, None)
if oid in xlf_pending:
print(" XLF 주문 reject → IDLE 복귀")
xlf_state = "IDLE"
xlf_pending.clear()
xlf_direction = None
place_bond_orders()
if oid in vale_pending:
print(" VALE 주문 reject → IDLE 복귀")
vale_state = "IDLE"
vale_pending.clear()
vale_direction = None
elif message["type"] == "ack":
if xlf_state == "CONVERTING":
print(" XLF 변환 완료 → 매도 시작")
if xlf_direction == "BASKET_TO_XLF":
om.positions["BOND"] -= 3
om.positions["GS"] -= 2
om.positions["MS"] -= 3
om.positions["WFC"] -= 2
om.positions["XLF"] += xlf_arb_size
xlf_state = "SELLING_XLF"
oid = next_id()
exchange.send_add_message(
oid, "XLF", Dir.SELL, state.bid_prices["XLF"], xlf_arb_size
)
xlf_pending[oid] = xlf_arb_size
elif xlf_direction == "XLF_TO_BASKET":
om.positions["XLF"] -= xlf_arb_size
om.positions["BOND"] += 3
om.positions["GS"] += 2
om.positions["MS"] += 3
om.positions["WFC"] += 2
xlf_state = "SELLING_BASKET"
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(
oid, sym, Dir.SELL, state.bid_prices[sym], qty
)
xlf_pending[oid] = qty
elif vale_state == "CONVERTING":
print(" VALE 변환 완료 → 매도 시작")
if vale_direction == "VALBZ_TO_VALE":
om.positions["VALBZ"] -= vale_arb_size
om.positions["VALE"] += vale_arb_size
vale_state = "SELLING_VALE"
oid = next_id()
exchange.send_add_message(
oid, "VALE", Dir.SELL, state.bid_prices["VALE"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif vale_direction == "VALE_TO_VALBZ":
om.positions["VALE"] -= vale_arb_size
om.positions["VALBZ"] += vale_arb_size
vale_state = "SELLING_VALBZ"
oid = next_id()
exchange.send_add_message(
oid, "VALBZ", Dir.SELL, state.bid_prices["VALBZ"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif message["type"] == "fill":
print(message)
qty = message["size"]
sym = message["symbol"]
dir_ = message["dir"]
oid = message["order_id"]
if dir_ == Dir.BUY:
om.update_position(sym, oid, qty)
else:
om.update_position(sym, oid, -qty)
print(f" 포지션 → {om.positions}")
if sym == "BOND" and oid in active_orders:
active_orders.pop(oid, None)
place_bond_orders()
if oid in active_ema_orders:
active_ema_orders.pop(oid, None)
if oid in active_mr_orders:
info = active_mr_orders.pop(oid)
if info["dir"] in ("CLOSE_LONG", "CLOSE_SHORT"):
mean_rev.set_position(sym, None)
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
place_bond_orders()
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
elif message["type"] == "book":
sym = message["symbol"]
state.update_bid_ask_price(
sym,
message["buy"][0][0] if message["buy"] else None,
message["sell"][0][0] if message["sell"] else None
)
if sym == "VALE":
now = time.time()
if now > vale_last_print_time + 1:
vale_last_print_time = now
print({
"vale_bid_price": state.bid_prices["VALE"],
"vale_ask_price": state.ask_prices["VALE"],
})
if sym in ["BOND", "GS", "MS", "WFC", "XLF"]:
try_xlf_arb()
if sym in ["VALE", "VALBZ"]:
try_vale_arb()
if sym in symbols_for_ema:
try_mean_reversion_trade()
if sym in symbols_for_ema:
try_ema_trade()
now = time.time()
if now - last_refresh > REFRESH_INTERVAL:
last_refresh = now
place_bond_orders()
class Dir(str, Enum):
BUY = "BUY"
SELL = "SELL"
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()})
def read_message(self):
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
):
self._write_message(
{
"type": "add",
"order_id": order_id,
"symbol": symbol,
"dir": dir,
"price": price,
"size": size,
"tif": "DAY",
}
)
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,
}
)
def send_cancel_message(self, order_id: int):
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:
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 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.",
)
exchange_address_group.add_argument(
"--specific-address", type=str, metavar="HOST:PORT", help=argparse.SUPPRESS
)
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)
return args
if __name__ == "__main__":
assert team_name != "REPLAC" + "EME", (
"Please put your team name in the variable [team_name]."
)
main()

1008
bot_x2.py Normal file

File diff suppressed because it is too large Load Diff

204
new_prac.py Normal file
View File

@@ -0,0 +1,204 @@
#!/usr/bin/env python3
import argparse
from collections import deque
from enum import Enum
import time
import socket
import json
from state import StateManager
from order import OrderManager
team_name = "HanyangFloorFunction"
MAX_ARBS = 5
XLF_SIZE = 50
VALE_SIZE = 50
def main():
args = parse_arguments()
exchange = ExchangeConnection(args=args)
hello_message = exchange.read_message()
print("First message:", hello_message)
state = StateManager()
om = OrderManager(exchange)
market_open = False
xlf_pending = {}
def next_id():
return om.next_order()
# ---------------- BOND ----------------
def place_bond_orders():
if not market_open:
return
bid = state.bid_prices["BOND"]
ask = state.ask_prices["BOND"]
if bid is None or ask is None:
return
buy_price = bid + 1
sell_price = ask - 1
size = 100
exchange.send_add_message(next_id(), "BOND", Dir.BUY, buy_price, size)
exchange.send_add_message(next_id(), "BOND", Dir.SELL, sell_price, size)
# ---------------- XLF ----------------
def try_xlf_arb():
if not market_open:
return
if len(xlf_pending) >= MAX_ARBS:
return
bond_ask = state.ask_prices["BOND"]
gs_ask = state.ask_prices["GS"]
ms_ask = state.ask_prices["MS"]
wfc_ask = state.ask_prices["WFC"]
xlf_bid = state.bid_prices["XLF"]
bond_bid = state.bid_prices["BOND"]
gs_bid = state.bid_prices["GS"]
ms_bid = state.bid_prices["MS"]
wfc_bid = state.bid_prices["WFC"]
xlf_ask = state.ask_prices["XLF"]
if None in [bond_ask, gs_ask, ms_ask, wfc_ask, xlf_bid,
bond_bid, gs_bid, ms_bid, wfc_bid, xlf_ask]:
return
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
profit1 = xlf_bid * 10 - basket_ask - 100
profit2 = basket_bid - xlf_ask * 10 - 100
# 🔥 공격 (손해도 감수)
if profit1 > -50:
for sym, qty in [("BOND",3),("GS",2),("MS",3),("WFC",2)]:
oid = next_id()
exchange.send_add_message(oid, sym, Dir.BUY, state.ask_prices[sym], qty)
xlf_pending[oid] = qty
elif profit2 > -50:
oid = next_id()
exchange.send_add_message(oid, "XLF", Dir.BUY, xlf_ask, XLF_SIZE)
xlf_pending[oid] = XLF_SIZE
# ---------------- VALE ----------------
def try_vale_arb():
if not market_open:
return
vale_bid = state.bid_prices["VALE"]
vale_ask = state.ask_prices["VALE"]
valbz_bid = state.bid_prices["VALBZ"]
valbz_ask = state.ask_prices["VALBZ"]
if None in [vale_bid, vale_ask, valbz_bid, valbz_ask]:
return
profit1 = vale_bid - valbz_ask - 10
profit2 = valbz_bid - vale_ask - 10
if profit1 > -5:
exchange.send_add_message(next_id(), "VALBZ", Dir.BUY, valbz_ask, VALE_SIZE)
elif profit2 > -5:
exchange.send_add_message(next_id(), "VALE", Dir.BUY, vale_ask, VALE_SIZE)
# ---------------- MAIN LOOP ----------------
while True:
message = exchange.read_message()
if message["type"] == "close":
print("Round ended")
break
elif message["type"] == "open":
print("Market opened")
market_open = True
place_bond_orders()
elif message["type"] == "fill":
sym = message["symbol"]
qty = message["size"]
dir_ = message["dir"]
if dir_ == Dir.BUY:
om.update_position(sym, message["order_id"], qty)
else:
om.update_position(sym, message["order_id"], -qty)
print("Position:", om.positions)
elif message["type"] == "book":
sym = message["symbol"]
state.update_bid_ask_price(
sym,
message["buy"][0][0] if message["buy"] else None,
message["sell"][0][0] if message["sell"] else None
)
# 핵심 실행
try_xlf_arb()
try_vale_arb()
place_bond_orders()
class Dir(str, Enum):
BUY = "BUY"
SELL = "SELL"
class ExchangeConnection:
def __init__(self, args):
self.exchange_hostname = args.exchange_hostname
self.port = args.port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((self.exchange_hostname, self.port))
self.reader = sock.makefile("r", 1)
self.writer = sock
self._write({"type": "hello", "team": team_name})
def read_message(self):
msg = json.loads(self.reader.readline())
if "dir" in msg:
msg["dir"] = Dir(msg["dir"])
return msg
def send_add_message(self, order_id, symbol, dir, price, size):
self._write({
"type": "add",
"order_id": order_id,
"symbol": symbol,
"dir": dir,
"price": price,
"size": size,
"tif": "DAY"
})
def _write(self, msg):
self.writer.send((json.dumps(msg) + "\n").encode())
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("--test", required=True)
args = parser.parse_args()
args.exchange_hostname = "test-exch-" + team_name
args.port = 22000
return args
if __name__ == "__main__":
main()

161
order.py
View File

@@ -1,10 +1,165 @@
class OrderManager: import time
_order_size = 0 from collections import deque
from enum import Enum
def __init__(self):
class Dir(str, Enum):
BUY = "BUY"
SELL = "SELL"
class OrderManager:
def __init__(self, exchange: "ExchangeConnection"):
symbols = ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]
self.POSITIONS_LIMIT = {
"BOND": 100,
"VALBZ": 10,
"VALE": 10,
"GS": 100,
"MS": 100,
"WFC": 100,
"XLF": 100,
}
self.exchange = exchange
self._order_size = 0 self._order_size = 0
# 최근 500개의 전송 시간만 기록하는 데크
self._send_timestamps = deque(maxlen=500)
self.orders = {}
self.positions = {s: 0 for s in symbols}
self.future_positions = {s: 0 for s in symbols}
def add_order(self, order_id: int, symbol: str, quantity: int):
self.orders[order_id] = {"symbol": symbol, "quantity": quantity}
def get_order(self, order_id: int):
return self.orders.get(order_id)
def next_order(self): def next_order(self):
self._order_size += 1 self._order_size += 1
return self._order_size return self._order_size
def _attempt_send(self, action_type: str, **kwargs) -> bool:
"""
주문 전송을 시도합니다.
제한에 걸려 무시되면 False를, 정상 전송되면 True를 반환합니다.
"""
now = time.time()
# 최근 500개를 이미 보냈다면, 가장 오래된 주문(0번 인덱스)의 시간을 확인
if len(self._send_timestamps) == 500:
elapsed = now - self._send_timestamps[0]
if elapsed < 1.01:
# 1.01초 내에 500개를 이미 보냈으므로 이 요청은 무시합니다.
# (print문은 필요시 주석 해제하여 로깅 용도로 사용하세요)
# print("전송 제한 초과! 주문이 무시되었습니다.")
return False
# 전송 조건 통과 시 실제 통신 수행
if action_type == "add":
size = kwargs["size"]
dir = kwargs["dir"]
if dir == Dir.SELL:
size = -size
if (
self.future_positions[kwargs["symbol"]] + size
> self.POSITIONS_LIMIT[kwargs["symbol"]]
):
return False
self.future_positions[kwargs["symbol"]] += size
self.exchange.send_add_message(**kwargs)
elif action_type == "cancel":
self.exchange.send_cancel_message(**kwargs)
elif action_type == "convert":
self.exchange.send_convert_message(**kwargs)
# 전송한 시간 기록
self._send_timestamps.append(now)
return True
def sell(self, symbol: str, price: int, size: int):
return self._attempt_send(
"add",
order_id=self.next_order(),
symbol=symbol,
dir=Dir.SELL,
price=price,
size=size,
)
def buy(self, symbol: str, price: int, size: int):
return self._attempt_send(
"add",
order_id=self.next_order(),
symbol=symbol,
dir=Dir.BUY,
price=price,
size=size,
)
def buy_convert(self, symbol: str, size: int):
if symbol == "VALE":
self.future_positions["VALE"] -= size
self.future_positions["VALBZ"] += size
self.positions["VALE"] -= size
self.positions["VALBZ"] += size
return self.exchange.send_convert_message(
order_id=self.next_order(),
symbol=symbol,
dir="BUY",
size=size,
)
def sell_convert(self, symbol: str, size: int):
if symbol == "VALE":
self.future_positions["VALE"] += size
self.future_positions["VALBZ"] -= size
self.positions["VALE"] += size
self.positions["VALBZ"] -= size
return self.exchange.send_convert_message(
order_id=self.next_order(),
symbol=symbol,
dir="SELL",
size=size,
)
def convert(self, symbol: str, dir: str, size: int):
result = self._attempt_send(
"convert",
order_id=self.next_order(),
symbol=symbol,
dir=dir,
size=size,
)
if result and symbol == "VALE":
if dir == Dir.BUY:
self.positions["VALE"] -= size
self.positions["VALBZ"] += size
self.future_positions["VALE"] -= size
self.future_positions["VALBZ"] += size
else:
self.positions["VALE"] += size
self.positions["VALBZ"] -= size
self.future_positions["VALE"] += size
self.future_positions["VALBZ"] -= size
return result
def cancel(self, order_id: int):
return self._attempt_send("cancel", order_id=order_id)
def update_position(self, symbol: str, order_id: int, quantity: int):
self.positions[symbol] = self.positions.get(symbol, 0) + quantity
if order_id in self.orders:
new_size = self.orders[order_id]["quantity"] + quantity
if new_size <= 0:
del self.orders[order_id]
else:
self.orders[order_id]["quantity"] = new_size
def check_pos_limit(self, symbol: str) -> bool:
return abs(self.positions[symbol]) < self.POSITIONS_LIMIT[symbol]

228
prac copy.py Normal file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
import argparse
from enum import Enum
import socket
import json
from state import StateManager
team_name = "HanyangFloorFunction"
# ==================== 설정 ====================
ORDER_SIZE = 2
MAX_POS = 20
ARB_THRESHOLD = 40
# ====================
class Dir(str, Enum):
BUY = "BUY"
SELL = "SELL"
# ====================
def main():
args = parse_arguments()
exchange = ExchangeConnection(args)
state = StateManager()
hello = exchange.read_message()
# 포지션 직접 관리
positions = {}
for sym in hello["symbols"]:
positions[sym["symbol"]] = sym["position"]
order_id = 0
def next_id():
nonlocal order_id
order_id += 1
return order_id
active_orders = {}
# ==================== MARKET MAKING ====================
def market_make(sym):
bid = state.bid_prices.get(sym)
ask = state.ask_prices.get(sym)
if bid is None or ask is None:
return
if ask - bid <= 2:
return
pos = positions.get(sym, 0)
if abs(pos) > MAX_POS:
return
buy_price = bid + 1
sell_price = ask - 1
if buy_price >= sell_price:
return
# 포지션 조절
if pos > 0:
buy_size = 1
sell_size = ORDER_SIZE + 1
elif pos < 0:
buy_size = ORDER_SIZE + 1
sell_size = 1
else:
buy_size = sell_size = ORDER_SIZE
oid = next_id()
exchange.send_add_message(oid, sym, Dir.BUY, buy_price, buy_size)
active_orders[oid] = sym
oid = next_id()
exchange.send_add_message(oid, sym, Dir.SELL, sell_price, sell_size)
active_orders[oid] = sym
# ==================== BOND 안전 수익 ====================
def bond_arb():
bid = state.bid_prices.get("BOND")
ask = state.ask_prices.get("BOND")
if ask and ask < 999:
exchange.send_add_message_ioc(next_id(), "BOND", Dir.BUY, ask, 3)
if bid and bid > 1001:
exchange.send_add_message_ioc(next_id(), "BOND", Dir.SELL, bid, 3)
# ==================== XLF 차익거래 (초안전 버전) ====================
def xlf_arb():
bond_ask = state.ask_prices.get("BOND")
gs_ask = state.ask_prices.get("GS")
ms_ask = state.ask_prices.get("MS")
wfc_ask = state.ask_prices.get("WFC")
xlf_bid = state.bid_prices.get("XLF")
bond_bid = state.bid_prices.get("BOND")
gs_bid = state.bid_prices.get("GS")
ms_bid = state.bid_prices.get("MS")
wfc_bid = state.bid_prices.get("WFC")
xlf_ask = state.ask_prices.get("XLF")
if None in [bond_ask, gs_ask, ms_ask, wfc_ask, xlf_bid,
bond_bid, gs_bid, ms_bid, wfc_bid, xlf_ask]:
return
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 = positions.get("XLF", 0)
if abs(pos) > 10:
return
profit1 = xlf_bid * 10 - basket_ask
if profit1 > ARB_THRESHOLD:
exchange.send_add_message_ioc(next_id(), "XLF", Dir.SELL, xlf_bid, 10)
profit2 = basket_bid - xlf_ask * 10
if profit2 > ARB_THRESHOLD:
exchange.send_add_message_ioc(next_id(), "XLF", Dir.BUY, xlf_ask, 10)
# ==================== 리스크 관리 ====================
def risk():
for sym in ["GS", "MS", "WFC", "XLF"]:
pos = positions.get(sym, 0)
if abs(pos) > MAX_POS:
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)
)
# ==================== LOOP ====================
while True:
msg = exchange.read_message()
if msg["type"] == "close":
break
elif msg["type"] == "book":
sym = msg["symbol"]
bid = msg["buy"][0][0] if msg["buy"] else None
ask = msg["sell"][0][0] if msg["sell"] else None
state.update_bid_ask_price(sym, bid, ask)
for oid in list(active_orders.keys()):
exchange.send_cancel_message(oid)
del active_orders[oid]
# 전략 실행
bond_arb()
xlf_arb()
if sym in ["GS", "MS", "WFC"]:
market_make(sym)
risk()
elif msg["type"] == "fill":
qty = msg["size"]
sym = msg["symbol"]
if msg["dir"] == Dir.BUY:
positions[sym] = positions.get(sym, 0) + qty
else:
positions[sym] = positions.get(sym, 0) - qty
# ====================
class ExchangeConnection:
def __init__(self, args):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((args.exchange_hostname, args.port))
self.reader = s.makefile("r", 1)
self.writer = s
self._write({"type": "hello", "team": team_name.upper()})
def read_message(self):
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 send_cancel_message(self, oid):
self._write({"type": "cancel", "order_id": oid})
def _write(self, msg):
self.writer.send((json.dumps(msg)+"\n").encode())
# ====================
def parse_arguments():
parser = argparse.ArgumentParser()
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--production", action="store_true")
group.add_argument("--test", type=str, default="prod-like")
args = parser.parse_args()
if args.production:
args.exchange_hostname = "production"
args.port = 25000
else:
args.exchange_hostname = "test-exch-" + team_name
args.port = 22000
return args
if __name__ == "__main__":
main()

663
prac.py Normal file
View File

@@ -0,0 +1,663 @@
#!/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
from state import StateManager
from order import OrderManager
# ~~~~~============== 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 (고정)
BOND_ORDER_SIZE = 60 # BOND 주문당 수량
XLF_CONVERSION_FEE = 100 # XLF 변환 비용
XLF_MIN_PROFIT = 5 # XLF 차익거래 최소 수익
XLF_COOLDOWN = 2.0 # XLF 실패 후 재시도 대기 시간 (초)
VALE_CONVERSION_FEE = 10 # VALE 변환 비용
VALE_MIN_PROFIT = 1 # VALE 차익거래 최소 수익
VALE_ARB_SIZE = 10 # VALE 차익거래 단위
REFRESH_INTERVAL = 5.0 # 주문 갱신 주기 (초)
# XLF state machine
# IDLE → BUYING_BASKET → CONVERTING → SELLING_XLF → IDLE
# IDLE → BUYING_XLF → CONVERTING → SELLING_BASKET → IDLE
xlf_state = "IDLE"
xlf_pending = {}
xlf_direction = None
xlf_arb_size = 0
xlf_convert_oid = None
xlf_last_fail = 0.0 # 마지막 XLF 실패 시간
# VALE state machine (XLF와 독립적으로 동시 실행 가능)
# IDLE → BUYING_VALBZ → CONVERTING → SELLING_VALE → IDLE
# IDLE → BUYING_VALE → CONVERTING → SELLING_VALBZ → IDLE
vale_state = "IDLE"
vale_pending = {}
vale_direction = None
vale_arb_size = 0
vale_convert_oid = None
state = StateManager()
om = OrderManager(exchange)
market_open = False
active_orders = {} # BOND 전용 {order_id: {"dir": ..., "price": ..., "size": ...}}
last_refresh = time.time()
# hello 메시지에서 기존 포지션 로드 (재접속 시 필수)
for sym_info in hello_message["symbols"]:
sym = sym_info["symbol"]
pos = sym_info["position"]
if sym in om.positions:
om.positions[sym] = pos
om.future_positions[sym] = pos
print(f" 초기 포지션 로드: {om.positions}")
def next_id():
return om.next_order()
def cancel_bond_sell_orders():
"""BOND 매도 주문만 취소 (basket→XLF 시 매수 주문은 유지)"""
for oid in list(active_orders.keys()):
order_info = active_orders[oid]
if order_info["dir"] == Dir.SELL:
size = order_info.get("size", 0)
om.future_positions["BOND"] += size
om.cancel(oid)
active_orders.pop(oid, None)
def cancel_all_bond_orders():
"""활성 BOND 주문 전부 취소 + future_positions 롤백"""
for oid in list(active_orders.keys()):
order_info = active_orders[oid]
size = order_info.get("size", 0)
if order_info["dir"] == Dir.BUY:
om.future_positions["BOND"] -= size
else:
om.future_positions["BOND"] += size
om.cancel(oid)
active_orders.pop(oid, None)
def place_bond_orders():
"""포지션 한도 안에서 bid/ask 양방향 주문"""
if not market_open:
return
cancel_all_bond_orders()
buy_price = BOND_FAIR_VALUE - 1 # 999
sell_price = BOND_FAIR_VALUE + 1 # 1001
position = om.positions["BOND"]
# 포지션에 따라 size 비대칭 조정
base_size = BOND_ORDER_SIZE
adjustment = abs(position) // 5
if position < 0:
buy_size = min(base_size + adjustment, 100 - position)
sell_size = max(base_size - adjustment, 1)
elif position > 0:
buy_size = max(base_size - adjustment, 1)
sell_size = min(base_size + adjustment, 100 + position)
else:
buy_size = base_size
sell_size = base_size
# 포지션 한도 초과 방지
buy_size = max(0, min(buy_size, 100 - position))
sell_size = max(0, min(sell_size, 100 + position))
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
)
om.future_positions["BOND"] += buy_size
active_orders[bid] = {"dir": Dir.BUY, "price": buy_price, "size": buy_size}
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
)
om.future_positions["BOND"] -= sell_size
active_orders[ask] = {"dir": Dir.SELL, "price": sell_price, "size": sell_size}
print(f" BOND 주문 → 매수:{buy_price} x{buy_size}, 매도:{sell_price} x{sell_size}, 포지션:{position}")
def try_xlf_arb():
"""XLF 차익거래 시도 - IDLE + 쿨다운 통과 시에만"""
nonlocal xlf_state, xlf_direction, xlf_pending, xlf_arb_size, xlf_convert_oid
if not market_open or xlf_state != "IDLE":
return
# 실패 후 쿨다운 체크
if time.time() - xlf_last_fail < XLF_COOLDOWN:
return
bond_ask = state.ask_prices["BOND"]
gs_ask = state.ask_prices["GS"]
ms_ask = state.ask_prices["MS"]
wfc_ask = state.ask_prices["WFC"]
xlf_bid = state.bid_prices["XLF"]
bond_bid = state.bid_prices["BOND"]
gs_bid = state.bid_prices["GS"]
ms_bid = state.bid_prices["MS"]
wfc_bid = state.bid_prices["WFC"]
xlf_ask = state.ask_prices["XLF"]
if None in [bond_ask, gs_ask, ms_ask, wfc_ask, xlf_bid,
bond_bid, gs_bid, ms_bid, wfc_bid, xlf_ask]:
return
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
# 바스켓 심볼 포지션 한도 체크
def basket_has_room():
return (
om.positions["BOND"] + 3 <= 100 and
om.positions["GS"] + 2 <= 100 and
om.positions["MS"] + 3 <= 100 and
om.positions["WFC"] + 2 <= 100
)
# 케이스 1: 바스켓 매수 → XLF 변환 → XLF 매도
profit1 = xlf_bid * 10 - basket_ask - XLF_CONVERSION_FEE
if profit1 > XLF_MIN_PROFIT and om.check_pos_limit("XLF") and basket_has_room():
print(f" XLF 차익(바스켓→XLF) 시작, 예상수익:{profit1}")
cancel_bond_sell_orders()
xlf_state = "BUYING_BASKET"
xlf_direction = "BASKET_TO_XLF"
xlf_arb_size = 10
xlf_pending.clear()
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(oid, sym, Dir.BUY, state.ask_prices[sym], qty)
xlf_pending[oid] = qty
return
# 케이스 2: XLF 매수 → 바스켓 변환 → 각 종목 매도
profit2 = basket_bid - xlf_ask * 10 - XLF_CONVERSION_FEE
if profit2 > XLF_MIN_PROFIT and om.check_pos_limit("XLF"):
print(f" XLF 차익(XLF→바스켓) 시작, 예상수익:{profit2}")
cancel_all_bond_orders()
xlf_state = "BUYING_XLF"
xlf_direction = "XLF_TO_BASKET"
xlf_arb_size = 10
xlf_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "XLF", Dir.BUY, xlf_ask, 10)
xlf_pending[oid] = 10
def handle_xlf_fill(order_id, symbol, dir_, qty):
"""XLF state machine 체결 처리 (부분 체결 추적)"""
nonlocal xlf_state, xlf_pending, xlf_convert_oid
if order_id not in xlf_pending:
return
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"
xlf_convert_oid = next_id()
exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.BUY, xlf_arb_size)
elif xlf_state == "BUYING_XLF" and not xlf_pending:
print(" XLF 매수 완료 → 바스켓 변환 시작")
xlf_state = "CONVERTING"
xlf_convert_oid = next_id()
exchange.send_convert_message(xlf_convert_oid, "XLF", Dir.SELL, xlf_arb_size)
def try_vale_arb():
"""VALE/VALBZ 차익거래 시도 - XLF와 독립적으로 동시 실행"""
nonlocal vale_state, vale_direction, vale_pending, vale_arb_size, vale_convert_oid
if not market_open or vale_state != "IDLE":
return
vale_bid = state.bid_prices["VALE"]
vale_ask = state.ask_prices["VALE"]
valbz_bid = state.bid_prices["VALBZ"]
valbz_ask = state.ask_prices["VALBZ"]
if None in [vale_bid, vale_ask, valbz_bid, valbz_ask]:
return
valbz_pos = om.positions["VALBZ"]
vale_pos = om.positions["VALE"]
# 케이스 1: VALE가 비쌀 때 → VALBZ 매수 → VALE 변환 → VALE 매도
profit1 = vale_bid - valbz_ask - VALE_CONVERSION_FEE
arb_size1 = min(VALE_ARB_SIZE, 10 - valbz_pos)
if profit1 > VALE_MIN_PROFIT and arb_size1 > 0:
print(f" VALE 차익(VALBZ→VALE) 시작, 예상수익:{profit1 * arb_size1}, size:{arb_size1}")
vale_state = "BUYING_VALBZ"
vale_direction = "VALBZ_TO_VALE"
vale_arb_size = arb_size1
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALBZ", Dir.BUY, valbz_ask, arb_size1)
vale_pending[oid] = arb_size1
return
# 케이스 2: VALBZ가 비쌀 때 → VALE 매수 → VALBZ 변환 → VALBZ 매도
profit2 = valbz_bid - vale_ask - VALE_CONVERSION_FEE
arb_size2 = min(VALE_ARB_SIZE, 10 - vale_pos)
if profit2 > VALE_MIN_PROFIT and arb_size2 > 0:
print(f" VALE 차익(VALE→VALBZ) 시작, 예상수익:{profit2 * arb_size2}, size:{arb_size2}")
vale_state = "BUYING_VALE"
vale_direction = "VALE_TO_VALBZ"
vale_arb_size = arb_size2
vale_pending.clear()
oid = next_id()
exchange.send_add_message(oid, "VALE", Dir.BUY, vale_ask, arb_size2)
vale_pending[oid] = arb_size2
def handle_vale_fill(order_id, symbol, dir_, qty):
"""VALE state machine 체결 처리 (부분 체결 추적)"""
nonlocal vale_state, vale_pending, vale_convert_oid
if order_id not in vale_pending:
return
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"
vale_convert_oid = next_id()
exchange.send_convert_message(vale_convert_oid, "VALE", Dir.BUY, vale_arb_size)
elif vale_state == "BUYING_VALE" and not vale_pending:
print(" VALE 매수 완료 → VALBZ 변환 시작")
vale_state = "CONVERTING"
vale_convert_oid = next_id()
exchange.send_convert_message(vale_convert_oid, "VALE", Dir.SELL, vale_arb_size)
# 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_last_print_time = 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)
oid = message.get("order_id")
active_orders.pop(oid, None)
if oid in xlf_pending:
print(" XLF 주문 reject → IDLE 복귀")
xlf_state = "IDLE"
xlf_pending.clear()
xlf_direction = None
xlf_last_fail = time.time() # 쿨다운 시작
place_bond_orders() # try_xlf_arb() 호출 제거
if oid in vale_pending:
print(" VALE 주문 reject → IDLE 복귀")
vale_state = "IDLE"
vale_pending.clear()
vale_direction = None
elif message["type"] == "ack":
ack_oid = message.get("order_id")
# XLF 변환 ack 처리 (order_id로 구분)
if xlf_state == "CONVERTING" and ack_oid == xlf_convert_oid:
print(" XLF 변환 완료 → 매도 시작")
if xlf_direction == "BASKET_TO_XLF":
# 변환: BOND -3, GS -2, MS -3, WFC -2, XLF +10
om.positions["BOND"] -= 3
om.positions["GS"] -= 2
om.positions["MS"] -= 3
om.positions["WFC"] -= 2
om.positions["XLF"] += xlf_arb_size
om.future_positions["BOND"] -= 3
om.future_positions["GS"] -= 2
om.future_positions["MS"] -= 3
om.future_positions["WFC"] -= 2
om.future_positions["XLF"] += xlf_arb_size
xlf_state = "SELLING_XLF"
oid = next_id()
exchange.send_add_message(
oid, "XLF", Dir.SELL, state.bid_prices["XLF"], xlf_arb_size
)
xlf_pending[oid] = xlf_arb_size
elif xlf_direction == "XLF_TO_BASKET":
# 변환: XLF -10, BOND +3, GS +2, MS +3, WFC +2
om.positions["XLF"] -= xlf_arb_size
om.positions["BOND"] += 3
om.positions["GS"] += 2
om.positions["MS"] += 3
om.positions["WFC"] += 2
om.future_positions["XLF"] -= xlf_arb_size
om.future_positions["BOND"] += 3
om.future_positions["GS"] += 2
om.future_positions["MS"] += 3
om.future_positions["WFC"] += 2
xlf_state = "SELLING_BASKET"
for sym, qty in [("BOND", 3), ("GS", 2), ("MS", 3), ("WFC", 2)]:
oid = next_id()
exchange.send_add_message(
oid, sym, Dir.SELL, state.bid_prices[sym], qty
)
xlf_pending[oid] = qty
# VALE 변환 ack 처리 (order_id로 구분)
elif vale_state == "CONVERTING" and ack_oid == vale_convert_oid:
print(" VALE 변환 완료 → 매도 시작")
if vale_direction == "VALBZ_TO_VALE":
# 변환: VALBZ -size, VALE +size
om.positions["VALBZ"] -= vale_arb_size
om.positions["VALE"] += vale_arb_size
om.future_positions["VALBZ"] -= vale_arb_size
om.future_positions["VALE"] += vale_arb_size
vale_state = "SELLING_VALE"
oid = next_id()
exchange.send_add_message(
oid, "VALE", Dir.SELL, state.bid_prices["VALE"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif vale_direction == "VALE_TO_VALBZ":
# 변환: VALE -size, VALBZ +size
om.positions["VALE"] -= vale_arb_size
om.positions["VALBZ"] += vale_arb_size
om.future_positions["VALE"] -= vale_arb_size
om.future_positions["VALBZ"] += vale_arb_size
vale_state = "SELLING_VALBZ"
oid = next_id()
exchange.send_add_message(
oid, "VALBZ", Dir.SELL, state.bid_prices["VALBZ"], vale_arb_size
)
vale_pending[oid] = vale_arb_size
elif message["type"] == "fill":
print(message)
qty = message["size"]
sym = message["symbol"]
dir_ = message["dir"]
oid = message["order_id"]
# 포지션 업데이트
if dir_ == Dir.BUY:
om.update_position(sym, oid, qty)
else:
om.update_position(sym, oid, -qty)
print(f" 포지션 → {om.positions}")
# BOND 체결 시 무조건 재주문
if sym == "BOND" and oid in active_orders:
active_orders.pop(oid, None)
place_bond_orders()
# XLF state machine 체결 처리
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
place_bond_orders()
# try_xlf_arb() 제거 → book 메시지에서 자동 시도
# VALE state machine 체결 처리
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
# try_vale_arb() 제거 → book 메시지에서 자동 시도
elif message["type"] == "book":
sym = message["symbol"]
state.update_bid_ask_price(
sym,
message["buy"][0][0] if message["buy"] else None,
message["sell"][0][0] if message["sell"] else None
)
if sym == "VALE":
now = time.time()
if now > vale_last_print_time + 1:
vale_last_print_time = now
print({
"vale_bid_price": state.bid_prices["VALE"],
"vale_ask_price": state.ask_prices["VALE"],
})
# XLF 관련 심볼 호가 업데이트마다 차익거래 시도
if sym in ["BOND", "GS", "MS", "WFC", "XLF"]:
try_xlf_arb()
# VALE/VALBZ 호가 업데이트마다 차익거래 시도
if sym in ["VALE", "VALBZ"]:
try_vale_arb()
# 주기적으로 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 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()})
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
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):
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 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
)
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)
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()

99
state.py Normal file
View File

@@ -0,0 +1,99 @@
CONVERSION_FEE = 5
class StateManager:
def __init__(self):
symbols = ["BOND", "VALBZ", "VALE", "GS", "MS", "WFC", "XLF"]
self.bid_prices = {s: None for s in symbols}
self.ask_prices = {s: None for s in symbols}
self.bid_depths = {s: 0 for s in symbols}
self.ask_depths = {s: 0 for s in symbols}
self.last_prices = {s: None for s in symbols}
self.fair_values = {
"BOND": 1000,
"VALBZ": None,
"VALE": None,
"GS": None,
"MS": None,
"WFC": None,
"XLF": None,
}
self.predicted_prices = {s: None for s in symbols}
self.etf_components = {
"XLF": {"BOND": 3, "GS": 2, "MS": 3, "WFC": 2}
}
self.etf_shares = {"XLF": 10}
def set_last_price(self, symbol: str, price: int):
self.last_prices[symbol] = price
def get_last_price(self, symbol: str):
return self.last_prices.get(symbol)
def update_position(self, symbol: str, quantity: int):
self.positions[symbol] = self.positions.get(symbol, 0) + quantity
def update_bid_ask_price(self, symbol: str, bid_price: int, ask_price: int):
if bid_price is not None:
self.bid_prices[symbol] = bid_price
if ask_price is not None:
self.ask_prices[symbol] = ask_price
def get_best_bid_ask(self, symbol: str):
return self.bid_prices.get(symbol), self.ask_prices.get(symbol)
def get_position(self, symbol: str) -> int:
return self.positions.get(symbol, 0)
def get_spread(self, symbol: str) -> int:
bid = self.bid_prices.get(symbol)
ask = self.ask_prices.get(symbol)
if bid is not None and ask is not None:
return ask - bid
return None
def get_mid_price(self, symbol: str):
bid = self.bid_prices[symbol]
ask = self.ask_prices[symbol]
if bid is not None and ask is not None:
return (bid + ask) // 2
return self.last_prices.get(symbol)
def update_depth(self, symbol: str, bid_depth: int, ask_depth: int):
self.bid_depths[symbol] = bid_depth
self.ask_depths[symbol] = ask_depth
def get_imbalance(self, symbol: str) -> int:
bid_depth = self.bid_depths[symbol]
ask_depth = self.ask_depths[symbol]
return bid_depth - ask_depth
def get_predicted_price(self, symbol: str) -> int:
return self.predicted_prices.get(symbol)
def update_predicted_price(self, symbol: str):
mid = self.get_mid_price(symbol)
if mid is not None:
self.predicted_prices[symbol] = mid
def update_fair_value(self):
valbz_price = self.last_prices.get("VALBZ")
if valbz_price is not None:
self.fair_values["VALBZ"] = valbz_price
self.fair_values["VALE"] = valbz_price
bond_price = self.get_predicted_price("BOND")
gs_price = self.get_predicted_price("GS")
ms_price = self.get_predicted_price("MS")
wfc_price = self.get_predicted_price("WFC")
if all(p is not None for p in [bond_price, gs_price, ms_price, wfc_price]):
component_value = (
bond_price * self.etf_components["XLF"]["BOND"]
+ gs_price * self.etf_components["XLF"]["GS"]
+ ms_price * self.etf_components["XLF"]["MS"]
+ wfc_price * self.etf_components["XLF"]["WFC"]
) / self.etf_shares["XLF"]
self.fair_values["XLF"] = component_value
def get_fair_value(self, symbol: str) -> int:
return self.fair_values.get(symbol)