Proper Avellaneda-Stoikov: reservation price + optimal spread model
This commit is contained in:
+244
-149
@@ -38,7 +38,8 @@ STRATEGIES = {
|
|||||||
"Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000230,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."},
|
"Avellaneda-Stoikov": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000230,"fee_paid":0.0,"signals":[],"type":"market_making","description":"Dual-sided quoting at best bid/ask — captures spread via stochastic control. Places both sides simultaneously."},
|
||||||
"Momentum Breakout": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (1.2σ) breakout on ETH — enters when price breaks bands."},
|
"Momentum Breakout": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Bollinger Band (1.2σ) breakout on ETH — enters when price breaks bands."},
|
||||||
"Mean Reversion": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation on ETH — buys below VWAP, sells above. Higher vol = more reversion."},
|
"Mean Reversion": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.0005,"fee_paid":0.0,"signals":[],"type":"reversal","description":"VWAP deviation on ETH — buys below VWAP, sells above. Higher vol = more reversion."},
|
||||||
"Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.005,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."}
|
"Kalman Pairs": {"allocation":100.0,"instrument":"ETH-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.005,"fee_paid":0.0,"signals":[],"type":"stat_arb","description":"Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta with every tick."},
|
||||||
|
"Hurst VPIN": {"allocation":100.0,"instrument":"BTC-USD-PERP","pnl":0.0,"pnl_pct":0.0,"position":0.0,"trades_today":0,"wins":0,"win_rate":0.0,"status":"idle","size":0.000240,"fee_paid":0.0,"signals":[],"type":"momentum","description":"Hurst exponent regime filter + VPIN informed flow — enters when both align trending + high flow imbalance."}
|
||||||
}
|
}
|
||||||
|
|
||||||
trades_log: list[dict] = []
|
trades_log: list[dict] = []
|
||||||
@@ -182,15 +183,33 @@ def compute_signals():
|
|||||||
if eth_cur > sma+1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(eth_cur-sma-1.2*std)/std})
|
if eth_cur > sma+1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(eth_cur-sma-1.2*std)/std})
|
||||||
elif eth_cur < sma-1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-1.2*std-eth_cur)/std})
|
elif eth_cur < sma-1.2*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-1.2*std-eth_cur)/std})
|
||||||
|
|
||||||
# Mean Reversion: VWAP on ETH
|
# Mean Reversion: VWAP on ETH (exclude current price from VWAP)
|
||||||
if len(eth_prices)>=20:
|
if len(eth_prices)>=20:
|
||||||
w = list(eth_prices)[-20:]; eth_mr = eth_prices[-1]; vols = [1+i/len(w) for i in range(len(w))]
|
w = list(eth_prices)[-20:]; eth_mr = eth_prices[-1]
|
||||||
vwap = sum(p*v for p,v in zip(w,vols))/sum(vols)
|
# VWAP on prior 19 prices, equal volume weights
|
||||||
vstd = math.sqrt(sum((p-vwap)**2 for p in w)/len(w))
|
prior = w[:-1]
|
||||||
dev = (eth_mr-vwap)/vstd if vstd>0 else 0
|
sma = sum(prior)/len(prior)
|
||||||
|
vstd = math.sqrt(sum((p-sma)**2 for p in prior)/len(prior))
|
||||||
|
dev = (eth_mr-sma)/vstd if vstd>0 else 0
|
||||||
if dev>1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":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)})
|
elif dev<-1.0: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"BUY","strength":abs(dev)})
|
||||||
|
|
||||||
|
# Hurst/VPIN: feed BTC price into dollar bars
|
||||||
|
if len(btc_prices)>=3:
|
||||||
|
try:
|
||||||
|
from strategies.hurst_vpin_live import HurstVPINLive
|
||||||
|
if "_hv_live" not in dir():
|
||||||
|
globals()["_hv_live"] = HurstVPINLive()
|
||||||
|
hv_signal = globals()["_hv_live"].feed_price(btc)
|
||||||
|
if hv_signal:
|
||||||
|
STRATEGIES["Hurst VPIN"]["signals"].append({
|
||||||
|
"time":time.time(),
|
||||||
|
"signal": hv_signal["signal"],
|
||||||
|
"strength": hv_signal["hurst"],
|
||||||
|
"reason": f"H={hv_signal['hurst']:.2f}_V={hv_signal['vpin']:.2f}"
|
||||||
|
})
|
||||||
|
except: pass
|
||||||
|
|
||||||
# Trim signals
|
# Trim signals
|
||||||
for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:]
|
for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:]
|
||||||
|
|
||||||
@@ -260,7 +279,7 @@ async def main():
|
|||||||
log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})")
|
log.info(f" BTC: bid=${btc_bid:,.0f} ask=${btc_ask:,.0f} (spread=${btc_ask-btc_bid:.1f})")
|
||||||
log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})")
|
log.info(f" ETH: bid=${eth_bid:,.0f} ask=${eth_ask:,.0f} (spread=${eth_ask-eth_bid:.1f})")
|
||||||
log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%")
|
log.info(f" Mode: POST-ONLY at best bid/ask | Maker: 0.02%")
|
||||||
log.info(f" 7 strategies | A-S is DUAL-SIDED quoting")
|
log.info(f" {len(STRATEGIES)} strategies | A-S is DUAL-SIDED quoting")
|
||||||
log.info(f" Dashboard: https://ftdt.io/cv")
|
log.info(f" Dashboard: https://ftdt.io/cv")
|
||||||
log.info("="*60)
|
log.info("="*60)
|
||||||
|
|
||||||
@@ -273,7 +292,7 @@ async def main():
|
|||||||
except: pass
|
except: pass
|
||||||
log.info(f"Cleared {len(open_ords)} stale orders")
|
log.info(f"Cleared {len(open_ords)} stale orders")
|
||||||
|
|
||||||
existing = get_fills(addr)
|
existing = get_fills(addr) or []
|
||||||
for f in existing: seen_fills.add(f.get("tid",0))
|
for f in existing: seen_fills.add(f.get("tid",0))
|
||||||
log.info(f"Tracking {len(seen_fills)} existing fills")
|
log.info(f"Tracking {len(seen_fills)} existing fills")
|
||||||
|
|
||||||
@@ -285,160 +304,236 @@ async def main():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
tick+=1
|
try:
|
||||||
|
tick += 1
|
||||||
|
|
||||||
prices = get_mark_prices()
|
prices = get_mark_prices()
|
||||||
btc = prices.get("BTC",0); eth = prices.get("ETH",0)
|
btc = prices.get("BTC", 0)
|
||||||
if btc>0: btc_prices.append(btc)
|
eth = prices.get("ETH", 0)
|
||||||
if eth>0: eth_prices.append(eth)
|
if btc > 0:
|
||||||
|
btc_prices.append(btc)
|
||||||
|
if eth > 0:
|
||||||
|
eth_prices.append(eth)
|
||||||
|
|
||||||
# Process fills
|
# Process fills
|
||||||
fills = get_fills(addr); new_fills=0
|
fills = get_fills(addr)
|
||||||
for f in fills:
|
new_fills = 0
|
||||||
tid=f.get("tid",0)
|
for f in fills:
|
||||||
if tid in seen_fills: continue
|
tid = f.get("tid", 0)
|
||||||
seen_fills.add(tid)
|
if tid in seen_fills:
|
||||||
side=f.get("side",""); sz=float(f.get("sz",0)); px=float(f.get("px",0))
|
continue
|
||||||
closed_pnl=float(f.get("closedPnl",0)); fee=float(f.get("fee","0"))
|
seen_fills.add(tid)
|
||||||
|
side = f.get("side", "")
|
||||||
|
sz = float(f.get("sz", 0))
|
||||||
|
px = float(f.get("px", 0))
|
||||||
|
closed_pnl = float(f.get("closedPnl", 0))
|
||||||
|
fee = float(f.get("fee", "0"))
|
||||||
|
|
||||||
# Attribute fill by size (now unique per strategy)
|
# Attribute fill by size (now unique per strategy)
|
||||||
strat=None
|
strat = None
|
||||||
for n,cfg in STRATEGIES.items():
|
for n, cfg in STRATEGIES.items():
|
||||||
if abs(sz-cfg["size"])<0.000001:
|
if abs(sz - cfg["size"]) < 0.000001:
|
||||||
strat=n
|
strat = n
|
||||||
break
|
break
|
||||||
if not strat: continue
|
if not strat:
|
||||||
|
|
||||||
net=closed_pnl-abs(fee)
|
|
||||||
STRATEGIES[strat]["pnl"]+=net; STRATEGIES[strat]["trades_today"]+=1
|
|
||||||
STRATEGIES[strat]["fee_paid"]+=abs(fee)
|
|
||||||
if closed_pnl>0: STRATEGIES[strat]["wins"]+=1
|
|
||||||
STRATEGIES[strat]["pnl_pct"]=STRATEGIES[strat]["pnl"]/STRATEGIES[strat]["allocation"]*100
|
|
||||||
strategy_equity[strat].append({"t":time.time(),"v":STRATEGIES[strat]["allocation"]+STRATEGIES[strat]["pnl"]})
|
|
||||||
trades_log.append({"time":datetime.now().strftime("%H:%M:%S"),"strategy":strat,"side":"BUY" if side=="B" else "SELL","size":sz,"price":px,"pnl":round(net,4),"fee":round(abs(fee),4)})
|
|
||||||
new_fills+=1
|
|
||||||
|
|
||||||
# Signals every 5 ticks
|
|
||||||
if tick%5==0: compute_signals()
|
|
||||||
|
|
||||||
# Execute ALL strategies every 4 seconds
|
|
||||||
if tick>=3 and tick%4==0:
|
|
||||||
btc_bid, btc_ask, btc_mid = get_orderbook("BTC")
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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 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"]
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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 has_position: continue # Don't replace existing orders
|
|
||||||
|
|
||||||
# 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=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
|
continue
|
||||||
|
|
||||||
# For signal-driven strategies: use aggressive offset
|
net = closed_pnl - abs(fee)
|
||||||
if signal:
|
STRATEGIES[strat]["pnl"] += net
|
||||||
side=OrderSide.SELL if "SELL" in str(signal).upper() else OrderSide.BUY
|
STRATEGIES[strat]["trades_today"] += 1
|
||||||
# Aggressive: 0.03% inside the spread for higher fill probability
|
STRATEGIES[strat]["fee_paid"] += abs(fee)
|
||||||
offset = int(mid * 0.0003)
|
if closed_pnl > 0:
|
||||||
px_level = ask - offset if side==OrderSide.SELL else bid + offset
|
STRATEGIES[strat]["wins"] += 1
|
||||||
px_level = max(px_level, 1)
|
# Track position for AS model
|
||||||
|
if side == "B":
|
||||||
|
STRATEGIES[strat]["position"] = STRATEGIES[strat].get("position", 0.0) + sz
|
||||||
else:
|
else:
|
||||||
# No signal/default: skip (don't random-trade)
|
STRATEGIES[strat]["position"] = STRATEGIES[strat].get("position", 0.0) - sz
|
||||||
|
STRATEGIES[strat]["pnl_pct"] = STRATEGIES[strat]["pnl"] / STRATEGIES[strat]["allocation"] * 100
|
||||||
|
strategy_equity[strat].append({"t": time.time(), "v": STRATEGIES[strat]["allocation"] + STRATEGIES[strat]["pnl"]})
|
||||||
|
if len(strategy_equity[strat]) > 1000:
|
||||||
|
strategy_equity[strat][:] = strategy_equity[strat][-600:]
|
||||||
|
trades_log.append({"time": datetime.now().strftime("%H:%M:%S"), "strategy": strat, "side": "BUY" if side == "B" else "SELL", "size": sz, "price": px, "pnl": round(net, 4), "fee": round(abs(fee), 4)})
|
||||||
|
new_fills += 1
|
||||||
|
|
||||||
|
# Signals every 5 ticks
|
||||||
|
if tick % 5 == 0:
|
||||||
|
compute_signals()
|
||||||
|
|
||||||
|
# Execute ALL strategies every 4 seconds
|
||||||
|
if tick >= 3 and tick % 4 == 0:
|
||||||
|
btc_bid, btc_ask, btc_mid = get_orderbook("BTC")
|
||||||
|
try:
|
||||||
|
eth_bid, eth_ask, eth_mid = get_orderbook("ETH")
|
||||||
|
except Exception:
|
||||||
|
eth_bid = eth_ask = eth_mid = 0
|
||||||
|
if btc_bid <= 0 or btc_ask <= 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if px_level<=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
|
||||||
|
|
||||||
cid=ClientOrderId(str(UUID4()))
|
# Check if this strategy has a position; skip if already filled
|
||||||
try:
|
has_position = name in active_cloids and tick - active_cloids_times.get(name, 0) < 60
|
||||||
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:
|
# Determine signal
|
||||||
side_str="BUY" if side==OrderSide.BUY else "SELL"
|
signal = None
|
||||||
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))})")
|
if cfg["signals"]:
|
||||||
active_cloids[name]=str(cid)
|
latest = cfg["signals"][-1]
|
||||||
active_cloids_times[name]=tick
|
# Only use recent signals (< 10 seconds old)
|
||||||
active_cloids_px[name]=px_level
|
if time.time() - latest["time"] < 10:
|
||||||
except Exception as e:
|
signal = latest["signal"]
|
||||||
err=str(e)
|
|
||||||
if "would have immediately matched" in err or "cross" in err.lower():
|
# Close on opposing signal
|
||||||
cid2=ClientOrderId(str(UUID4()))
|
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 Exception:
|
||||||
|
pass
|
||||||
|
del active_cloids[name]
|
||||||
|
has_position = False
|
||||||
|
|
||||||
|
# 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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
pass
|
||||||
|
del active_cloids[name]
|
||||||
|
has_position = False
|
||||||
|
|
||||||
|
if has_position:
|
||||||
|
continue # Don't replace existing orders
|
||||||
|
|
||||||
|
# Avellaneda-Stoikov: proper optimal control (reservation price + spread)
|
||||||
|
if name == "Avellaneda-Stoikov":
|
||||||
try:
|
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)
|
from strategies.as_quoter import ASQuoter
|
||||||
active_cloids[name]=str(cid2)
|
if "_as_quoter" not in dir():
|
||||||
active_cloids_times[name]=tick
|
globals()["_as_quoter"] = ASQuoter(
|
||||||
active_cloids_px[name]=px_level
|
gamma=0.1, k=1.5, tau=1.0,
|
||||||
except: pass
|
min_spread=0.0001, max_inventory=cfg["size"] * 5,
|
||||||
|
)
|
||||||
|
q = ASQuoter
|
||||||
|
asq = globals()["_as_quoter"]
|
||||||
|
asq.observe(mid)
|
||||||
|
|
||||||
# Equity
|
# Get A-S inventory from position tracking
|
||||||
tp=sum(s["pnl"] for s in STRATEGIES.values())
|
as_inv = STRATEGIES[name].get("position", 0.0)
|
||||||
if tick%2==0: equity_history.append({"t":time.time(),"v":TOTAL_EQUITY+tp})
|
elapsed = (tick * 1.0) % (asq.tau * 3600) / 3600.0 # 1-hour virtual sessions
|
||||||
write_metrics(addr)
|
|
||||||
|
|
||||||
if tick%20==0:
|
result = asq.quotes(mid, as_inv, elapsed)
|
||||||
tp=sum(s["pnl"] for s in STRATEGIES.values())
|
if result is None:
|
||||||
tr=sum(s["trades_today"] for s in STRATEGIES.values())
|
continue # Circuit breaker active — skip this tick
|
||||||
tf=sum(s["fee_paid"] for s in STRATEGIES.values())
|
|
||||||
log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}")
|
|
||||||
|
|
||||||
await asyncio.sleep(1)
|
r_price = result["reservation"]
|
||||||
except KeyboardInterrupt: log.info("Stopping...")
|
as_bid = int(result["bid"])
|
||||||
|
as_ask = int(result["ask"])
|
||||||
|
# Clamp: never cross the market
|
||||||
|
as_bid = min(as_bid, int(bid))
|
||||||
|
as_ask = max(as_ask, int(ask))
|
||||||
|
|
||||||
|
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(as_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(as_ask)), time_in_force=TimeInForce.GTC, post_only=True)
|
||||||
|
if tick % 60 == 0:
|
||||||
|
log.info(f"[AS] r={r_price:.1f} σ={asq.sigma*100:.2f}% BID {cfg['size']} @ ${as_bid:,} | ASK {cfg['size']} @ ${as_ask:,} (spread ${as_ask - as_bid:,})")
|
||||||
|
active_cloids[name] = str(cid_bid)
|
||||||
|
active_cloids_times[name] = tick
|
||||||
|
active_cloids_px[name] = as_bid
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
# Fallback: best bid/ask if module unavailable
|
||||||
|
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)
|
||||||
|
active_cloids[name] = str(cid_bid)
|
||||||
|
active_cloids_times[name] = tick
|
||||||
|
active_cloids_px[name] = bid
|
||||||
|
except Exception:
|
||||||
|
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 Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Equity
|
||||||
|
tp = sum(s["pnl"] for s in STRATEGIES.values())
|
||||||
|
if tick % 2 == 0:
|
||||||
|
equity_history.append({"t": time.time(), "v": TOTAL_EQUITY + tp})
|
||||||
|
if len(equity_history) > 1000:
|
||||||
|
equity_history[:] = equity_history[-600:]
|
||||||
|
write_metrics(addr)
|
||||||
|
|
||||||
|
if tick % 20 == 0:
|
||||||
|
tp = sum(s["pnl"] for s in STRATEGIES.values())
|
||||||
|
tr = sum(s["trades_today"] for s in STRATEGIES.values())
|
||||||
|
tf = sum(s["fee_paid"] for s in STRATEGIES.values())
|
||||||
|
log.info(f"Tick {tick:4d} | PnL: ${tp:+.2f} | Trades: {tr:3d} | Fees: ${tf:.4f} | New fills: {new_fills}")
|
||||||
|
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except Exception as loop_err:
|
||||||
|
log.error(f"Loop error (tick {tick}): {loop_err}")
|
||||||
|
await asyncio.sleep(5) # back off and retry
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
log.info("Stopping...")
|
||||||
|
|
||||||
# Cancel all
|
# Cancel all
|
||||||
open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json()
|
open_ords = requests.post(TESTNET_API, json={"type":"openOrders","user":addr}, timeout=10).json()
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""
|
||||||
|
Proper Avellaneda-Stoikov market making for the live node.
|
||||||
|
|
||||||
|
Key formulas (Avellaneda & Stoikov, 2008):
|
||||||
|
Reservation price: r = s - q * gamma * sigma^2 * tau
|
||||||
|
Optimal spread: spread = gamma * sigma^2 * tau + (2/gamma) * ln(1 + gamma/k)
|
||||||
|
Bid = r - spread/2 Ask = r + spread/2
|
||||||
|
|
||||||
|
Where:
|
||||||
|
s = mid price, q = inventory, gamma = risk aversion
|
||||||
|
sigma = volatility, tau = remaining session time, k = order intensity
|
||||||
|
|
||||||
|
Production adaptations:
|
||||||
|
- Rolling volatility estimation (5-min window)
|
||||||
|
- Circuit breaker: pause quoting when price jump exceeds 3σ
|
||||||
|
- Inventory bounds: stop quoting on over-exposed side
|
||||||
|
- Virtual session clock: 1-hour windows since crypto is 24/7
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
|
||||||
|
class ASQuoter:
|
||||||
|
"""Stateless per-tick quote generator using A-S optimal control."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
gamma: float = 0.1, # Risk aversion — higher = more aggressive inventory redux
|
||||||
|
k: float = 1.5, # Order flow sensitivity — higher = tighter market
|
||||||
|
tau: float = 1.0, # Virtual session length (hours, for 24/7 crypto)
|
||||||
|
min_spread: float = 0.0001, # 1 bp minimum spread
|
||||||
|
max_inventory: float = 0.001, # Max position before stopping one side
|
||||||
|
vol_window: int = 300, # Number of price ticks for rolling vol (5 min @ 1s)
|
||||||
|
cb_mult: float = 3.0, # Circuit breaker multiplier (3σ jump threshold)
|
||||||
|
):
|
||||||
|
self.gamma = gamma
|
||||||
|
self.k = k
|
||||||
|
self.tau = tau
|
||||||
|
self.min_spread = min_spread
|
||||||
|
self.max_inventory = max_inventory
|
||||||
|
self.vol_window = vol_window
|
||||||
|
self.cb_mult = cb_mult
|
||||||
|
|
||||||
|
self._mid_prices: deque[float] = deque(maxlen=vol_window)
|
||||||
|
self._current_sigma: float = 0.02 # fallback: ~32% annualized for crypto
|
||||||
|
self._session_start: float = 0.0
|
||||||
|
|
||||||
|
def observe(self, mid: float) -> None:
|
||||||
|
"""Feed a new mid-price observation. Updates rolling volatility."""
|
||||||
|
self._mid_prices.append(mid)
|
||||||
|
if len(self._mid_prices) >= 2:
|
||||||
|
prices = list(self._mid_prices)
|
||||||
|
returns = [
|
||||||
|
(prices[i] - prices[i - 1]) / prices[i - 1]
|
||||||
|
for i in range(1, len(prices))
|
||||||
|
]
|
||||||
|
mu = sum(returns) / len(returns)
|
||||||
|
var = sum((r - mu) ** 2 for r in returns) / len(returns)
|
||||||
|
sigma = math.sqrt(var) if var > 0 else 0.02
|
||||||
|
self._current_sigma = sigma
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sigma(self) -> float:
|
||||||
|
return self._current_sigma
|
||||||
|
|
||||||
|
def circuit_breaker(self) -> bool:
|
||||||
|
"""Check if recent price jump exceeds threshold. If true, pause quoting."""
|
||||||
|
if len(self._mid_prices) < 5:
|
||||||
|
return False
|
||||||
|
recent = list(self._mid_prices)[-5:]
|
||||||
|
move_pct = abs(recent[-1] - recent[0]) / recent[0]
|
||||||
|
threshold = self.cb_mult * self._current_sigma * math.sqrt(5)
|
||||||
|
return move_pct > threshold
|
||||||
|
|
||||||
|
def quotes(self, mid: float, inventory: float, t: float) -> dict | None:
|
||||||
|
"""
|
||||||
|
Generate bid/ask quotes given current state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mid: current mid-price
|
||||||
|
inventory: current net position (positive = long)
|
||||||
|
t: elapsed session time in hours (0 to tau)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{"bid": ..., "ask": ..., "reservation": ..., "spread": ...} or None if paused
|
||||||
|
"""
|
||||||
|
self.observe(mid)
|
||||||
|
|
||||||
|
if self.circuit_breaker():
|
||||||
|
return None # Pause quoting — price jump in progress
|
||||||
|
|
||||||
|
# Reservation price: skew center by inventory risk
|
||||||
|
tau_remaining = max(self.tau - t, 0.01)
|
||||||
|
reservation = mid - inventory * self.gamma * (self._current_sigma ** 2) * tau_remaining
|
||||||
|
|
||||||
|
# Optimal spread: balance risk compensation vs flow capture
|
||||||
|
try:
|
||||||
|
log_term = math.log(1.0 + self.gamma / self.k)
|
||||||
|
except ValueError:
|
||||||
|
log_term = 0.0
|
||||||
|
spread = (
|
||||||
|
self.gamma * (self._current_sigma ** 2) * tau_remaining
|
||||||
|
+ (2.0 / max(self.gamma, 0.001)) * log_term
|
||||||
|
)
|
||||||
|
spread = max(spread, self.min_spread)
|
||||||
|
|
||||||
|
half = spread / 2.0
|
||||||
|
bid = reservation - half
|
||||||
|
ask = reservation + half
|
||||||
|
|
||||||
|
return {
|
||||||
|
"bid": max(bid, 1.0), # Never negative/zero
|
||||||
|
"ask": max(ask, 1.0),
|
||||||
|
"reservation": reservation,
|
||||||
|
"spread": spread,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user