From 2160cc0e2a5bbc508f4dc9497840d84aae6f16bb Mon Sep 17 00:00:00 2001 From: yenru0 Date: Sat, 9 May 2026 16:47:15 +0900 Subject: [PATCH] fix --- bot_x.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 190 insertions(+), 1 deletion(-) diff --git a/bot_x.py b/bot_x.py index 0e175c4..9901684 100644 --- a/bot_x.py +++ b/bot_x.py @@ -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