""" TEFAS Fon Takip Uygulaması - HuggingFace Spaces Orijinal PHP projesinin tam Python versiyonu """ import gradio as gr import json import sys import os import requests from datetime import datetime, timedelta from selenium import webdriver from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from html import escape from bs4 import BeautifulSoup import time import logging # Debug/Logging ayarları logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.StreamHandler(sys.stdout), logging.FileHandler('/tmp/tefas_debug.log', mode='a', encoding='utf-8') ] ) logger = logging.getLogger('TEFAS') # Default config (PHP config.php'den) DEFAULT_CONFIG = { 'email': { 'from_email': 'admin@mersinuroloji.com', 'from_name': 'TEFAS Günlük Rapor', 'to_email': 'mertbasaranoglu@gmail.com', 'to_name': 'Mert Başaranoğlu', 'smtp': { 'host': 'mail.mersinuroloji.com', 'port': 465, 'username': 'admin@mersinuroloji.com', 'password': '1i9XD:wT44EA4[q[', 'encryption': 'ssl', }, 'subject': 'TEFAS Günlük Fon Raporu - {DATE}', }, 'fonlar': ['DSD', 'IUF', 'TP2'], 'euro_api': { 'url': 'https://finans.truncgil.com/today.json', }, 'portfolios': { 'senaryo1': { 'name': 'Portföy 1 (DSD + IUF)', 'positions': [ {'fonKod': 'DSD', 'kurum': 'İŞCEP', 'ilkGirisTarihi': '20.06.2025', 'adet': 1900, 'girisMaliyeti': 100854.44, 'girisOrtalamaMaliyet': 53.081285}, {'fonKod': 'DSD', 'kurum': 'İŞCEP', 'ilkGirisTarihi': '26.06.2025', 'adet': 4428, 'girisMaliyeti': 247897.78, 'girisOrtalamaMaliyet': 55.984141}, {'fonKod': 'IUF', 'kurum': 'İŞCEP', 'ilkGirisTarihi': '26.06.2025', 'adet': 13128, 'girisMaliyeti': 718148.74, 'girisOrtalamaMaliyet': 54.703591}, {'fonKod': 'IUF', 'kurum': 'İŞCEP', 'ilkGirisTarihi': '13.08.2025', 'adet': 257, 'girisMaliyeti': 14160.13, 'girisOrtalamaMaliyet': 55.097782}, ] }, 'senaryo2': { 'name': 'Portföy 2 (TP2)', 'positions': [ {'fonKod': 'TP2', 'kurum': 'İŞCEP', 'ilkGirisTarihi': '10.10.2025', 'adet': 794633.88, 'girisMaliyeti': 1146596.30, 'girisOrtalamaMaliyet': 1.442924}, ] }, } } class TefasHttpFetcher: """HTTP ile TEFAS verilerini çeker (Fallback - Selenium çalışmazsa)""" def fetch_fund_data(self, fund_code): """HTTP ile fon verisi çeker""" url = f"https://www.tefas.gov.tr/FonAnaliz.aspx?FonKod={fund_code}" logger.info(f"[HTTP] Fon verisi çekiliyor: {fund_code}") try: headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' } response = requests.get(url, headers=headers, timeout=30, verify=False) if response.status_code != 200: raise Exception(f"HTTP hatası: {response.status_code}") html = response.text if not html: raise Exception("Boş sayfa döndü") result = self.parse_html(html, fund_code, url) logger.info(f"[HTTP] Fon verisi başarıyla çekildi: {fund_code}") return result except Exception as e: logger.error(f"[HTTP] Fon verisi çekilirken hata: {fund_code} - {str(e)}") return { 'error': True, 'message': str(e), 'fonKod': fund_code, 'tarih': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } def parse_html(self, html, fund_code, url): """HTML içeriğinden veri çıkarır""" soup = BeautifulSoup(html, 'lxml') # Fon adı fon_adi = fund_code try: fon_adi_element = soup.find(id="MainContent_FormViewMainIndicators_LabelFund") if fon_adi_element: fon_adi = fon_adi_element.get_text(strip=True) except: pass # top-list class'ındaki li elementlerini bul son_fiyat = "N/A" gunluk_degisim = "N/A" try: top_list = soup.find('ul', class_='top-list') if top_list: li_elements = top_list.find_all('li') if len(li_elements) > 0: # İlk li: Son Fiyat try: span = li_elements[0].find('span') if span: son_fiyat = span.get_text(strip=True) except: pass if len(li_elements) > 1: # İkinci li: Günlük Getiri try: span = li_elements[1].find('span') if span: gunluk_degisim = span.get_text(strip=True) except: pass except: pass return { 'fonKod': fund_code, 'fonAdi': fon_adi, 'sonFonFiyati': son_fiyat, 'gunlukDegisim': gunluk_degisim, 'tarih': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'url': url, 'error': False } def fetch_multiple_funds(self, fund_codes): """Birden fazla fon için veri çeker""" logger.info(f"[HTTP] Çoklu fon verisi çekiliyor: {fund_codes}") results = [] for fund_code in fund_codes: if fund_code.strip(): data = self.fetch_fund_data(fund_code.strip()) results.append(data) # Rate limit için bekleme time.sleep(2) logger.info(f"[HTTP] Toplam {len(results)} fon verisi çekildi") return results class TefasSeleniumFetcher: def __init__(self): """ChromeDriver ile Selenium başlatır""" self.driver = None self.driver_available = False def setup_driver(self): """Headless Chrome driver'ı yapılandırır""" logger.info("[SELENIUM] ChromeDriver başlatılıyor...") chrome_options = Options() chrome_options.add_argument('--headless=new') chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--disable-software-rasterizer') chrome_options.add_argument('--disable-extensions') chrome_options.add_argument('--disable-setuid-sandbox') chrome_options.add_argument('--window-size=1920,1080') chrome_options.add_argument('--user-agent=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36') # HuggingFace Spaces için Chrome binary path chromium_paths = ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/usr/bin/google-chrome'] for path in chromium_paths: if os.path.exists(path): chrome_options.binary_location = path break # HuggingFace Spaces için ChromeDriver ayarları try: # Önce HuggingFace Spaces'te yüklü olan ChromeDriver'ı kullanmayı dene logger.info("[SELENIUM] ChromeDriver path aranıyor...") import subprocess chromedriver_paths = [ '/usr/bin/chromedriver', '/usr/local/bin/chromedriver', '/snap/bin/chromedriver', ] chromedriver_path = None for path in chromedriver_paths: try: result = subprocess.run(['which', 'chromedriver'], capture_output=True, text=True, timeout=5) if result.returncode == 0: chromedriver_path = result.stdout.strip() logger.info(f"[SELENIUM] ChromeDriver bulundu: {chromedriver_path}") break except Exception as e: logger.debug(f"[SELENIUM] Path kontrol edilemedi: {path} - {str(e)}") # Chromedriver bulunamazsa sistem PATH'inden kullan if chromedriver_path: from selenium.webdriver.chrome.service import Service as ChromeService service = ChromeService(chromedriver_path) self.driver = webdriver.Chrome(service=service, options=chrome_options) self.driver_available = True logger.info("[SELENIUM] ChromeDriver başarıyla başlatıldı (Service ile)") else: # Selenium'un otomatik driver bulmasını dene logger.info("[SELENIUM] Otomatik ChromeDriver deneniyor...") self.driver = webdriver.Chrome(options=chrome_options) self.driver_available = True logger.info("[SELENIUM] ChromeDriver başarıyla başlatıldı (Otomatik)") except Exception as e: logger.warning(f"[SELENIUM] İlk deneme başarısız: {str(e)}") # Fallback: webdriver-manager ile try: logger.info("[SELENIUM] webdriver-manager ile deneniyor...") from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager service = ChromeService(ChromeDriverManager().install()) self.driver = webdriver.Chrome(service=service, options=chrome_options) self.driver_available = True logger.info("[SELENIUM] ChromeDriver başarıyla başlatıldı (webdriver-manager ile)") except Exception as e2: logger.warning(f"[SELENIUM] webdriver-manager başarısız: {str(e2)}") # Son fallback: Service olmadan try: logger.info("[SELENIUM] Son fallback deneniyor...") self.driver = webdriver.Chrome(options=chrome_options) self.driver_available = True logger.info("[SELENIUM] ChromeDriver başarıyla başlatıldı (Son fallback)") except Exception as e3: # Selenium çalışmıyor, HTTP fallback kullanılacak logger.error(f"[SELENIUM] ChromeDriver başlatılamadı, HTTP fallback kullanılacak: {str(e3)}") self.driver_available = False self.driver = None # Exception fırlatma, sadece flag'i False yap def fetch_fund_data(self, fund_code): """Belirli bir fon için veri çeker""" logger.info(f"[SELENIUM] Fon verisi çekiliyor: {fund_code}") if not self.driver_available: if not self.driver: self.setup_driver() if not self.driver_available: logger.warning(f"[SELENIUM] Selenium kullanılamıyor, exception fırlatılıyor") raise Exception("Selenium kullanılamıyor") url = f"https://www.tefas.gov.tr/FonAnaliz.aspx?FonKod={fund_code}" try: logger.debug(f"[SELENIUM] URL açılıyor: {url}") self.driver.get(url) wait = WebDriverWait(self.driver, 10) # Fon adı try: fon_adi_element = wait.until( EC.presence_of_element_located((By.ID, "MainContent_FormViewMainIndicators_LabelFund")) ) fon_adi = fon_adi_element.text.strip() except: fon_adi = fund_code # top-list class'ındaki li elementlerini bul try: top_list = self.driver.find_element(By.CSS_SELECTOR, "ul.top-list") li_elements = top_list.find_elements(By.TAG_NAME, "li") son_fiyat = "N/A" if len(li_elements) > 0: try: span = li_elements[0].find_element(By.TAG_NAME, "span") son_fiyat = span.text.strip() except: pass gunluk_degisim = "N/A" if len(li_elements) > 1: try: span = li_elements[1].find_element(By.TAG_NAME, "span") gunluk_degisim = span.text.strip() except: pass except Exception as e: son_fiyat = "N/A" gunluk_degisim = "N/A" return { 'fonKod': fund_code, 'fonAdi': fon_adi, 'sonFonFiyati': son_fiyat, 'gunlukDegisim': gunluk_degisim, 'tarih': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'url': url, 'error': False } except Exception as e: logger.error(f"[SELENIUM] Fon verisi çekilirken hata: {fund_code} - {str(e)}") return { 'error': True, 'message': str(e), 'fonKod': fund_code, 'tarih': datetime.now().strftime('%Y-%m-%d %H:%M:%S') } def fetch_multiple_funds(self, fund_codes): """Birden fazla fon için veri çeker""" logger.info(f"[SELENIUM] Çoklu fon verisi çekiliyor: {fund_codes}") results = [] for fund_code in fund_codes: if fund_code.strip(): data = self.fetch_fund_data(fund_code.strip()) results.append(data) logger.info(f"[SELENIUM] Toplam {len(results)} fon verisi çekildi") return results def close(self): """Browser'ı kapatır""" if self.driver: self.driver.quit() self.driver = None class PortfolioCalculator: def __init__(self, config): self.config = config self.euroRate = None def fetch_euro_rate(self): """Euro kurunu API'den çeker""" if self.euroRate is not None: return self.euroRate try: url = self.config['euro_api']['url'] response = requests.get(url, timeout=10) data = response.json() if 'EUR' not in data or 'Satış' not in data['EUR']: raise Exception("Euro kuru bulunamadı") # Virgülü nokta ile değiştir ve sayıya çevir self.euroRate = float(str(data['EUR']['Satış']).replace(',', '.')) return self.euroRate except Exception as e: # Fallback: Manuel kur self.euroRate = 48.77 return self.euroRate def calculate_days_difference(self, date_str): """İki tarih arasındaki gün farkını hesaplar""" try: # dd.mm.yyyy formatını parse et parts = date_str.split('.') if len(parts) != 3: return 0 date = datetime(int(parts[2]), int(parts[1]), int(parts[0])) now = datetime.now() diff = now - date return diff.days except Exception as e: return 0 def calculate_portfolio(self, scenario_key, fund_prices): """Portföy senaryosunu hesaplar""" if scenario_key not in self.config['portfolios']: raise Exception(f"Senaryo bulunamadı: {scenario_key}") scenario = self.config['portfolios'][scenario_key] euro_rate = self.fetch_euro_rate() total_giris_maliyeti = 0 total_mevcut_deger = 0 total_gunluk_degisim = 0 position_details = [] for position in scenario['positions']: fon_kod = position['fonKod'] # Güncel fiyatı ve günlük değişimi al guncel_fiyat = 0 gunluk_degisim_yuzde = 0 # fund_prices dict veya list olabilir fund_data = None if isinstance(fund_prices, dict): fund_data = fund_prices.get(fon_kod) else: fund_data = next((f for f in fund_prices if f.get('fonKod') == fon_kod), None) if fund_data and not fund_data.get('error'): if 'sonFonFiyati' in fund_data and fund_data['sonFonFiyati'] and fund_data['sonFonFiyati'] != 'N/A': fiyat_str = str(fund_data['sonFonFiyati']).replace(',', '.') guncel_fiyat = float(fiyat_str) if 'gunlukDegisim' in fund_data and fund_data['gunlukDegisim'] and fund_data['gunlukDegisim'] != 'N/A': degisim_str = str(fund_data['gunlukDegisim']).replace('%', '').replace(',', '.') gunluk_degisim_yuzde = float(degisim_str) # Hesaplamalar giris_maliyeti = position['girisMaliyeti'] adet = position['adet'] mevcut_deger = guncel_fiyat * adet fark = mevcut_deger - giris_maliyeti fark_yuzde = (fark / giris_maliyeti * 100) if giris_maliyeti > 0 else 0 # Kaç gündür ilk_giris_tarihi = position['ilkGirisTarihi'] kac_gundur = self.calculate_days_difference(ilk_giris_tarihi) # Günlük değişim (bugünkü TEFAS verisi) gunluk_degisim_tl = mevcut_deger * (gunluk_degisim_yuzde / 100) # Yıllık getiri yillik_getiri = (fark_yuzde / kac_gundur * 365) if kac_gundur > 0 else 0 total_giris_maliyeti += giris_maliyeti total_mevcut_deger += mevcut_deger total_gunluk_degisim += gunluk_degisim_tl position_details.append({ 'fonKod': fon_kod, 'fonAdi': fund_data.get('fonAdi', fon_kod) if fund_data else fon_kod, 'kurum': position['kurum'], 'ilkGirisTarihi': ilk_giris_tarihi, 'adet': adet, 'girisOrtalamaMaliyet': position['girisOrtalamaMaliyet'], 'girisMaliyeti': giris_maliyeti, 'guncelFiyat': guncel_fiyat, 'mevcutDeger': mevcut_deger, 'mevcutDegerEuro': mevcut_deger / euro_rate if euro_rate > 0 else 0, 'fark': fark, 'farkYuzde': fark_yuzde, 'kacGundur': kac_gundur, 'gunlukDegisimTL': gunluk_degisim_tl, 'gunlukDegisimYuzde': gunluk_degisim_yuzde, 'yillikGetiri': yillik_getiri, }) total_fark = total_mevcut_deger - total_giris_maliyeti total_fark_yuzde = (total_fark / total_giris_maliyeti * 100) if total_giris_maliyeti > 0 else 0 total_gunluk_degisim_yuzde = (total_gunluk_degisim / total_mevcut_deger * 100) if total_mevcut_deger > 0 else 0 return { 'name': scenario['name'], 'euroRate': euro_rate, 'positions': position_details, 'summary': { 'totalGirisMaliyeti': total_giris_maliyeti, 'totalGirisMaliyetiEuro': total_giris_maliyeti / euro_rate if euro_rate > 0 else 0, 'totalMevcutDeger': total_mevcut_deger, 'totalMevcutDegerEuro': total_mevcut_deger / euro_rate if euro_rate > 0 else 0, 'totalFark': total_fark, 'totalFarkYuzde': total_fark_yuzde, 'totalGunlukDegisim': total_gunluk_degisim, 'totalGunlukDegisimYuzde': total_gunluk_degisim_yuzde, } } def calculate_all_portfolios(self, fund_prices): """Tüm portföyleri hesaplar""" results = {} for key in self.config['portfolios']: results[key] = self.calculate_portfolio(key, fund_prices) return results class EmailSender: def __init__(self, config): self.config = config def generate_html_email(self, fon_data_list, portfolios=None): """HTML e-posta içeriği oluşturur""" if portfolios is None: portfolios = {} tarih = datetime.now().strftime('%d.%m.%Y %H:%M:%S') # Convert list to dict if needed fon_dict = {} if isinstance(fon_data_list, list): for item in fon_data_list: if 'fonKod' in item: fon_dict[item['fonKod']] = item else: fon_dict = fon_data_list html = f""" TEFAS Günlük Rapor

