Overall Statistics
Total Orders
945
Average Win
1.79%
Average Loss
-1.63%
Compounding Annual Return
37.014%
Drawdown
22.400%
Expectancy
0.286
Start Equity
15000
End Equity
125337.69
Net Profit
735.585%
Sharpe Ratio
1.216
Sortino Ratio
1.461
Probabilistic Sharpe Ratio
73.777%
Loss Rate
39%
Win Rate
61%
Profit-Loss Ratio
1.09
Alpha
0.198
Beta
0.524
Annual Standard Deviation
0.199
Annual Variance
0.04
Information Ratio
0.813
Tracking Error
0.195
Treynor Ratio
0.461
Total Fees
$1242.71
Estimated Strategy Capacity
$250000000.00
Lowest Capacity Asset
KLAC R735QTJ8XC9X
Portfolio Turnover
32.55%
from AlgorithmImports import *
'''
Estrategia: VIXSIHedge

Creador: Enrique Abad Clarimón
email: enr40@hotmail.com

Fecha última modificación: 16-12-2024

Descripción:
Esta estrategia utiliza una señal basada en VIX (VIXSI) para actuar de cobertura con una cartera 
específica (self.port_edge), operando la cartera en corto cuando la señal VIXSI es -1,
y el activo (_v_asset) en largo cuando la señal VIXSI es 1.
 
Si la cartera está vacía (self.port_edge no tiene datos), se usa al mismo activo  cartera de cobertura.

Parámetros de la estrategia:

_v_asset: activo a realizar cobertura

_v_start_date: fecha inicio backtesting

_v_end_date: fecha finalización backtesting

_v_leverage: apalancamiento (mayor o igual a 1, puede ser con decimales)

_str_window: ventana de datos para calcular drawdown

_str_mindwn: mínimo %drawdown en valor absoluto para activar la estrategia (-1 la activa siempre,
100 inactiva siempre).  Si el %drawdown del activo en el periodo especificado _wtr_window es menor o igual que _str_mindwn,
 el activo se opera en holding (el activo está en máximos para la estrategia: self.maxim=True)

_str_maxdwdn: máximo %drawdown en valor absoluto para estar invertido (100 siempre se está invertido).
 Si el %drawdown del activo en el periodo especificado _str_window es mayor que _str_maxwdn,
 se está desinvertido en largo y en corto (el activo está en mínimos para la estrategia: self.minim=True)

_cash_init: capital inicial de la estrategia (sólo para backtesting)

_cash_month_incr: incremento de capital al inicio de cada mes (sólo para backtesting)

_api_url: url del api para obtener la señal vixsi en tiempo real (sólo para live trading)

_api_key: clave de la api

_port_activo: cartera hedge se utiliza como activo (valor 1). En este caso, tanto si se opera en largo
como se opera en corto, se hace con la cartera self.port_hedge

Observaciones: Para operar en live trading, se puede solicitar la url y clave de la api por email a:
enr40@hotmail.com (Enrique Abad)

'''

import requests

