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