📊 TEFAS Günlük Fon Raporu

{tarih}

""" for fon_data in fon_dict.values(): if fon_data.get('error'): html += f"""
{escape(str(fon_data.get('fonKod', '')))}
❌ Hata: {escape(str(fon_data.get('message', 'Bilinmeyen hata')))}
""" else: degisim_class = '' degisim_icon = '' gunluk_degisim = str(fon_data.get('gunlukDegisim', '')) if '-' in gunluk_degisim: degisim_class = 'negative' degisim_icon = '📉' elif '%' in gunluk_degisim and gunluk_degisim != '0%': degisim_class = 'positive' degisim_icon = '📈' html += f"""
{escape(str(fon_data.get('fonAdi', '')))} ({escape(str(fon_data.get('fonKod', '')))})
Son Fon Fiyatı: {escape(str(fon_data.get('sonFonFiyati', 'N/A')))}
Günlük Değişim: {degisim_icon} {escape(gunluk_degisim)}
""" # Portföy analizi ekle if portfolios: html += '
' html += '

📊 Portföy Analizi

' for portfolio_key, portfolio in portfolios.items(): summary = portfolio['summary'] html += f"""

{escape(portfolio['name'])}

Euro Kuru

₺{portfolio['euroRate']:,.4f}

🔥 Günlük Değişim (TL)

