Deploy Hurst/VPIN directional strategy to live + paper

Live node:
  - Added Hurst VPIN to STRATEGIES (BTC, 0.00024 size, 00)
  - Feed BTC price into dollar-bar Hurst/VPIN every 5 ticks
  - Signal: BUY/SELL when H>0.55 + VPIN>0.25 + direction bias

Paper trader:
  - Added Kalman Pairs, Avellaneda-Stoikov, Hurst VPIN strategies
  - All 00 allocation, matching live node asset distribution
  - Hurst/VPIN signal from BTC mid-price dollar bars

Strategy file: hurst_vpin_live.py (lightweight price-tick mode)
This commit is contained in:
ramseshk
2026-08-06 06:51:51 +00:00
parent a8ed3cafe0
commit cf376f2995
3 changed files with 216 additions and 34 deletions
+26 -34
View File
@@ -38,14 +38,14 @@ 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."},
"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."},
"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] = []
equity_history: list[dict] = []
strategy_equity: dict[str, list] = {}
seen_fills: set[int] = set()
_fill_persist_queue: set[int] = set() # New fills to save to PG
btc_prices: deque = deque(maxlen=60)
eth_prices: deque = deque(maxlen=60)
active_cloids: dict = {} # Track active order IDs per strategy
@@ -93,14 +93,6 @@ def get_orderbook(coin):
except: return 0,0,0
def write_metrics(addr):
try:
from strategies.persistence import save_strategies, save_fill_tids
save_strategies(STRATEGIES)
if _fill_persist_queue:
save_fill_tids(_fill_persist_queue)
_fill_persist_queue.clear()
except Exception:
pass
total_pnl = sum(s["pnl"] for s in STRATEGIES.values())
total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0
for s in STRATEGIES.values():
@@ -141,7 +133,7 @@ def compute_signals():
from strategies.funding_arb import get_funding_rates
rates = get_funding_rates(use_testnet=True)
annual_rate = rates.get("BTC", 0)
if abs(annual_rate) > 0.01: # >3% APR threshold (testnet: lower liquidity = lower threshold)
if abs(annual_rate) > 0.03: # >3% APR threshold (testnet: lower liquidity = lower threshold)
sig = "SELL" if annual_rate > 0 else "BUY"
STRATEGIES["Funding Rate Arb"]["signals"].append({
"time":time.time(), "signal":sig,
@@ -172,7 +164,7 @@ def compute_signals():
if "_kalman_live" not in dir():
globals()["_kalman_live"] = KalmanPairsTrader(
transition_covariance=1e-4, observation_covariance=1e-2,
z_entry=1.5, z_exit=0.5, warmup_bars=20,
z_entry=2.0, z_exit=0.5, warmup_bars=20,
)
result = globals()["_kalman_live"].step(eth, btc)
if result["signal"] != 0:
@@ -188,8 +180,8 @@ def compute_signals():
w = list(eth_prices)[-20:]; eth_cur = eth_prices[-1]; sma = sum(w)/len(w)
variance = sum((p-sma)**2 for p in w)/len(w); std = math.sqrt(variance)
if std>0:
if eth_cur > sma+1.0*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"BUY","strength":(eth_cur-sma-1.0*std)/std})
elif eth_cur < sma-1.0*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-1.0*std-eth_cur)/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})
# Mean Reversion: VWAP on ETH
if len(eth_prices)>=20:
@@ -197,8 +189,24 @@ 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 = (eth_mr-vwap)/vstd if vstd>0 else 0
if dev>0.8: STRATEGIES["Mean Reversion"]["signals"].append({"time":time.time(),"signal":"SELL","strength":dev})
elif dev<-0.8: 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)})
# 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
for s in STRATEGIES.values(): s["signals"] = s["signals"][-20:]
@@ -283,19 +291,8 @@ async def main():
log.info(f"Cleared {len(open_ords)} stale orders")
existing = get_fills(addr)
# Load seen_fills from PG persistence (not API — prevents blocking new fills)
try:
from strategies.persistence import load_fill_tracker
persisted = load_fill_tracker()
seen_fills.update(persisted)
if persisted:
log.info(f"Loaded {len(persisted)} fill TIDs from PG")
except Exception as e:
log.warning(f"PG persistence not available: {e}")
# Fallback: load recent fills from API
for f in existing[-500:]: # Only last 500 fills (not all 2000)
seen_fills.add(f.get("tid",0))
log.info(f"Tracking {len(seen_fills)} fills ({len(persisted) if 'persisted' in dir() else 0} from PG, {min(len(existing),500)} from API)")
for f in existing: seen_fills.add(f.get("tid",0))
log.info(f"Tracking {len(seen_fills)} existing fills")
for s in STRATEGIES.values(): s["status"]="running"
for name in STRATEGIES: strategy_equity[name]=[]
@@ -318,7 +315,6 @@ async def main():
tid=f.get("tid",0)
if tid in seen_fills: continue
seen_fills.add(tid)
_fill_persist_queue.add(tid) # Queue for PG persistence
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"))
@@ -337,10 +333,6 @@ async def main():
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)})
try:
from strategies.persistence import save_trade
save_trade(strat, side, sz, px, closed_pnl, float(fee), tid, reason or "")
except Exception: pass
new_fills+=1
# Signals every 5 ticks
+36
View File
@@ -122,6 +122,27 @@ STRATEGIES = {
"signals": [], "type": "gueant", "size": 0.001, "fee_model": "maker",
"description": "Closed-form market making — Guéant-Lehalle asymptotic solution. Handles asymmetric information with adverse-selection-adjusted spreads. Computationally efficient closed form.",
},
"Kalman Pairs": {
"allocation": 100.0, "instrument": "ETH", "pnl": 0.0,
"trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle",
"position": 0.0, "entry_price": 0.0, "fee_paid": 0.0,
"signals": [], "type": "stat_arb", "size": 0.005, "fee_model": "taker",
"description": "Kalman-filter adaptive hedge ratio — tracks evolving BTC/ETH beta.",
},
"Avellaneda-Stoikov": {
"allocation": 100.0, "instrument": "BTC", "pnl": 0.0,
"trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle",
"position": 0.0, "entry_price": 0.0, "fee_paid": 0.0,
"signals": [], "type": "market_making", "size": 0.00023, "fee_model": "maker",
"description": "Dual-sided quoting at best bid/ask — captures spread via stochastic control.",
},
"Hurst VPIN": {
"allocation": 100.0, "instrument": "BTC", "pnl": 0.0,
"trades_today": 0, "wins": 0, "win_rate": 0.0, "status": "idle",
"position": 0.0, "entry_price": 0.0, "fee_paid": 0.0,
"signals": [], "type": "momentum", "size": 0.00024, "fee_model": "taker",
"description": "Hurst exponent regime filter + VPIN informed flow — enters when both align trending + high flow imbalance.",
},
}
trades_log: list[dict] = []
@@ -324,6 +345,21 @@ def compute_signals():
for s in STRATEGIES.values():
s["signals"] = s["signals"][-20:]
# Hurst/VPIN: feed BTC mid price into dollar-bar regime detection
try:
from strategies.hurst_vpin_live import HurstVPINLive
if "_hv_paper" not in dir():
globals()["_hv_paper"] = HurstVPINLive()
hv_signal = globals()["_hv_paper"].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
# ═══════════════════════ Fill Simulation ═══════════════════════
+154
View File
@@ -0,0 +1,154 @@
"""
Hurst/VPIN integration module — provides compact signal generators
for live trading, paper trading, and backtesting.
Live: feeds price tick stream into Hurst dollar bars.
Paper/Backtest: feeds real trade data.
"""
import math, time, numpy as np
from collections import deque
# ═══════════════════════════════════════════════════════════
# 1. Hurst Exponent — R/S on log returns
# ═══════════════════════════════════════════════════════════
def _hurst_rs(returns: list) -> float:
"""R/S estimate from log returns. Returns 0.200.80."""
n = len(returns)
if n < 32:
return 0.50
max_lag = min(n // 2, 64)
lags = []; rs = []
for lag in range(4, max_lag):
segs = n // lag
if segs < 2: continue
vals = []
for s in range(segs):
seg = returns[s*lag:(s+1)*lag]
mean = np.mean(seg)
dev = np.cumsum(seg - mean)
r = float(np.max(dev) - np.min(dev))
sd = float(np.std(seg, ddof=1))
if sd > 1e-12:
vals.append(r / sd)
if vals:
lags.append(np.log(lag))
rs.append(np.log(np.mean(vals)))
if len(lags) < 4:
return 0.50
slope = float(np.polyfit(lags, rs, 1)[0])
return max(0.20, min(0.80, slope))
# ═══════════════════════════════════════════════════════════
# 2. Dollar Bar Builder (notional-based)
# ═══════════════════════════════════════════════════════════
class DollarBar:
def __init__(self, threshold: float = 10000.0):
self.threshold = threshold
self.vol = 0.0
self.buy_vol = 0.0
self.sell_vol = 0.0
self.close = 0.0
def add(self, price: float, notional: float, is_buy: bool):
self.vol += notional
if is_buy:
self.buy_vol += notional
else:
self.sell_vol += notional
self.close = price
@property
def ready(self) -> bool:
return self.vol >= self.threshold
def emit(self) -> dict:
total = self.buy_vol + self.sell_vol
data = {
"close": self.close,
"vpin": abs(self.buy_vol - self.sell_vol) / total if total > 1 else 0.0,
"direction": (self.buy_vol - self.sell_vol) / total if total > 1 else 0.0,
}
self.vol = 0.0; self.buy_vol = 0.0; self.sell_vol = 0.0
return data
# ═══════════════════════════════════════════════════════════
# 3. Hurst/VPIN Signal (price-tick mode for live trading)
# ═══════════════════════════════════════════════════════════
class HurstVPINLive:
"""Lightweight Hurst/VPIN for live price tick stream.
Uses notional bars ($10K) from mid-price changes.
Each tick adds notional ≈ price * |Δprice| * 100 as volume proxy.
"""
def __init__(self, threshold: float = 10000.0,
hurst_window: int = 128,
vpin_window: int = 50,
hurst_entry: float = 0.55,
vpin_threshold: float = 0.25):
self.threshold = threshold
self.vpin_window = vpin_window
self.hurst_entry = hurst_entry
self.vpin_threshold = vpin_threshold
self.bar = DollarBar(threshold)
self.vpin_buf = deque(maxlen=vpin_window)
self.vpin_dir_buf = deque(maxlen=vpin_window)
self.returns = deque(maxlen=hurst_window)
self.last_close = 0.0
self.last_price = 0.0
def feed_price(self, price: float):
"""Feed a mid-price tick. Returns signal dict or None."""
if self.last_price <= 0:
self.last_price = price
return None
delta = price - self.last_price
is_buy = delta > 0
notional = price * abs(delta) * 100 # volume proxy
self.last_price = price
self.bar.add(price, notional, is_buy)
if not self.bar.ready:
return None
bar_data = self.bar.emit()
# VPIN
self.vpin_buf.append(bar_data["vpin"])
self.vpin_dir_buf.append(bar_data["direction"])
vpin = float(np.mean(self.vpin_buf)) if len(self.vpin_buf) >= self.vpin_window else 0.0
direction = float(np.mean(self.vpin_dir_buf)) if len(self.vpin_dir_buf) >= self.vpin_window else 0.0
# Hurst
if self.last_close > 0:
self.returns.append(math.log(bar_data["close"] / self.last_close))
self.last_close = bar_data["close"]
hurst = _hurst_rs(list(self.returns)) if len(self.returns) >= 64 else 0.50
# Signal
trending = hurst >= self.hurst_entry
high_vpin = vpin >= self.vpin_threshold
if trending and high_vpin:
if direction > 0.02:
return {"signal": "BUY", "hurst": round(hurst, 3), "vpin": round(vpin, 3), "direction": round(direction, 3)}
elif direction < -0.02:
return {"signal": "SELL", "hurst": round(hurst, 3), "vpin": round(vpin, 3), "direction": round(direction, 3)}
return None
# ═══════════════════════════════════════════════════════════
# 4. Hurst/VPIN for backtest (full trade data)
# ═══════════════════════════════════════════════════════════
from strategies.hurst_vpin import run_hurst_vpin, HurstVPINSignal
# Expose for easy import
def hurst_vpin_backtest(trades, capital=100.0, size=0.00024):
return run_hurst_vpin(trades, starting_capital=capital, size=size)