Overall Statistics |
Total Orders 683 Average Win 1.85% Average Loss -1.53% Compounding Annual Return 39.876% Drawdown 21.000% Expectancy 0.335 Start Equity 15000 End Equity 80218.55 Net Profit 434.790% Sharpe Ratio 1.328 Sortino Ratio 1.503 Probabilistic Sharpe Ratio 76.469% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 1.21 Alpha 0.207 Beta 0.573 Annual Standard Deviation 0.199 Annual Variance 0.04 Information Ratio 0.871 Tracking Error 0.188 Treynor Ratio 0.462 Total Fees $1193.84 Estimated Strategy Capacity $19000000.00 Lowest Capacity Asset TER R735QTJ8XC9X Portfolio Turnover 30.10% |
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 = {'2019': ['WU'], '2020': ['TXN'], '2021': ['TER'], '2022': ['TER', 'KLAC'], '2023': ['AMAT', 'TER'], '2024': ['KLAC']} self.port_hedge = {'2018': ['AVB'], '2019': ['WU'], '2020': ['TXN'], '2021': ['TER', 'KLAC'], '2022': ['TER', 'KLAC'], '2023': ['TER'], '2024': ['TER']} # 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" 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