diff --git a/live/node.py b/live/node.py index 3ec41e7..e7dfbb3 100644 --- a/live/node.py +++ b/live/node.py @@ -47,6 +47,8 @@ seen_fills: set[int] = set() btc_prices: deque = deque(maxlen=60) eth_prices: deque = deque(maxlen=60) active_cloids: dict = {} # Track active order IDs per strategy +active_cloids_times: dict = {} # Tick when order was placed +active_cloids_px: dict = {} # Entry price for take-profit # ═══════════════════════ Helpers ═══════════════════════ @@ -115,20 +117,27 @@ def compute_signals(): # OFI: 5-tick reversal if len(btc_prices)>=5: ret = (btc-btc_prices[-5])/btc_prices[-5] - if ret>0.0008: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) - elif ret<-0.0008: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) + if ret>0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"SELL","strength":ret}) + elif ret<-0.0004: STRATEGIES["Order Book Imbalance"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(ret)}) # Iceberg: trend count if len(btc_prices)>=10: up = sum(1 for i in range(-9,0) if btc_prices[i+1]>btc_prices[i]) - if up>=7: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) - elif up<=3: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) + if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) + elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) - # Funding Arb: rate proxy + # Funding Arb: use real funding rate if available, else wider proxy if len(btc_prices)>=20: - fr = (btc/btc_prices[-20]-1)/20 - if abs(fr)>0.0008: - STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if fr>0 else "BUY","strength":abs(fr)}) + try: + fr = requests.post(TESTNET_API, json={"type":"funding","coin":"BTC"}, timeout=5).json() + if isinstance(fr, list) and fr: + rate = float(fr[0].get("funding_rate", 0)) + else: + rate = (btc/btc_prices[-20]-1)/20 + except: + rate = (btc/btc_prices[-20]-1)/20 + if abs(rate)>0.0001: + STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) # Pairs: ratio Z-score if len(btc_prices)>=20 and len(eth_prices)>=20: @@ -146,8 +155,8 @@ def compute_signals(): w = list(btc_prices)[-20:]; sma = sum(w)/len(w) variance = sum((p-sma)**2 for p in w)/len(w); std = math.sqrt(variance) if std>0: - if btc > sma+2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) - elif btc < sma-2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) + if btc > sma+1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(btc-sma-2*std)/std}) + elif btc < sma-1.5*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-2*std-btc)/std}) # Mean Reversion: VWAP if len(btc_prices)>=20: @@ -155,8 +164,8 @@ def compute_signals(): vwap = sum(p*v for p,v in zip(w,vols))/sum(vols) vstd = math.sqrt(sum((p-vwap)**2 for p in w)/len(w)) dev = (btc-vwap)/vstd if vstd>0 else 0 - if dev>1.5: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) - elif dev<-1.5: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) + if dev>1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev}) + elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)}) # Trim signals for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:] @@ -281,78 +290,110 @@ async def main(): # Signals every 5 ticks if tick%5==0: compute_signals() - # Place/refresh orders every 3-5 ticks - if tick>=3 and tick%random.randint(3,5)==0: + # Execute ALL strategies every 4 seconds + if tick>=3 and tick%4==0: btc_bid, btc_ask, btc_mid = get_orderbook("BTC") - try: - btc_bid, btc_ask, btc_mid = get_orderbook("BTC") - except Exception as e: - log.debug(f"OB BTC error: {e}") - btc_bid = btc_ask = btc_mid = 0 try: eth_bid, eth_ask, eth_mid = get_orderbook("ETH") except Exception as e: eth_bid = eth_ask = eth_mid = 0 + if btc_bid<=0 or btc_ask<=0: continue - name = names[idx%7]; idx+=1; cfg=STRATEGIES[name] - coin="BTC" if "BTC" in cfg["instrument"] else "ETH" - perp=btc_perp if coin=="BTC" else eth_perp - bid=btc_bid if coin=="BTC" else eth_bid - ask=btc_ask if coin=="BTC" else eth_ask - mid=btc_mid if coin=="BTC" else eth_mid - if bid<=0 or ask<=0: continue + for name in names: + cfg=STRATEGIES[name] + coin="BTC" if "BTC" in cfg["instrument"] else "ETH" + perp=btc_perp if coin=="BTC" else eth_perp + bid=btc_bid if coin=="BTC" else eth_bid + ask=btc_ask if coin=="BTC" else eth_ask + mid=btc_mid if coin=="BTC" else eth_mid + if bid<=0 or ask<=0: continue - # Cancel previous order for this strategy - if name in active_cloids: - try: - client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) - except: pass + # Check if this strategy has a position; skip if already filled + has_position = name in active_cloids and tick - active_cloids_times.get(name,0) < 60 - # Determine side from signal or market-making pattern - signal=None - if cfg["signals"]: signal=cfg["signals"][-1]["signal"] if cfg["signals"] else None + # Determine signal + signal=None + if cfg["signals"]: + latest = cfg["signals"][-1] + # Only use recent signals (< 10 seconds old) + if time.time() - latest["time"] < 10: + signal=latest["signal"] - if name=="Avellaneda-Stoikov": - # DUAL-SIDED: place both bid and ask simultaneously - cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) - try: - client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) - client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) - log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,} | spread=${ask-bid:.1f}") - active_cloids[name]=str(cid_bid) # track one - except Exception as e: log.warning(f"Avel dual error: {str(e)[:60]}") - continue + # Close on opposing signal + if has_position and signal: + prev_signal = active_cloids.get(name,"") + if ("BUY" in str(signal).upper() and "SELL" in str(prev_signal).upper()) or ("SELL" in str(signal).upper() and "BUY" in str(prev_signal).upper()): + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False - # Single-sided for other strategies - side=None; px_level=0 - if signal and "SELL" in str(signal).upper(): - side=OrderSide.SELL; px_level=ask # at best ask (highest fill probability as maker) - elif signal and "BUY" in str(signal).upper(): - side=OrderSide.BUY; px_level=bid # at best bid - else: - # No signal: market-making default — alternate sides at best bid/ask - side=OrderSide.BUY if tick%2==0 else OrderSide.SELL - px_level=bid if side==OrderSide.BUY else ask + # Take-profit: close if price moved 2x fee in our favor + if has_position: + entry_px = active_cloids_px.get(name, 0) + if entry_px > 0: + if "BUY" in str(active_cloids[name]).upper() and mid > entry_px * 1.001: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False + elif "SELL" in str(active_cloids[name]).upper() and mid < entry_px * 0.999: + try: + client.cancel_order(instrument_id=perp.id, client_order_id=ClientOrderId(active_cloids[name])) + except: pass + del active_cloids[name] + has_position = False - if not side or px_level<=0: continue + if has_position: continue # Don't replace existing orders - cid=ClientOrderId(str(UUID4())) - try: - client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) - side_str="BUY " if side==OrderSide.BUY else "SELL" - log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} {coin} MAKER @ ${int(px_level):,} (best {'bid' if side==OrderSide.BUY else 'ask'}: ${int(px_level):,})") - active_cloids[name]=str(cid) - except Exception as e: - err=str(e) - if "would have immediately matched" in err or "cross" in err.lower(): - # Post-only would cross — fall back to regular limit at same level - cid2=ClientOrderId(str(UUID4())) + # Avellaneda-Stoikov: DUAL-SIDED (always active) + if name=="Avellaneda-Stoikov": + cid_bid=ClientOrderId(str(UUID4())); cid_ask=ClientOrderId(str(UUID4())) try: - client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) - log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} {coin} FILLED @ ${int(px_level):,} (post-only crossed → IOC)") - active_cloids[name]=str(cid2) - except Exception as e2: log.debug(f"[{name[:8]}] fallback failed: {str(e2)[:50]}") - else: log.warning(f"Order [{name[:8]}]: {err[:60]}") + client.submit_order(instrument_id=perp.id,client_order_id=cid_bid,order_side=OrderSide.BUY,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(bid))),time_in_force=TimeInForce.GTC,post_only=True) + client.submit_order(instrument_id=perp.id,client_order_id=cid_ask,order_side=OrderSide.SELL,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(ask))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + log.info(f"[Avel] DUAL: BID {cfg['size']} @ ${int(bid):,} | ASK {cfg['size']} @ ${int(ask):,}") + active_cloids[name]=str(cid_bid) + active_cloids_times[name]=tick + active_cloids_px[name]=bid + except Exception as e: pass + continue + + # For signal-driven strategies: use aggressive offset + if signal: + side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY + # Aggressive: 0.03% inside the spread for higher fill probability + offset = int(mid * 0.0003) + px_level = ask - offset if side==OrderSide.SELL else bid + offset + px_level = max(px_level, 1) + else: + # No signal/default: skip (don't random-trade) + continue + + if px_level<=0: continue + + cid=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.GTC,post_only=True) + if tick%60==0: + side_str="BUY" if side==OrderSide.BUY else "SELL" + log.info(f"[{name[:4]:4s}] {side_str} {cfg['size']} @ ${int(px_level):,} ({'best bid '+str(int(bid)) if side==OrderSide.BUY else 'best ask '+str(int(ask))})") + active_cloids[name]=str(cid) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except Exception as e: + err=str(e) + if "would have immediately matched" in err or "cross" in err.lower(): + cid2=ClientOrderId(str(UUID4())) + try: + client.submit_order(instrument_id=perp.id,client_order_id=cid2,order_side=side,order_type=OrderType.LIMIT,quantity=Quantity.from_str(str(cfg["size"])),price=Price.from_str(str(int(px_level))),time_in_force=TimeInForce.IOC) + active_cloids[name]=str(cid2) + active_cloids_times[name]=tick + active_cloids_px[name]=px_level + except: pass # Equity tp=sum(s["pnl"] for s in STRATEGIES.values())