₺{summary['totalGunlukDegisim']:,.2f}

🔥 Günlük Değişim (%)

%{summary['totalGunlukDegisimYuzde']:.2f}

""" for pos in portfolio['positions']: fark_color = '#27ae60' if pos['fark'] >= 0 else '#e74c3c' html += f""" """ total_fark_color = '#27ae60' if summary['totalFark'] >= 0 else '#e74c3c' html += f"""
Fon Adet Giriş Maliyet Güncel Fiyat Mevcut Değer Fark Fark % 🔥 Günlük TL 🔥 Günlük % Gün Yıllık %
{escape(pos['fonKod'])} {pos['adet']:,.2f} ₺{pos['girisMaliyeti']:,.2f} {pos['guncelFiyat']:,.6f} ₺{pos['mevcutDeger']:,.2f} ₺{pos['fark']:,.2f} %{pos['farkYuzde']:.2f} ₺{pos['gunlukDegisimTL']:,.2f} %{pos['gunlukDegisimYuzde']:.2f} {pos['kacGundur']:.0f} %{pos['yillikGetiri']:.2f}
TOPLAM ₺{summary['totalGirisMaliyeti']:,.2f}
€{summary['totalGirisMaliyetiEuro']:,.2f}
₺{summary['totalMevcutDeger']:,.2f}
€{summary['totalMevcutDegerEuro']:,.2f}
₺{summary['totalFark']:,.2f} %{summary['totalFarkYuzde']:.2f} ₺{summary['totalGunlukDegisim']:,.2f} %{summary['totalGunlukDegisimYuzde']:.2f}
""" # İki senaryo varsa karşılaştırma ekle if len(portfolios) >= 2: portfolio_keys = list(portfolios.keys()) portfolio1 = portfolios[portfolio_keys[0]] portfolio2 = portfolios[portfolio_keys[1]] mevcut_deger1 = portfolio1['summary']['totalMevcutDeger'] mevcut_deger2 = portfolio2['summary']['totalMevcutDeger'] giris_maliyet1 = portfolio1['summary']['totalGirisMaliyeti'] giris_maliyet2 = portfolio2['summary']['totalGirisMaliyeti'] mevcut_deger_fark = mevcut_deger2 - mevcut_deger1 giris_maliyet_fark = giris_maliyet2 - giris_maliyet1 mevcut_deger_fark_euro = mevcut_deger_fark / portfolio2['euroRate'] if portfolio2['euroRate'] > 0 else 0 gunluk_degisim_fark = portfolio2['summary']['totalGunlukDegisim'] - portfolio1['summary']['totalGunlukDegisim'] gunluk_degisim_fark_yuzde = portfolio2['summary']['totalGunlukDegisimYuzde'] - portfolio1['summary']['totalGunlukDegisimYuzde'] fark_color = '#27ae60' if mevcut_deger_fark >= 0 else '#e74c3c' fark_icon = '▲' if mevcut_deger_fark >= 0 else '▼' gunluk_fark_color = '#27ae60' if gunluk_degisim_fark >= 0 else '#e74c3c' gunluk_fark_icon = '▲' if gunluk_degisim_fark >= 0 else '▼' html += f"""