class VixSignal (QCAlgorithm):

    def Initialize(self):

        # Diccionario que contienen el portfolio de cobertura, indicando la cartera a utilizar por año
        self.port_hedge = dict()
        self.port_hedge = {'2018': ['BK'],
                            '2019': ['SLB'],
                            '2020': ['PFG', 'AMAT'],
                            '2021': ['KLAC'],
                            '2022': ['KLAC'],
                            '2023': ['KLAC'],
                            '2024': ['AMAT', 'KLAC']}

        # Indicar si el portfolio de cobertura también va a ser el activo sustituyendo a SPY (valor 1)
        self.port_activo = int(self.GetParameter("_port_activo"))
        # guarda los activos de la cartera invertida
        self.port_assets = []
               # Inicialización de la cartera de cobertura a partir de su diccionario port_hedge
        self.assets = []
        if len(self.port_hedge)>0:
            hedge_tickers = []
            for _value in self.port_hedge.values():
                if isinstance(_value, list):
                    for __value in _value:
                        if __value not in hedge_tickers:
                            hedge_tickers.append(__value)
                else:
                    if _value not in hedge_tickers:
                        hedge_tickers.append(_value)
            self.assets = dict()
            for ticker in hedge_tickers:
                self.assets[ticker] = self.AddEquity(ticker, Resolution.Minute).Symbol

        # Controla si el portfolio de cobertura está invefrtido o no
        self.port_orders = False


        # Inicialización de variables por parámetros
        v_asset = self.GetParameter("_v_asset")
        v_start_date = self.GetParameter("_v_start_date")
        v_end_date = self.GetParameter("_v_end_date")
        v_symbol = self.GetParameter("_v_symbol")
        v_leverage = int(self.GetParameter("_v_leverage"))
        # Apalancamiento debe ser mayor o igual a 1
        if v_leverage < 1:
            self.Quit ()
            return

        # Inicialización de parámetros de la estrategia
        str_window = int(self.GetParameter("_str_window"))
        str_mindwdn = float(self.GetParameter("_str_mindwdn"))
        str_maxdwdn = float(self.GetParameter("_str_maxdwdn"))


        # Initialization de las variables capital inicial y capital mensual (en USD)
        cash_init = float(self.GetParameter("_cash_init"))
        cash_month_incr = float(self.GetParameter("_cash_month_incr"))


        # Inicialización de los parámetros de la API 
        api_url = self.GetParameter("_api_url")
        api_key = self.GetParameter("_api_key")


        self.SetStartDate(int(v_start_date[:4]), int(v_start_date[5:7]), int(v_start_date[8:10]))
        self.SetEndDate(int(v_end_date[:4]), int(v_end_date[5:7]), int(v_end_date[8:10]))

        self.capital = cash_init
        self.SetCash(self.capital)

        # Se utiliza el broker Interactive Brokers para calcular comisiones, margen, ...   
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)

        # Si la orden no se ejecuta antes del cierre del mercado, se cancelará automáticamente.
        self.default_order_properties.time_in_force = TimeInForce.DAY

        # La resolución es de 1 minuto para poder operar 1 minuto antes del cierre del mercado 
        self.asset = self.AddEquity(v_asset, Resolution.Minute).Symbol

        # mínimo en caja para evitar llamada de margen (en IB) cuando el apalancamiento es mayor que 1
        self.min_cash = 2500

        # máximo apalancamiento cuando en caja hay más que self.min_cash
        self.max_leverage = v_leverage

        # el benchmark es el propio activo (parámetro v_asset)
        self.SetBenchmark(self.asset)


        # Parámetros para salir de la estrategia VIXSI cuando el activo está en máximos (stop hedging),
        # y para salir del activo cuando el activo está en su mínimo (stoploss)

        # minimo/maximo dwdn
        self.mindwdn = str_mindwdn
        self.maxdwdn = str_maxdwdn


        # ventana de rolling para obtener el drawdown del activo
        self.asset_w = RollingWindow[float](str_window)
    
        # Si self.maxim es True, el activo está en holding (sin operar la estrategia VIXSI)
        # Si self.minim es True, el activo está fuera de mercado
        # Ambos valores son calculados a partir del drawdown del activo y sus parámetros
        self.maxim = False
        self.minim = False


        # Inicialización de la ventana de rolling desde el histórico de datos 
        prices_df = self.History(self.asset, str_window, Resolution.Daily)["close"]
        for time, price in prices_df.loc[self.asset].items():
            self.asset_w.Add(price)

        # Si la estrategia está en modo de prueba retrospectiva, los datos de la señal vixsi
        # se extraen de "dropbox",
        # Si la estrategia es en operaciones reales, los datos de la señal vixsi se obtienen
        # mediante solicitud de API.
        if self.live_mode == False:
            self.vixsi = self.AddData(VixsiData, v_asset, Resolution.Minute).Symbol

        # Initialización de la señal de cobertura
        self.vixsi_value = 0


        # La estrategia se ejecuta en "updPositions" un minuto antes del cierre del mercado, 
        # para dar tiempo para obtener la señal y ejecutar la orden a mercado    
        self.Schedule.On(self.DateRules.EveryDay(self.asset),
                 self.TimeRules.BeforeMarketClose(self.asset, 1),      
                 self.UpdPositions)

        if self.live_mode == False:
            # Incrementos mensuales del capital a principio de cada mes
            self.cash_incr = cash_month_incr
            self.Schedule.On(self.DateRules.MonthStart(self.asset), 
                            self.TimeRules.AfterMarketOpen(self.asset), 
                            self.AddMonthlyCash)


    def get_dwdown(self, asset_value):
        '''
        calcula el drawdown del activo con respecto al período de la ventana "_str_window",
        actualizando self.maxim y self.minim
        
        '''

        high_price = max(self.asset_w)
        if high_price < asset_value:
            high_price = asset_value

        drwdwn = (high_price - asset_value) / high_price * 100
        self.Log(f"max_value:{high_price:.2f} actual_valor:{asset_value:.2f} drawdown:{drwdwn:.2f}")
        if drwdwn <= self.mindwdn:
            self.maxim = True
        else:
            self.maxim = False

        if drwdwn > self.maxdwdn:
            self.minim = True
        else:
            self.minim = False
        

    def OnData(self, data):
        '''
        actualiza la señal vixsi desde el conjunto de datos de Dropbox,
        sólo actua en modo backtesting, en modo live trading la señal la 
        recoge en tiempo real por API
         
        '''

        if self.live_mode == False and self.vixsi in data:
            self.vixsi_value= data[self.vixsi].Value

    def OnMarginCallWarning(self):

        self.Debug(f"maringcall warning")
        self.Liquidate()

    def comercializar (self, v_invest):

        total_portfolio = self.capital + self.Portfolio.TotalProfit + self.Portfolio.TotalUnrealizedProfit
        hedge_assets = []
        if (len(self.port_hedge)>0):
            year = self.Time.strftime("%Y")
            if year in self.port_hedge:
                hedge_assets = self.port_hedge[year]
        num_assets = len(hedge_assets)

        # Si la nueva cartera es diferente a la anterior y está invertida, liquida las posiciones
        if len(self.port_assets) > 0 and self.port_assets != hedge_assets and self.port_orders > 0:
            self.liquidate()

        # Si la nueva cartera es diferente a la actual, se igualan
        if self.port_assets != hedge_assets:
            self.port_assets = hedge_assets.copy()

        # Invierte la cartera si no está en máximos, hay activos en la cartera y
        # es cobertura o la cartera actua también como activo
        if num_assets > 0 and self.maxim == False and (v_invest < 0 or self.port_activo > 0):
            # desinvierte el activo por si está invertido cuando se invierte la cartera
            if self.port_orders == False:
                self.SetHoldings(self.asset, 0)
                self.port_orders = True                
            for ticker in hedge_assets:
                symbol_hedge = self.assets[ticker]
                self.SetHoldings(symbol_hedge, v_invest/num_assets)
                if v_invest < 0:
                    self.Log(f"{total_portfolio} con {num_assets} activos vende en {ticker} apalancamiento: {self.leverage}")
                else:
                    self.Log(f"{total_portfolio} con {num_assets} activos compra en {ticker} apalancamiento: {self.leverage}")
        # Invierte SPY
        else:
            # liquida todas las posiciones de la cartera si está invertida
            if self.port_orders:
                self.liquidate()
                self.port_orders = False

            self.SetHoldings(self.asset, v_invest)
            if v_invest < 0:
                self.Log(f"{total_portfolio} con {self.asset} apalancamiento: {v_invest}")
            else:
                self.Log(f"{total_portfolio} con {self.asset} apalancamiento: {v_invest}")

        return

    def UpdPositions(self):

        asset_value = self.Securities[self.asset].Close
        total_portfolio = self.capital + self.Portfolio.TotalProfit + self.Portfolio.TotalUnrealizedProfit
        if (total_portfolio - self.min_cash) <= 0:
            self.leverage = 1
        else:
            self.leverage = self.max_leverage

        # Ventana de datos de actualización diaria
        self.asset_w.Add(asset_value)

        # sólo se comercializa si la ventana de datos está llena
        if not self.asset_w.IsReady:
            return

        if self.live_mode == True:
            # Obtienela señal vixsi en tiempo real con api cuando self.live_mode es True
            auth_key = api_key
            base_url = api_url
    
            headers = {'Authorization': auth_key}
            vixsi_value = 0

            try:
                response = requests.get(base_url, headers=headers)
                if response.status_code == 200:
                    data = response.json()
                    vixsi_value = data["vixsi"]
                    self.Log(f"vixsi_value {vixsi_value}")
                else:
                    self.Debug(f"Error al obtener datos: {response.status_code}")
            except Exception as e:
                self.Debug(f"Excepción durante la solicitud de API: {str(e)}")
        else:
            vixsi_value = self.vixsi_value

        self.Log(f"LiveMode: {self.live_mode} vixsi_valor:{vixsi_value}")

        if np.abs(vixsi_value) > 0:
            asset_drwd = self.get_dwdown(asset_value)

            if (vixsi_value == 1 or self.maxim or self.minim):

                # liquida todas las posiciones si la cartera de cobertura está invertida
                # y si el activo no es la cartera
                #if self.port_orders and self.port_activo == 0:
                #    self.liquidate()
                #    self.port_orders = False

                if self.minim:
                    self.comercializar(0)
                    #self.SetHoldings(self.asset, 0)
                else:
                    self.comercializar(self.leverage)
                    #self.SetHoldings(self.asset, self.leverage)
                #self.Log(f"{total_portfolio} Buy in {asset_value} with leverage {self.leverage}")
            elif vixsi_value == -1:
                '''
                # Comprueba si hay cartera de cobertura
                hedge_assets = []
                if (len(self.port_hedge)>0):
                    year = self.Time.strftime("%Y")
                    if year in self.port_hedge:
                        hedge_assets = self.port_hedge[year]
                num_assets = len(hedge_assets)
                if num_assets > 0:
                    if (self.port_orders == False):
                        # Liquida la posición del activo
                        self.SetHoldings(self.asset, 0)
                        self.port_orders = True
                    
                    for ticker in hedge_assets:
                        symbol_hedge = self.assets[ticker]
                        self.SetHoldings(symbol_hedge, -self.leverage/num_assets)
                        self.Log(f"{total_portfolio} con {num_assets} activos vende en {ticker} apalancamiento: {self.leverage}")
                else:
                    self.SetHoldings(self.asset, -self.leverage)
                    self.Log(f"{total_portfolio} vende en {asset_value} apalancamiento: {self.leverage}")
                '''
                self.comercializar(-self.leverage)

    def AddMonthlyCash(self):
        if self.cash_incr > 0:
            self.Log(f"Incrementa {self.cash_incr} USD en {self.Time}")
            self.portfolio.cash_book["USD"].add_amount(self.cash_incr)
            self.capital = self.capital + self.cash_incr


class VixsiData(PythonData):

    def GetSource(self, config, date, isLive):
        #source = "https://www.dropbox.com/scl/fi/u502ukcth8g8e3qzf7yuv/VIXSIyf.csv?rlkey=446pbh1c50e2dk1f7z1pbk34x&st=p4nwhtol&dl=1"
        source = "https://www.dropbox.com/scl/fi/kodwp9m69wuf1nixyvsp3/vixsi.csv?rlkey=s47sto8bqvmw0a38pnx2ctb80&st=vmafzm2v&dl=1"
        return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile)


    def Reader(self, config, line, date, isLive):
        '''
        Lee el valor de la señal Vixsi
        
        '''
        
        data = line.split(',')
        vixsi = VixsiData()
        
        try:
            vixsi.Symbol = config.Symbol
            # Actualiza la fecha de 00:00 a 9:35 para que Quantconnect la lea el mismo
            # día que ejecuta UpdPositions, ya que los datos del día los recoge a partir de
            # la hora de inicio de mercado.
            vixsi.Time = datetime.strptime(data[0], "%Y-%m-%d") + timedelta(hours=9, minutes=35)
            vixsi.Value = int(data[1])          
            
        except ValueError:
            return None
        
        return vixsi