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:
+26
-34
@@ -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."},
|
"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] = []
|
||||||
equity_history: list[dict] = []
|
equity_history: list[dict] = []
|
||||||
strategy_equity: dict[str, list] = {}
|
strategy_equity: dict[str, list] = {}
|
||||||
seen_fills: set[int] = set()
|
seen_fills: set[int] = set()
|
||||||
_fill_persist_queue: set[int] = set() # New fills to save to PG
|
|
||||||
btc_prices: deque = deque(maxlen=60)
|
btc_prices: deque = deque(maxlen=60)
|
||||||
eth_prices: deque = deque(maxlen=60)
|
eth_prices: deque = deque(maxlen=60)
|
||||||
active_cloids: dict = {} # Track active order IDs per strategy
|
active_cloids: dict = {} # Track active order IDs per strategy
|
||||||
@@ -93,14 +93,6 @@ def get_orderbook(coin):
|
|||||||
except: return 0,0,0
|
except: return 0,0,0
|
||||||
|
|
||||||
def write_metrics(addr):
|
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 = sum(s["pnl"] for s in STRATEGIES.values())
|
||||||
total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0
|
total_pnl_pct = (total_pnl/TOTAL_EQUITY)*100 if TOTAL_EQUITY>0 else 0
|
||||||
for s in STRATEGIES.values():
|
for s in STRATEGIES.values():
|
||||||
@@ -141,7 +133,7 @@ def compute_signals():
|
|||||||
from strategies.funding_arb import get_funding_rates
|
from strategies.funding_arb import get_funding_rates
|
||||||
rates = get_funding_rates(use_testnet=True)
|
rates = get_funding_rates(use_testnet=True)
|
||||||
annual_rate = rates.get("BTC", 0)
|
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"
|
sig = "SELL" if annual_rate > 0 else "BUY"
|
||||||
STRATEGIES["Funding Rate Arb"]["signals"].append({
|
STRATEGIES["Funding Rate Arb"]["signals"].append({
|
||||||
"time":time.time(), "signal":sig,
|
"time":time.time(), "signal":sig,
|
||||||
@@ -172,7 +164,7 @@ def compute_signals():
|
|||||||
if "_kalman_live" not in dir():
|
if "_kalman_live" not in dir():
|
||||||
globals()["_kalman_live"] = KalmanPairsTrader(
|
globals()["_kalman_live"] = KalmanPairsTrader(
|
||||||
transition_covariance=1e-4, observation_covariance=1e-2,
|
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)
|
result = globals()["_kalman_live"].step(eth, btc)
|
||||||
if result["signal"] != 0:
|
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)
|
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)
|
variance = sum((p-sma)**2 for p in w)/len(w); std = math.sqrt(variance)
|
||||||
if std>0:
|
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})
|
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.0*std: STRATEGIES["Momentum Breakout"]["signals"].append({"time":time.time(),"signal":"SELL","strength":(sma-1.0*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
|
||||||
if len(eth_prices)>=20:
|
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)
|
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))
|
vstd = math.sqrt(sum((p-vwap)**2 for p in w)/len(w))
|
||||||
dev = (eth_mr-vwap)/vstd if vstd>0 else 0
|
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})
|
if dev>1.0: 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)})
|
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:]
|
||||||
@@ -283,19 +291,8 @@ async def main():
|
|||||||
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)
|
||||||
# Load seen_fills from PG persistence (not API — prevents blocking new fills)
|
for f in existing: seen_fills.add(f.get("tid",0))
|
||||||
try:
|
log.info(f"Tracking {len(seen_fills)} existing fills")
|
||||||
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 s in STRATEGIES.values(): s["status"]="running"
|
for s in STRATEGIES.values(): s["status"]="running"
|
||||||
for name in STRATEGIES: strategy_equity[name]=[]
|
for name in STRATEGIES: strategy_equity[name]=[]
|
||||||
@@ -318,7 +315,6 @@ async def main():
|
|||||||
tid=f.get("tid",0)
|
tid=f.get("tid",0)
|
||||||
if tid in seen_fills: continue
|
if tid in seen_fills: continue
|
||||||
seen_fills.add(tid)
|
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))
|
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"))
|
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
|
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"]})
|
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)})
|
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
|
new_fills+=1
|
||||||
|
|
||||||
# Signals every 5 ticks
|
# Signals every 5 ticks
|
||||||
|
|||||||
@@ -122,6 +122,27 @@ STRATEGIES = {
|
|||||||
"signals": [], "type": "gueant", "size": 0.001, "fee_model": "maker",
|
"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.",
|
"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] = []
|
trades_log: list[dict] = []
|
||||||
@@ -324,6 +345,21 @@ def compute_signals():
|
|||||||
|
|
||||||
for s in STRATEGIES.values():
|
for s in STRATEGIES.values():
|
||||||
s["signals"] = s["signals"][-20:]
|
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 ═══════════════════════
|
# ═══════════════════════ Fill Simulation ═══════════════════════
|
||||||
|
|
||||||
|
|||||||
@@ -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.20–0.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)
|
||||||
Reference in New Issue
Block a user