diegobeyl/backtesting
2
1"""2Cargador de datos multi-fuente3Soporta: Yahoo Finance4"""5 6import pandas as pd7import time8from typing import Optional, List, Dict9from datetime import datetime, timedelta10import streamlit as st11 12import yfinance as yf13 14 15# Fuentes de datos disponibles16DATA_SOURCES = {17 'yahoo': 'Yahoo Finance',18}19 20# Mapeo de símbolos por fuente21SYMBOL_MAPPING = {}22 23# Mapeo de timeframes por fuente24TIMEFRAME_MAPPING = {25 'yahoo': {26 '1m': '1m', '5m': '5m', '15m': '15m', '30m': '30m',27 '1h': '1h', '4h': '4h', '1d': '1d', '1wk': '1wk', '1mo': '1mo'28 },29}30 31 32# Símbolos disponibles organizados por categoría33AVAILABLE_SYMBOLS: Dict[str, Dict[str, str]] = {34 '🇺🇸 USA - Large Cap (Dow Jones, S&P 500, Nasdaq €30M+ mensual)': {35 'AAPL': 'Apple',36 'MSFT': 'Microsoft',37 'GOOGL': 'Alphabet Google',38 'AMZN': 'Amazon',39 'META': 'Meta',40 'NVDA': 'NVIDIA',41 'TSLA': 'Tesla',42 'BRK.B': 'Berkshire Hathaway',43 'JNJ': 'Johnson & Johnson',44 'V': 'Visa',45 'WMT': 'Walmart',46 'JPM': 'JPMorgan',47 'MCD': 'McDonald\'s',48 'INTC': 'Intel',49 'GOOG': 'Alphabet',50 'NFLX': 'Netflix',51 # 'BRK.B' removed per user request52 'AMD': 'AMD',53 'PYPL': 'PayPal',54 'AVGO': 'Broadcom',55 'ADBE': 'Adobe',56 'CRM': 'Salesforce',57 'ACN': 'Accenture',58 'CSCO': 'Cisco',59 'INTC': 'Intel',60 'QCOM': 'Qualcomm',61 'TXN': 'Texas Instruments',62 'MU': 'Micron',63 'LRCX': 'Lam Research',64 'AMAT': 'Applied Materials',65 'KLAC': 'KLA',66 'MRVL': 'Marvell',67 # 'ANSS' removed per user request68 'ASML': 'ASML Holdings',69 'NXPI': 'NXP',70 'SANM': 'Sanmina',71 'SMCI': 'Super Micro',72 'TSM': 'TSMC',73 'VLO': 'Valero',74 'WMB': 'Williams',75 'OKE': 'ONEOK',76 'KMI': 'Kinder Morgan',77 'SPY': 'S&P 500 ETF',78 'QQQ': 'Nasdaq 100 ETF',79 'DIA': 'Dow Jones ETF',80 'IWM': 'Russell 2000 ETF',81 'EEM': 'Emerging Markets ETF',82 'EFA': 'Int\'l Developed ETF',83 'AGG': 'Bond ETF',84 'BND': 'Total Bond ETF',85 'BAC': 'Bank of America',86 'WFC': 'Wells Fargo',87 'C': 'Citigroup',88 'USB': 'US Bancorp',89 'PNC': 'PNC Financial',90 'TFC': 'Truist',91 'COF': 'Capital One',92 'GS': 'Goldman Sachs',93 'MS': 'Morgan Stanley',94 'BX': 'Blackstone',95 'KKR': 'KKR',96 'APO': 'Apollo',97 'SCHW': 'Schwab',98 'BLK': 'BlackRock',99 'VANG': 'Vanguard',100 'SPLG': 'SPDR Portfolio',101 'XLV': 'Healthcare ETF',102 'XLK': 'Tech ETF',103 'XLY': 'Consumer ETF',104 'XLE': 'Energy ETF',105 'XLRE': 'Real Estate ETF',106 'XLP': 'Staples ETF',107 'XLI': 'Industrial ETF',108 'XLUF': 'Utility ETF',109 'XLNX': 'Xilinx',110 'CRH': 'CRH',111 'NRG': 'NRG Energy',112 'EXC': 'Exelon',113 114 'SO': 'Southern Company',115 'DUK': 'Duke Energy',116 'EDF': 'EDF Energy',117 'EOAN': 'E.ON',118 'RWE': 'RWE',119 'AEP': 'American Electric Power',120 'XEL': 'Xcel Energy',121 'CMS': 'CMS Energy',122 'DTE': 'DTE Energy',123 'AWK': 'American Water',124 'WTR': 'Water Tech',125 'NEE': 'NextEra',126 'LIN': 'Linde',127 'APD': 'Air Products',128 'ECL': 'Ecolab',129 'HLT': 'Hilton',130 'RHT': 'Red Hat',131 'IBM': 'IBM',132 'X': 'US Steel',133 'MT': 'Mittal Steel',134 'CLF': 'Cleveland-Cliffs',135 'RX': 'Rexahn Pharmaceuticals',136 'NRG': 'NRG Energy',137 'EXC': 'Exelon',138 'SO': 'Southern Company',139 'DUK': 'Duke Energy',140 'AEP': 'American Electric Power',141 'XEL': 'Xcel Energy',142 'CMS': 'CMS Energy',143 'DTE': 'DTE Energy',144 'AWK': 'American Water',145 'NEE': 'NextEra Energy',146 'LIN': 'Linde',147 'APD': 'Air Products',148 'ECL': 'Ecolab',149 'HLT': 'Hilton Hotels',150 'MAR': 'Marriott',151 'RCL': 'Royal Caribbean',152 'LUV': 'Southwest Airlines',153 'UAL': 'United Airlines',154 'DAL': 'Delta Air Lines',155 'ALK': 'Alaska Air Group',156 'RBRK': 'Rubrik',157 'ASTS': 'AST SpaceMobile',158 'OKLO': 'Oklo Inc.',159 'NNE': 'Nano Nuclear Energy Inc.',160 'SOUN': 'Soundtrack',161 'SOFI': 'SoFi Technologies',162 'UPST': 'Upstart',163 'DKNG': 'DraftKings',164 'PENN': 'Penn Entertainment',165 'MSGS': 'MSG Sports',166 'MSGS': 'MSG Entertainment',167 'NCLH': 'Norwegian Cruise Line',168 'CCL': 'Carnival Corporation',169 'F': 'Ford',170 'GM': 'General Motors',171 'TM': 'Toyota',172 'HMC': 'Honda',173 'VWAGY': 'Volkswagen',174 'GELYY': 'Geely',175 'NIO': 'NIO',176 'LI': 'Li Auto',177 'XPEV': 'XPeng',178 # 'LEOV' removed per user request179 'TSP': 'Tusimple',180 'PRPL': 'Purple Innovation',181 'BBBY': 'Bed Bath Beyond',182 'PTON': 'Peloton',183 'LULU': 'Lululemon',184 'GIII': 'G-III Apparel',185 'AEO': 'American Eagle',186 'GPS': 'Gap Inc',187 'KSS': 'Kohl\'s',188 'M': 'Macy\'s',189 # 'JWN' removed per user request190 'DDS': 'Dillard\'s',191 'DICT': 'Dictate Inc',192 'DECK': 'Deckers Outdoor',193 'SKX': 'Skechers',194 'CROX': 'Crocs',195 'DNOW': 'DistributionNOW',196 'DXCM': 'Dexcom',197 'INMD': 'Inmode',198 'INTU': 'Intuit',199 'QUAD': 'Quadratec',200 'QUBT': 'Quantum Computing',201 'RIOT': 'Riot Blockchain',202 'MARA': 'Marathon Digital',203 'CLSK': 'CleanSpark',204 'HUT': 'Hut 8 Mining',205 'COIN': 'Coinbase',206 'MSTR': 'Microstrategy',207 'SQ': 'Block Inc',208 'AFRM': 'Affirm',209 # 'IPOE' removed per user request210 'OPEN': 'Opendoor',211 'RBLX': 'Roblox',212 'U': 'Unity Software',213 'TTD': 'Trade Desk',214 'CTV': 'Connectwise',215 'ESTC': 'Elastic',216 'DDOG': 'Datadog',217 'OKTA': 'Okta',218 'NET': 'Cloudflare',219 'FIVN': 'Five9',220 'ZM': 'Zoom',221 'CRWD': 'CrowdStrike',222 'CHKP': 'Check Point',223 224 'CVLT': 'Cavalier Therapeutics',225 'VEEV': 'Veeva Systems',226 227 'NOW': 'ServiceNow',228 'ORCL': 'Oracle',229 'SAP': 'SAP SE',230 'ADSK': 'Autodesk',231 # 'ANSS' removed per user request232 'CADM': 'Cadence',233 'SYNA': 'Synaptics',234 'MANH': 'Manhattan Associates',235 'WDAY': 'Workday',236 'SNOW': 'Snowflake',237 'DBX': 'Dropbox',238 'GDDY': 'GoDaddy',239 'MNST': 'Monster Beverage',240 'KO': 'Coca-Cola',241 'PEP': 'PepsiCo',242 'KMB': 'Kimberly-Clark',243 'CL': 'Colgate-Palmolive',244 'PG': 'Procter & Gamble',245 'EL': 'Estée Lauder',246 'UL': 'Unilever',247 'CLX': 'Clorox',248 'HSII': 'Heidrick & Struggles',249 'CONE': 'Cone Midstream Partners',250 'MO': 'Altria',251 'PM': 'Philip Morris',252 'BTI': 'British American Tobacco',253 'SMPL': 'Sample Holdings',254 'SPLK': 'Splunk',255 'SUMO': 'Sumo Logic',256 'CRSR': 'Corsair',257 258 'RIOT': 'Riot Blockchain',259 'TLRY': 'Tilray',260 'SNDL': 'Sundial Growers',261 'ACB': 'Aurora Cannabis',262 'CGC': 'Canopy Growth',263 'CURLF': 'Curaleaf',264 'TRSSF': 'Trulieve',265 'TCNNF': 'Tetra Bio-Pharma',266 'HITI': 'High Tide',267 'SNDL': 'Sundial Growers',268 'VAPER': 'Vapor Hub',269 'VECO': 'Veeco Instruments',270 'WING': 'Wingman Inc',271 'XBIT': 'Xbit Holdings',272 'YEXT': 'Yext',273 'ZASH': 'Zash Global Media',274 'ZETA': 'Zeta Global',275 'ZIGM': 'Zigmark Holdings',276 'ZION': 'Zion Bancorp',277 },278 '🇪🇸 España': {279 'SAN.MC': 'Banco Santander',280 'BBVA.MC': 'BBVA',281 'IBE.MC': 'Iberdrola',282 'ELE.MC': 'Endesa',283 'TEF.MC': 'Telefónica',284 'ITX.MC': 'Inditex',285 'ACS.MC': 'ACS',286 'REP.MC': 'Repsol',287 'AENA.MC': 'AENA',288 'MAP.MC': 'Mapfre',289 # 'ACE.MC' removed per user request290 'AMS.MC': 'Amstrad',291 'ANA.MC': 'Acciona',292 'ANE.MC': 'Acciona Energía',293 # 'ARCE.MC' removed per user request294 'CLNX.MC': 'Cellnex',295 'COL.MC': 'Colombier',296 'CRIS.MC': 'Cristalería Española',297 'DIA.MC': 'Distribuidora Internacional de Alimentación',298 'ENG.MC': 'Enagás',299 'EVE.MC': 'Evelop Airlines',300 'FCC.MC': 'Fomento de Construcciones',301 'FER.MC': 'Ferrovial',302 'GRF.MC': 'Grifols',303 'IAG.MC': 'International Airlines Group',304 'IDR.MC': 'Indra Sistemas',305 # 'INE.MC' removed per user request306 # 'MAS.MC' removed per user request307 'MEL.MC': 'Meliá Hotels',308 'MDF.MC': 'Modefinance',309 310 311 312 313 'PVA.MC': 'Proa Ventures',314 'RTVE.MC': 'RTVE',315 'SLR.MC': 'Solaria',316 'VIS.MC': 'Vistaprint',317 'CABK.MC': 'CaixaBank',318 'SCYR.MC': 'Scyres',319 'TKFT.MC': 'Tekfor',320 'UNI.MC': 'Unipago',321 },322 '🇩🇪 Alemania - DAX': {323 'SAP.DE': 'SAP',324 'SIE.DE': 'Siemens',325 'ALV.DE': 'Allianz',326 'MUV2.DE': 'Munich Re',327 'LIN.DE': 'Linde',328 'IFX.DE': 'Infineon',329 'DB1.DE': 'Deutsche Börse',330 'RWE.DE': 'RWE',331 'VOW3.DE': 'Volkswagen',332 'BMW.DE': 'BMW',333 # 'BAY.DE' removed per user request334 'BAYN.DE': 'Bayer (Non-Voting)',335 'MRK.DE': 'Merck KGaA',336 'HEI.DE': 'Heidelberg Cement',337 'FRE.DE': 'Fresenius',338 'FME.DE': 'Fresenius Medical Care',339 'HNR1.DE': 'Henkel',340 'ENR.DE': 'Eon SE',341 'EXS1.DE': 'Exel Industries',342 'PUM.DE': 'Puma SE',343 'TKA.DE': 'ThyssenKrupp',344 'LHA.DE': 'Deutsche Lufthansa',345 'VNA.DE': 'Vonovia SE',346 'ZAL.DE': 'Zalando SE',347 'EOAN.DE': 'E.ON SE',348 'DAI.DE': 'Daimler',349 'BMW.DE': 'BMW Group',350 'BAS.DE': 'BASF',351 'CBK.DE': 'Commerzbank',352 'DB.DE': 'Deutsche Bank',353 'DTE.DE': 'Deutsche Telekom',354 'DRI.DE': 'Drägerwerk',355 'EVD.DE': 'Evotec SE',356 'FLT.DE': 'Flextronics',357 'GEI.DE': 'Geberit',358 'GFT.DE': 'GFT Technologies',359 'HAM.DE': 'Hamborner',360 'HAS.DE': 'Hauck + Aufhäuser',361 'HDM.DE': 'Heidelberg Printing',362 'HYQ.DE': 'Hyundai Engineering',363 'IXX.DE': 'Ixx',364 },365 '🇩🇪 Alemania - SDAX (Mid-cap, €30M+ mensual)': {366 'NEM.DE': 'Nemetschek SE',367 368 'SRT.DE': 'Sartorius AG',369 'TAL.DE': 'Talanx AG',370 'VAR1.DE': 'Varta AG',371 'OHB.DE': 'OHB SE',372 'SNG.DE': 'Singulus Technologies',373 'AAD.DE': 'Amadeus Fire',374 'STD.DE': 'Stabilus',375 'KSC.DE': 'K+S AG',376 'MVV1.DE': 'MVV Energie',377 'TDG.DE': 'Telefónica Deutschland',378 'ZAL.DE': 'Zalando SE',379 'MUV2.DE': 'Munich Re',380 'HNR.DE': 'Hannover Re',381 'EOAN.DE': 'E.ON SE',382 'ENR.DE': 'Encavis AG',383 'HEI.DE': 'Heidelberg Materials',384 'VNA.DE': 'Vonovia SE',385 'LEG.DE': 'LEG Immobilien SE',386 'PUM.DE': 'Puma SE',387 'QIA.DE': 'Qiagen NV',388 'RIB.DE': 'RIB Software SE',389 'RUS.DE': 'Rheinmetall AG',390 'SDF.DE': 'Südzucker AG',391 'SYMV.DE': 'Symrise AG',392 'DRI.DE': 'Drägerwerk AG',393 'GFT.DE': 'GFT Technologies',394 'HAM.DE': 'Hamborner REIT',395 },396 '🇩🇪 Alemania - TecDAX (Tech €30M+ mensual)': {397 'IFX.DE': 'Infineon Technologies',398 'SYZ.DE': 'Sartorius Stedim Biotech',399 # 'LEO.DE' removed per user request400 'EVD.DE': 'Evotec SE',401 # 'KBC.DE' removed per user request402 # 'KKC.DE' removed per user request403 'HEX.DE': 'Hecla Mining',404 'VBK.DE': 'Villeroy & Boch',405 },406 '🇩🇪 Alemania - SETRA (Small-cap €30M+ mensual)': {407 'ADS.DE': 'Adidas AG',408 'BAS.DE': 'BASF SE',409 'BMW.DE': 'BMW AG',410 'DBK.DE': 'Deutsche Bank AG',411 'DPW.DE': 'Deutsche Post AG',412 'DTE.DE': 'Deutsche Telekom AG',413 'FRE.DE': 'Fresenius SE & Co. KGaA',414 'FME.DE': 'Fresenius Medical Care AG & Co. KGaA',415 'IFX.DE': 'Infineon Technologies AG',416 'LIN.DE': 'Linde plc',417 'RWE.DE': 'RWE AG',418 'SIE.DE': 'Siemens AG',419 'VOW3.DE': 'Volkswagen AG',420 'VNA.DE': 'Vonovia SE',421 'ZAL.DE': 'Zalando SE',422 'PUM.DE': 'Puma SE',423 'HEI.DE': 'Heidelberg Materials',424 425 'NEM.DE': 'Nemetschek SE',426 'RCG.DE': 'Rational AG',427 'SRT.DE': 'Sartorius AG',428 'TAL.DE': 'Talanx AG',429 'VAR1.DE': 'Varta AG',430 'OHB.DE': 'OHB SE',431 'RIB.DE': 'RIB Software SE',432 'RUS.DE': 'Rheinmetall AG',433 'SDF.DE': 'Südzucker AG',434 'SYMV.DE': 'Symrise AG',435 'NXT.DE': 'Nemetschek (alternate)',436 'CON.DE': 'Continental AG',437 'OTH.DE': 'Other German Smallcap',438 },439 '🇫🇷 Francia - CAC 40': {440 'MC.PA': 'LVMH Moët Hennessy',441 'OR.PA': 'L\'Oréal',442 'TTE.PA': 'TotalEnergies',443 'SAN.PA': 'Sanofi',444 'BNP.PA': 'BNP Paribas',445 'RMS.PA': 'Hermès International',446 'DSY.PA': 'Dassault Systèmes',447 'AIR.PA': 'Airbus',448 'SAF.PA': 'Safran',449 'KER.PA': 'Kering',450 'LR.PA': 'Legrand',451 'SU.PA': 'Schneider Electric',452 'ENGI.PA': 'ENGIE',453 'EDF.PA': 'EDF',454 'CA.PA': 'Carrefour',455 'CAP.PA': 'Capgemini',456 'MT.PA': 'ArcelorMittal',457 'VIE.PA': 'Veolia',458 'FP.PA': 'Accor',459 'VIV.PA': 'Vivendi',460 'SGO.PA': 'Saint-Gobain',461 'RI.PA': 'Pernod Ricard',462 'HO.PA': 'Thales',463 'PUB.PA': 'Publicis Groupe',464 'NOKIA.PA': 'Nokia',465 'ML.PA': 'Michelin',466 'NOKIA.PA': 'Nokia OYJ',467 # 'ASML.PA' removed per user request468 'FR.PA': 'Accor',469 'GLE.PA': 'Société Générale',470 'ACA.PA': 'Crédit Agricole',471 'ORANGE.PA': 'Orange',472 'NOKIA.PA': 'Nokia',473 'UNIBAIL.PA': 'Unibail-Rodamco-Westfield',474 'BOUYGUES.PA': 'Bouygues',475 'NESTLE.PA': 'Nestlé',476 'ROCHE.PA': 'Roche Holding',477 'VALEO.PA': 'Valeo',478 'GDF.PA': 'GDF Suez',479 'RENAULT.PA': 'Renault',480 },481 '🇬🇧 UK': {482 'SHEL.L': 'Shell',483 'BP.L': 'BP',484 'HSBA.L': 'HSBC',485 'GSK.L': 'GSK',486 'AZN.L': 'AstraZeneca',487 'RIO.L': 'Rio Tinto',488 'ULVR.L': 'Unilever',489 'LLOY.L': 'Lloyds Banking Group',490 'VOD.L': 'Vodafone',491 'TSCO.L': 'Tesco',492 'BARC.L': 'Barclays',493 'PRU.L': 'Prudential',494 },495 '🇨🇦 Canadá': {496 'TD.TO': 'Toronto-Dominion Bank',497 'RY.TO': 'Royal Bank of Canada',498 'BNS.TO': 'Bank of Nova Scotia',499 'BCE.TO': 'BCE Inc',500 'ENB.TO': 'Enbridge',501 'SU.TO': 'Suncor Energy',502 'CNQ.TO': 'Canadian Natural Resources',503 'SHOP.TO': 'Shopify',504 'BMO.TO': 'Bank of Montreal',505 },506 '🇦🇺 Australia': {507 'CBA.AX': 'Commonwealth Bank',508 'ANZ.AX': 'ANZ Group',509 'NAB.AX': 'National Australia Bank',510 'WBC.AX': 'Westpac Banking',511 'WES.AX': 'Wesfarmers',512 'CSL.AX': 'CSL Ltd',513 'BHP.AX': 'BHP Group',514 'TLS.AX': 'Telstra',515 'WOW.AX': 'Woolworths Group',516 },517 '🇨🇳 China - ADR': {518 'BABA': 'Alibaba',519 'TCEHY': 'Tencent',520 'BIDU': 'Baidu',521 'JD': 'JD.com',522 'NTES': 'NetEase',523 'XPEV': 'XPeng',524 'NIO': 'NIO',525 'LI': 'Li Auto',526 'PDD': 'Pinduoduo',527 'BILI': 'Bilibili',528 'IQ': 'iQIYI',529 },530}531 532 533def get_all_symbols() -> List[str]:534 """Retorna lista plana de todos los símbolos disponibles"""535 all_symbols = []536 for category_symbols in AVAILABLE_SYMBOLS.values():537 all_symbols.extend(category_symbols.keys())538 return all_symbols539 540 541def get_available_sources_for_symbol(symbol: str) -> List[str]:542 """Retorna las fuentes disponibles para un símbolo específico"""543 return ['yahoo'] # Solo Yahoo Finance disponible544 545 546# Timeframes disponibles547AVAILABLE_TIMEFRAMES = {548 '1m': '1 Minuto',549 '2m': '2 Minutos',550 '5m': '5 Minutos',551 '15m': '15 Minutos',552 '30m': '30 Minutos',553 '1h': '1 Hora',554 '4h': '4 Horas',555 '1d': 'Diario',556 '1wk': 'Semanal',557 '1mo': 'Mensual',558}559 560 561class DataLoader:562 """563 Cargador de datos históricos564 Soporta: Yahoo Finance565 """566 567 def __init__(self):568 self._cache = {}569 570 @staticmethod571 def download(572 symbol: str,573 interval: str = '1d',574 start_date: str = None,575 end_date: str = None,576 source: str = 'yahoo'577 ) -> pd.DataFrame:578 """579 Descarga datos históricos de la fuente especificada580 581 Args:582 symbol: Símbolo del activo (ej: 'BTC-USD', 'AAPL')583 interval: Intervalo de las velas584 start_date: Fecha de inicio (YYYY-MM-DD)585 end_date: Fecha de fin (YYYY-MM-DD)586 source: Fuente de datos ('yahoo', 'binance', 'coinbase', etc.)587 588 Returns:589 DataFrame con columnas OHLCV590 """591 if source == 'yahoo':592 return DataLoader._download_yahoo(symbol, interval, start_date, end_date)593 else:594 st.error(f"Fuente no soportada: {source}")595 return pd.DataFrame()596 597 @staticmethod598 @st.cache_data(ttl=3600)599 def _download_yahoo(600 symbol: str,601 interval: str,602 start_date: str,603 end_date: str604 ) -> pd.DataFrame:605 """Descarga datos de Yahoo Finance"""606 try:607 if interval == '4h':608 df = DataLoader._download_and_resample_4h_yahoo(symbol, start_date, end_date)609 elif interval == '1mo':610 df = DataLoader._download_and_resample_1mo_yahoo(symbol, start_date, end_date)611 else:612 ticker = yf.Ticker(symbol)613 df = ticker.history(start=start_date, end=end_date, interval=interval)614 615 if df.empty:616 return pd.DataFrame()617 618 # Renombrar columnas619 df = df.rename(columns={620 'Open': 'open', 'High': 'high', 'Low': 'low',621 'Close': 'close', 'Volume': 'volume'622 })623 624 cols_to_keep = ['open', 'high', 'low', 'close', 'volume']625 df = df[[col for col in cols_to_keep if col in df.columns]]626 627 # Remover timezone info para evitar problemas en Plotly628 if hasattr(df.index, 'tz') and df.index.tz is not None:629 df.index = df.index.tz_localize(None)630 631 return df632 633 except Exception as e:634 st.error(f"Error Yahoo Finance ({symbol}): {e}")635 return pd.DataFrame()636 637 @staticmethod638 def _download_and_resample_4h_yahoo(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:639 """Descarga datos de 1h y los convierte a 4h"""640 try:641 ticker = yf.Ticker(symbol)642 df = ticker.history(start=start_date, end=end_date, interval='1h')643 644 if df.empty:645 return pd.DataFrame()646 647 df = df.rename(columns={648 'Open': 'open', 'High': 'high', 'Low': 'low',649 'Close': 'close', 'Volume': 'volume'650 })651 652 cols_to_keep = ['open', 'high', 'low', 'close', 'volume']653 df = df[[col for col in cols_to_keep if col in df.columns]]654 655 # Resamplear a 4h656 df_4h = df.resample('4h').agg({657 'open': 'first',658 'high': 'max',659 'low': 'min',660 'close': 'last',661 'volume': 'sum'662 }).dropna()663 664 # Remover timezone info para evitar problemas en Plotly665 if df_4h.index.tz is not None:666 df_4h.index = df_4h.index.tz_localize(None)667 668 return df_4h669 670 except Exception as e:671 st.error(f"Error resampleando 4h: {e}")672 return pd.DataFrame()673 674 @staticmethod675 def _download_and_resample_1mo_yahoo(symbol: str, start_date: str, end_date: str) -> pd.DataFrame:676 """Descarga datos de 1d y los convierte a 1mo (Month End)"""677 try:678 ticker = yf.Ticker(symbol)679 df = ticker.history(start=start_date, end=end_date, interval='1d')680 681 if df.empty:682 return pd.DataFrame()683 684 df = df.rename(columns={685 'Open': 'open', 'High': 'high', 'Low': 'low',686 'Close': 'close', 'Volume': 'volume'687 })688 689 cols_to_keep = ['open', 'high', 'low', 'close', 'volume']690 df = df[[col for col in cols_to_keep if col in df.columns]]691 692 # Resamplear a Month End (ME)693 df_monthly = df.resample('ME').agg({694 'open': 'first',695 'high': 'max',696 'low': 'min',697 'close': 'last',698 'volume': 'sum'699 }).dropna()700 701 # Remover timezone info para evitar problemas en Plotly702 if hasattr(df_monthly.index, 'tz') and df_monthly.index.tz is not None:703 df_monthly.index = df_monthly.index.tz_localize(None)704 705 return df_monthly706 707 except Exception as e:708 st.error(f"Error resampleando 1mo: {e}")709 return pd.DataFrame()710 711 @staticmethod712 @st.cache_data(ttl=3600)713 714 @staticmethod715 def get_available_sources() -> Dict[str, str]:716 """Retorna fuentes disponibles"""717 return {'yahoo': 'Yahoo Finance'}718 719 @staticmethod720 def is_crypto(symbol: str) -> bool:721 """Verifica si el símbolo es criptomoneda"""722 return False # Ya no tenemos criptomonedas en AVAILABLE_SYMBOLS723 