This commit is contained in:
2026-05-09 16:47:15 +09:00
parent b2aa766f25
commit 2160cc0e2a

191
bot_x.py
View File

@@ -57,6 +57,39 @@ class TechnicalAnalyzer:
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,
@@ -119,7 +152,56 @@ class CrossEMAStrategy:
return False
team_name = "HanyangFloorFunction"
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
def main():
@@ -141,6 +223,11 @@ def main():
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
@@ -164,11 +251,22 @@ def main():
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()
@@ -184,6 +282,12 @@ def main():
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
@@ -405,6 +509,82 @@ def main():
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
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:
@@ -427,6 +607,7 @@ def main():
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"
@@ -510,6 +691,11 @@ def main():
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 복귀")
@@ -549,6 +735,9 @@ def main():
if sym in symbols_for_ema:
try_ema_trade()
if sym in symbols_for_mr:
try_mean_reversion_trade()
now = time.time()
if now - last_refresh > REFRESH_INTERVAL:
last_refresh = now