Overall Statistics
Total Orders
828
Average Win
1.99%
Average Loss
-1.85%
Compounding Annual Return
37.764%
Drawdown
22.400%
Expectancy
0.296
Start Equity
15000
End Equity
131526.43
Net Profit
776.843%
Sharpe Ratio
1.278
Sortino Ratio
1.502
Probabilistic Sharpe Ratio
79.056%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.07
Alpha
0.197
Beta
0.577
Annual Standard Deviation
0.192
Annual Variance
0.037
Information Ratio
0.896
Tracking Error
0.181
Treynor Ratio
0.424
Total Fees
$1041.23
Estimated Strategy Capacity
$120000000.00
Lowest Capacity Asset
KLAC R735QTJ8XC9X
Portfolio Turnover
32.37%
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)

_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: En esta versión no está disponible la api para operar en live trading, sólo es para backtesting

'''

import requests

class VixsiHedge (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': ['TXN'],
                            '2021': ['KLAC'],
                            '2022': ['KLAC'],
                            '2023': ['KLAC'],
                            '2024': ['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 hedge invertida para cancelarlos si los nuevos activos hedge son distintos
        self.port_assets = []
    
        # Inicialización de la cartera de cobertura a partir de su diccionario port_hedge
        self.hedge_symbols = dict()
        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)
            for ticker in hedge_tickers:
                self.hedge_symbols[ticker] = self.AddEquity(ticker, Resolution.Minute).Symbol

        # Controla si el portfolio de cobertura está invertido 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 = float(self.GetParameter("_v_leverage"))
        
        # Apalancamiento debe ser mayor o igual a 1
        if v_leverage < 1:
            self.Quit ()
            return

        # 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)

        # 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 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):
        '''
        Se ejecuta si el broker avisa de llamada de margen si
        el apalancamiento es mayor de 1 (_v_leverage>1).

        Liquida todas las posiciones

        '''

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

    def comercializar (self, v_invest):
        '''
        Realiza las órdenes de compra/venta en función de v_invest, que indica el % del total capital a invertir y 
        el signo.

        Si es positivo, invierte en el activo, si es negativo, invierte en corto con la cartera de cobertura

        '''

        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()
            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 (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.hedge_symbols[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):
        '''
        
        Función que se activa un minuto antes de cierre de mercado para acceder al valor de vixsignal y 
        operar en función de la señal.

        '''

        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

        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:
            if vixsi_value == 1:
                self.comercializar(self.leverage)
            else:
                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/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 del fichero en dropbox (sólo se aplica en backtesting, en live trading accede a la api)
        
        '''
        
        data = line.split(',')
        vixsi = VixsiData()
        
        try:
            vixsi.Symbol = config.Symbol
            # La señal en el histórico tiene únicamente la fecha, no la hora, que por defecto se coloca a 00:00:00.
            # Se debe de añadir las 09:35 para que Quantconnect la recoja el mismo en onData en el mismo día que 
            # se opera en UpdPosition (como hay un solo valor por día, se mantiene hasta final de día) 

            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