📊 Senaryolar Arası Karşılaştırma

Giriş Maliyeti Farkı

₺{abs(giris_maliyet_fark):,.2f}

Mevcut Değer Farkı

{fark_icon} ₺{abs(mevcut_deger_fark):,.2f}

€{abs(mevcut_deger_fark_euro):,.2f}

🔥 Günlük Değişim Farkı

{gunluk_fark_icon} ₺{abs(gunluk_degisim_fark):,.2f}

{gunluk_fark_icon} %{abs(gunluk_degisim_fark_yuzde):.2f}

{escape(portfolio2['name'])} portföyü, {escape(portfolio1['name'])} portföyüne göre mevcut değerde {'daha yüksek' if mevcut_deger_fark >= 0 else 'daha düşük'}, günlük değişim de {'daha iyi' if gunluk_degisim_fark >= 0 else 'daha kötü'}.

""" html += """
""" return html def send_report(self, fon_data_list, portfolios=None): """Fon verilerini e-posta ile gönderir""" logger.info("[EMAIL] HTML e-posta içeriği oluşturuluyor...") html_content = self.generate_html_email(fon_data_list, portfolios) logger.info(f"[EMAIL] HTML içeriği oluşturuldu ({len(html_content)} karakter)") try: logger.info("[EMAIL] E-posta mesajı hazırlanıyor...") msg = MIMEMultipart('alternative') subject = self.config['email']['subject'].replace('{DATE}', datetime.now().strftime('%d.%m.%Y')) msg['Subject'] = subject msg['From'] = f"{self.config['email']['from_name']} <{self.config['email']['smtp']['username']}>" msg['To'] = f"{self.config['email']['to_name']} <{self.config['email']['to_email']}>" html_part = MIMEText(html_content, 'html', 'utf-8') msg.attach(html_part) logger.info(f"[EMAIL] Mesaj hazırlandı - Konu: {subject}") # SMTP bağlantısı smtp_config = self.config['email']['smtp'] host = smtp_config['host'] port = smtp_config['port'] username = smtp_config['username'] use_ssl = smtp_config.get('encryption', '').lower() == 'ssl' logger.info(f"[EMAIL] SMTP bağlantısı kuruluyor: {host}:{port} (SSL: {use_ssl})") # Timeout ayarları timeout = 30 # 30 saniye timeout if use_ssl: logger.debug("[EMAIL] SMTP_SSL bağlantısı açılıyor...") server = smtplib.SMTP_SSL(host, port, timeout=timeout) logger.info("[EMAIL] SMTP_SSL bağlantısı başarılı") else: logger.debug("[EMAIL] SMTP bağlantısı açılıyor...") server = smtplib.SMTP(host, port, timeout=timeout) logger.info("[EMAIL] SMTP bağlantısı başarılı, STARTTLS deneniyor...") server.starttls() logger.info("[EMAIL] STARTTLS başarılı") logger.info(f"[EMAIL] SMTP login deneniyor: {username}") server.login(username, smtp_config['password']) logger.info("[EMAIL] SMTP login başarılı") to_email = self.config['email']['to_email'] logger.info(f"[EMAIL] E-posta gönderiliyor: {to_email}") server.sendmail(username, to_email, msg.as_string()) logger.info("[EMAIL] E-posta başarıyla gönderildi") logger.info("[EMAIL] SMTP bağlantısı kapatılıyor...") server.quit() logger.info("[EMAIL] SMTP bağlantısı kapatıldı") return {'success': True, 'message': 'E-posta başarıyla gönderildi'} except TimeoutError as e: logger.error(f"[EMAIL] SMTP timeout hatası: {str(e)}") return {'success': False, 'message': f'E-posta gönderilemedi: Timeout (30 saniye içinde bağlanılamadı) - HuggingFace Spaces\'ten dış SMTP erişimi engellenmiş olabilir'} except OSError as e: if 'timed out' in str(e).lower() or 'timeout' in str(e).lower(): logger.error(f"[EMAIL] SMTP timeout hatası: {str(e)}") return {'success': False, 'message': f'E-posta gönderilemedi: Timeout - HuggingFace Spaces\'ten dış SMTP erişimi engellenmiş olabilir. Lütfen local ortamda çalıştırın.'} else: logger.error(f"[EMAIL] SMTP network hatası: {str(e)}") return {'success': False, 'message': f'E-posta gönderilemedi: Ağ hatası - {str(e)}'} except smtplib.SMTPAuthenticationError as e: logger.error(f"[EMAIL] SMTP authentication hatası: {str(e)}") return {'success': False, 'message': f'E-posta gönderilemedi: Kimlik doğrulama hatası - {str(e)}'} except smtplib.SMTPException as e: logger.error(f"[EMAIL] SMTP hatası: {str(e)}") return {'success': False, 'message': f'E-posta gönderilemedi: SMTP hatası - {str(e)}'} except Exception as e: logger.error(f"[EMAIL] Genel hata: {str(e)}", exc_info=True) return {'success': False, 'message': f'E-posta gönderilemedi: {str(e)}'} # Global instances - Fallback mekanizması selenium_fetcher = None http_fetcher = TefasHttpFetcher() def get_fetcher(): """Selenium varsa onu kullan, yoksa HTTP fallback kullan""" global selenium_fetcher logger.info("[FETCHER] Fetcher seçiliyor...") if selenium_fetcher is None: try: logger.info("[FETCHER] Selenium fetcher oluşturuluyor...") selenium_fetcher = TefasSeleniumFetcher() selenium_fetcher.setup_driver() if selenium_fetcher.driver_available: logger.info("[FETCHER] Selenium fetcher kullanılacak") return selenium_fetcher else: logger.info("[FETCHER] Selenium kullanılamadı, HTTP fetcher kullanılacak") return http_fetcher except Exception as e: # Selenium çalışmıyor, HTTP kullan logger.warning(f"[FETCHER] Selenium başlatılamadı, HTTP kullanılacak: {str(e)}") return http_fetcher if selenium_fetcher.driver_available: logger.debug("[FETCHER] Mevcut Selenium fetcher kullanılıyor") return selenium_fetcher else: logger.debug("[FETCHER] HTTP fetcher kullanılıyor") return http_fetcher def run_full_report(send_email=True): """Tam rapor çalıştırır (PHP tefas_daily_report.php'nin aynısı)""" logger.info("="*50) logger.info("[REPORT] Tam rapor başlatılıyor...") try: config = DEFAULT_CONFIG logger.info(f"[REPORT] Config yüklendi, {len(config['fonlar'])} fon takip ediliyor") # Fon verilerini çek (Selenium varsa onu kullan, yoksa HTTP) fund_codes = config['fonlar'] try: fetcher = get_fetcher() logger.info(f"[REPORT] Fetcher seçildi: {type(fetcher).__name__}") fon_data_list = fetcher.fetch_multiple_funds(fund_codes) logger.info(f"[REPORT] {len(fon_data_list)} fon verisi çekildi") except Exception as e: # Selenium hatası, HTTP fallback kullan logger.warning(f"[REPORT] Selenium hatası, HTTP fallback deneniyor: {str(e)}") try: fon_data_list = http_fetcher.fetch_multiple_funds(fund_codes) logger.info(f"[REPORT] HTTP fallback başarılı, {len(fon_data_list)} fon verisi çekildi") except Exception as e2: logger.error(f"[REPORT] HTTP fallback de başarısız: {str(e2)}") return f"
Veri çekilemedi!

Selenium hatası: {escape(str(e))}
HTTP hatası: {escape(str(e2))}
" # Dict'e çevir fon_dict = {} for item in fon_data_list: fon_dict[item['fonKod']] = item # Portföy hesaplamalarını yap logger.info("[REPORT] Portföy hesaplamaları yapılıyor...") portfolio_calc = PortfolioCalculator(config) portfolios = portfolio_calc.calculate_all_portfolios(fon_dict) logger.info(f"[REPORT] {len(portfolios)} portföy hesaplandı") # Sonuçları HTML olarak oluştur email_sender = EmailSender(config) html_content = email_sender.generate_html_email(fon_dict, portfolios) # E-posta göndermeyi dene (opsiyonel) email_result = None if send_email: logger.info(f"[REPORT] E-posta gönderiliyor: {config['email']['to_email']}") try: email_result = email_sender.send_report(fon_dict, portfolios) if email_result['success']: logger.info("[REPORT] E-posta başarıyla gönderildi") else: logger.warning(f"[REPORT] E-posta gönderilemedi: {email_result['message']}") except Exception as e: logger.warning(f"[REPORT] E-posta gönderme hatası (devam ediliyor): {str(e)}") email_result = {'success': False, 'message': str(e)} # Sonuç HTML'ine e-posta durumunu ekle basarili = sum(1 for f in fon_data_list if not f.get('error')) hatali = sum(1 for f in fon_data_list if f.get('error')) status_html = f"""

📊 Rapor Özeti

Başarılı fon: {basarili} | Hatalı fon: {hatali}

Portföy sayısı: {len(portfolios)}

{f"

✅ E-posta gönderildi: {config['email']['to_email']}

" if send_email and email_result and email_result.get('success') else ""} {f"

⚠️ E-posta gönderilemedi: {escape(email_result.get('message', 'Bilinmeyen hata') if email_result else 'E-posta gönderilmedi')}
HuggingFace Spaces dış SMTP bağlantılarını engelliyor olabilir. Sonuçlar aşağıda görüntülenebilir.

" if send_email and (not email_result or not email_result.get('success')) else ""} {f"

E-posta gönderilmedi (opsiyonel kapatıldı)

" if not send_email else ""}
""" # HTML içeriğini status ile birleştir final_html = status_html + html_content return final_html except Exception as e: logger.error(f"[REPORT] Genel hata: {str(e)}", exc_info=True) return f"
Hata oluştu:

{escape(str(e))}
" def fetch_fund(fund_code): """Tek bir fon için veri çeker ve formatlar""" logger.info(f"[FUND] Tek fon sorgusu: {fund_code}") if not fund_code or not fund_code.strip(): return "Lütfen bir fon kodu girin (örn: DSD, IUF, TP2)" try: # Selenium varsa onu kullan, yoksa HTTP fetcher = get_fetcher() logger.debug(f"[FUND] Fetcher: {type(fetcher).__name__}") try: data = fetcher.fetch_fund_data(fund_code.strip()) logger.debug(f"[FUND] Veri başarıyla çekildi: {fund_code}") except Exception as e: # Selenium çalışmıyorsa HTTP fallback logger.warning(f"[FUND] Selenium başarısız, HTTP fallback: {str(e)}") data = http_fetcher.fetch_fund_data(fund_code.strip()) if data.get('error'): return f"❌ Hata: {data.get('message', 'Bilinmeyen hata')}" result = f"""

📊 {data['fonAdi']}

Fon Kodu: {data['fonKod']}


Son Fon Fiyatı: {data['sonFonFiyati']}

Günlük Değişim: {data['gunlukDegisim']}

📅 {data['tarih']}

🔗 TEFAS'ta Görüntüle →

""" return result except Exception as e: return f"❌ Hata oluştu: {str(e)}" # Gradio Interface with gr.Blocks(title="TEFAS Fon Takip", theme=gr.themes.Soft()) as app: gr.Markdown(""" # 📊 TEFAS Fon Takip Uygulaması Bu uygulama, TEFAS (Türkiye Elektronik Fon Alım Satım Platformu) web sitesinden fon bilgilerini otomatik olarak çekerek güncel verileri gösterir ve e-posta ile rapor gönderir. """) with gr.Tabs(): with gr.Tab("🚀 Tam Rapor"): gr.Markdown("### Tüm fonları çekip, portföy analizi yapıp rapor oluşturur") with gr.Row(): send_email_checkbox = gr.Checkbox( label="E-posta Gönder (Opsiyonel - HuggingFace Spaces'te çalışmayabilir)", value=False, info="E-posta göndermeyi denemek için işaretleyin. HuggingFace Spaces dış SMTP bağlantılarını engelliyor olabilir." ) btn_full_report = gr.Button("📊 Tam Rapor Oluştur", variant="primary", size="lg") output_full_report = gr.HTML(label="Rapor") def run_report_with_email(send_email): return run_full_report(send_email=send_email) btn_full_report.click( fn=run_report_with_email, inputs=send_email_checkbox, outputs=output_full_report ) with gr.Tab("🔍 Tek Fon Sorgula"): fund_input_single = gr.Textbox( label="Fon Kodu", placeholder="DSD, IUF, TP2 gibi fon kodunu girin", value="DSD" ) btn_single = gr.Button("Fon Bilgisini Getir", variant="primary") output_single = gr.HTML(label="Sonuç") btn_single.click(fn=fetch_fund, inputs=fund_input_single, outputs=output_single) fund_input_single.submit(fn=fetch_fund, inputs=fund_input_single, outputs=output_single) with gr.Tab("ℹ️ Hakkında"): gr.Markdown(""" ## TEFAS Fon Takip Uygulaması Bu uygulama, orijinal PHP projesinin tam Python versiyonudur. ### Özellikler: - ✅ Headless Chrome Selenium ile güvenilir veri çekme - ✅ TEFAS web sitesinden otomatik veri çekme - ✅ Birden fazla fonu aynı anda takip etme - ✅ Portföy analizi ve hesaplamaları - ✅ HTML e-posta raporu gönderme - ✅ Euro kuru entegrasyonu ### Kullanım: - **Tam Rapor**: Tüm fonları çeker, portföy analizi yapar ve e-posta gönderir - **Tek Fon Sorgula**: Tek bir fon için güncel bilgileri gösterir """) if __name__ == "__main__": app.launch(server_name="0.0.0.0", server_port=7860, share=False)