Overall Statistics
Total Orders
655
Average Win
2.20%
Average Loss
-0.99%
Compounding Annual Return
20.959%
Drawdown
15.300%
Expectancy
0.362
Start Equity
150000
End Equity
438361.34
Net Profit
192.241%
Sharpe Ratio
0.85
Sortino Ratio
0.668
Probabilistic Sharpe Ratio
50.794%
Loss Rate
58%
Win Rate
42%
Profit-Loss Ratio
2.22
Alpha
0.083
Beta
0.345
Annual Standard Deviation
0.14
Annual Variance
0.02
Information Ratio
0.094
Tracking Error
0.162
Treynor Ratio
0.345
Total Fees
$4638.66
Estimated Strategy Capacity
$26000000.00
Lowest Capacity Asset
VX YYDBHHPS76JT
Portfolio Turnover
7.40%
Drawdown Recovery
189
from AlgorithmImports import *
from datetime import datetime, timedelta
import json

class BTC_VIX_Hedge_Strategy(QCAlgorithm):
    
    def Initialize(self):
        """Initialize the algorithm with BTC/VIX hedge strategy"""
        
        # === PAPER TRADING SETUP ===
        # Comment out broker lines for paper trading (QC default)
        # self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        # self.SetBrokerageModel(BrokerageName.CoinbaseBrokerage)
        
        # === BACKTEST SETTINGS ===
        self.SetStartDate(2020, 5, 3)
        self.SetEndDate(2025, 12, 20)
        self.SetCash(150000)
        
        # === SYMBOLS ===
        #self.btc_future_object = self.AddFuture(Futures.Currencies.MICRO_BTC, Resolution.HOUR)
        self.vix_future = self.AddFuture(Futures.Indices.VIX, Resolution.Hour)
        #self.btc_future_object.SetFilter(30, 90)
        self.vix_future.SetFilter(15, 45)
        self.vix_index = self.AddIndex("VIX", Resolution.Daily).Symbol
        
        # === TRADERSPOST WEBHOOK ===
        self.webhook_url = "https://webhooks.traderspost.io/trading/webhook/fd213722-ba73-4113-8eff-51ba52760495/e257562b94b6865bb4b34012c2dab64a"
        
        # === STATE TRACKING ===
        self.invested = False
        self.previous_vix_close = None
        self.last_vix_update_date = None
        
        # === SLIPPAGE TRACKING ===
        self.last_slippage_reset_date = None
        self.daily_slippage_dollars = 0.0
        self.daily_trades_count = 0
        
        # === SIGNAL THRESHOLDS ===
        self.vix_high_threshold = 21.0
        
        # Warm up period
        self.SetWarmUp(10)
        
        self.Log("[INIT] BTC-VIX Strategy with Pyramid Scaling")

    def OnData(self, data):
        """Main trading logic"""
        
        if self.IsWarmingUp:
            return
        
        # Check for expiry
        for holding in self.Portfolio.Values:
            if holding.Symbol.SecurityType == SecurityType.Future and holding.Invested:
                
                # Drawdown Alert
                if holding.UnrealizedProfit < -4500:
                    self.Log(f"[DRAWDOWN ALERT] {holding.Symbol.Value} drawdown > $4500: ${holding.UnrealizedProfit:.2f}")

                if (holding.Symbol.ID.Date - self.Time).days < 5:
                    # self.Log(f"[EXPIRY] Closing all.")
                    self.close_all_positions()
                    return
                
                # Stop Loss
                if holding.UnrealizedProfitPercent < -0.029:
                    # self.Log(f"[STOP LOSS] {holding.Symbol.Value} hit -2.5%.")
                    self.close_all_positions()
                    return
                
                # Take Profit
                if holding.UnrealizedProfitPercent > 0.07:
                    # self.Log(f"[TAKE PROFIT] {holding.Symbol.Value} hit +6.5%.")
                    self.close_all_positions()
                    return

        # VIX DATA
        current_vix_close = self.Securities[self.vix_index].Price
        
        # Initialize if None
        if self.last_vix_update_date is None:
            self.previous_vix_close = current_vix_close
            self.last_vix_update_date = self.Time.date()
            return

        # ===== ENTRY LOGIC WITH GAP FILTER =====
        # Only check once per day
        if self.Time.date() > self.last_vix_update_date:
            if self.previous_vix_close is not None:
                vix_change_pct = (current_vix_close - self.previous_vix_close) / self.previous_vix_close
                
                # OPTION 1: Enter on 7.5% VIX gap
                if not self.Portfolio.Invested and abs(vix_change_pct) > 0.05:
                    # Dynamic sizing by VIX level
                    if current_vix_close >= 26.0:
                        target_qty = 5
                    elif current_vix_close >= 23.5:
                        target_qty = 4
                    elif current_vix_close >= 21.0:
                        target_qty = 3
                    elif current_vix_close >= 18.0:
                        target_qty = 2
                    else:
                        target_qty = 1
                    
                    self.enter_vix_trade(data, target_qty)
                
                # SCALE UP if already invested AND VIX rises further
                elif self.Portfolio.Invested and vix_change_pct > 0.08:  # Only on UP moves
                    current_qty = sum(abs(h.Quantity) for h in self.Portfolio.Values 
                                     if h.Symbol.SecurityType == SecurityType.Future and h.Invested)
                    
                    # Determine new target based on current VIX
                    if current_vix_close >= 26.0:
                        new_target = 5
                    elif current_vix_close >= 23.5:
                        new_target = 4
                    elif current_vix_close >= 21.0:
                        new_target = 3
                    else:
                        new_target = current_qty  # Don't scale down
                    
                    # Only add more if VIX went UP and we haven't hit max
                    if new_target > current_qty:
                        add_qty = new_target - current_qty
                        # Find current VIX holding
                        for holding in self.Portfolio.Values:
                            if holding.Symbol.SecurityType == SecurityType.Future and holding.Invested:
                                self.MarketOrder(holding.Symbol, -add_qty)
                                
                                # Send webhook
                                self.send_traderspost_webhook(
                                    ticker=holding.Symbol.Value,
                                    action="sell",
                                    quantity=add_qty,
                                    price=self.Securities[holding.Symbol].Price
                                )
                                
                                self.Log(f"[SCALE] VIX jumped to {current_vix_close:.2f}. Adding {add_qty} shorts. Total: {new_target}")
                                break
            
            # Update for next day
            self.previous_vix_close = current_vix_close
            self.last_vix_update_date = self.Time.date()

    def enter_vix_trade(self, data, quantity):
        """Short VIX Futures"""
        vix_chain = data.FutureChains.get(self.vix_future.Symbol)
        if not vix_chain:
            return
        
        vix_contracts = sorted([c for c in vix_chain], key=lambda x: x.Expiry)
        if not vix_contracts:
            return
        
        selected_vix = vix_contracts[0]
        self.MarketOrder(selected_vix.Symbol, -quantity)
        
        # Send webhook to TradersPost
        self.send_traderspost_webhook(
            ticker=selected_vix.Symbol.Value,
            action="sell",  # We're shorting
            quantity=quantity,
            price=selected_vix.LastPrice
        )
        
        self.Log(f"[ENTRY] Short {quantity} {selected_vix.Symbol.Value} | VIX: {self.Securities[self.vix_index].Price}")
    
    def close_all_positions(self):
        """Close all positions and send exit webhook"""
        for holding in self.Portfolio.Values:
            if holding.Invested:
                symbol = holding.Symbol.Value
                quantity = abs(holding.Quantity)
                price = holding.Price
                
                # Liquidate in QuantConnect
                self.Liquidate(holding.Symbol)
                
                # Send exit webhook to TradersPost
                self.send_traderspost_webhook(
                    ticker=symbol,
                    action="exit",
                    quantity=quantity,
                    price=price
                )
                
                self.Log(f"[CLOSE] Closed {symbol}")
        
        self.Log(f"[EXIT] All positions closed | Portfolio: ${self.Portfolio.TotalPortfolioValue:,.2f}")

    def send_traderspost_webhook(self, ticker, action, quantity, price=None):
        """Send webhook to TradersPost"""
        payload = {
            "ticker": ticker,
            "action": action,
            "quantity": quantity
        }
        
        if price is not None:
            payload["price"] = float(price)
        
        json_data = json.dumps(payload)
        
        if self.LiveMode:
            try:
                self.Notify.Web(address=self.webhook_url, data=json_data)
                self.Log(f"[WEBHOOK] Sent to TradersPost: {json_data}")
            except Exception as e:
                self.Log(f"[WEBHOOK ERROR] {str(e)}")
        else:
            self.Log(f"[WEBHOOK BACKTEST] Would send: {json_data}")
    
    def OnOrderEvent(self, orderEvent: OrderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
        order = self.Transactions.GetOrderById(orderEvent.OrderId)

        # Reset daily slippage at start of new trading day
        if self.last_slippage_reset_date is None or self.Time.date() > self.last_slippage_reset_date:
            self.daily_slippage_dollars = 0.0
            self.daily_trades_count = 0
            self.last_slippage_reset_date = self.Time.date()

        # Slippage calc
        market_price = self.Securities[order.Symbol].Price
        if market_price > 0:
            filled_price = orderEvent.FillPrice
            if order.Direction == OrderDirection.Buy:
                slippage_per_unit = filled_price - market_price
            else:
                slippage_per_unit = market_price - filled_price
            trade_slippage_dollars = slippage_per_unit * order.AbsoluteQuantity
            self.daily_slippage_dollars += trade_slippage_dollars
            self.daily_trades_count += 1

    def OnEndOfDay(self, symbol):
        pass