Spaces:
Sleeping
Sleeping
Make email optional and show results as HTML in Gradio - solves HuggingFace Spaces SMTP timeout issue
0a5b9f7
| """ | |
| 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"""<!DOCTYPE html> | |
| <html lang="tr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>TEFAS Günlük Rapor</title> | |
| <style> | |
| body {{ font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 20px; }} | |
| .container {{ max-width: 800px; margin: 0 auto; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden; }} | |
| .header {{ background-color: #2c3e50; color: #ffffff; padding: 20px; text-align: center; }} | |
| .header h1 {{ margin: 0; font-size: 24px; }} | |
| .header p {{ margin: 5px 0 0 0; font-size: 14px; opacity: 0.9; }} | |
| .content {{ padding: 20px; }} | |
| .fon-item {{ border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 15px; padding: 15px; background-color: #fafafa; }} | |
| .fon-header {{ font-size: 18px; font-weight: bold; color: #2c3e50; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #3498db; }} | |
| .fon-detail {{ display: flex; justify-content: space-between; align-items: center; margin: 8px 0; padding: 8px; background-color: #ffffff; border-radius: 4px; }} | |
| .detail-label {{ font-weight: bold; color: #555; }} | |
| .detail-value {{ color: #2c3e50; font-size: 16px; }} | |
| .positive {{ color: #27ae60; font-weight: bold; }} | |
| .negative {{ color: #e74c3c; font-weight: bold; }} | |
| .error-item {{ background-color: #ffe6e6; border: 1px solid #ffcccc; padding: 15px; border-radius: 6px; margin-bottom: 15px; }} | |
| .error-message {{ color: #c0392b; font-weight: bold; }} | |
| .footer {{ background-color: #ecf0f1; padding: 15px; text-align: center; font-size: 12px; color: #7f8c8d; }} | |
| .link {{ color: #3498db; text-decoration: none; }} | |
| .link:hover {{ text-decoration: underline; }} | |
| table {{ width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 12px; }} | |
| th, td {{ padding: 8px; text-align: right; border: 1px solid #ddd; }} | |
| th {{ background: #34495e; color: white; text-align: left; }} | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>📊 TEFAS Günlük Fon Raporu</h1> | |
| <p>{tarih}</p> | |
| </div> | |
| <div class="content">""" | |
| for fon_data in fon_dict.values(): | |
| if fon_data.get('error'): | |
| html += f""" | |
| <div class="error-item"> | |
| <div class="fon-header">{escape(str(fon_data.get('fonKod', '')))}</div> | |
| <div class="error-message">❌ Hata: {escape(str(fon_data.get('message', 'Bilinmeyen hata')))}</div> | |
| </div>""" | |
| 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""" | |
| <div class="fon-item"> | |
| <div class="fon-header">{escape(str(fon_data.get('fonAdi', '')))} ({escape(str(fon_data.get('fonKod', '')))})</div> | |
| <div class="fon-detail"> | |
| <span class="detail-label">Son Fon Fiyatı:</span> | |
| <span class="detail-value">{escape(str(fon_data.get('sonFonFiyati', 'N/A')))}</span> | |
| </div> | |
| <div class="fon-detail"> | |
| <span class="detail-label">Günlük Değişim:</span> | |
| <span class="detail-value {degisim_class}">{degisim_icon} {escape(gunluk_degisim)}</span> | |
| </div> | |
| <div class="fon-detail"> | |
| <span class="detail-label">Detay:</span> | |
| <span class="detail-value"><a href="{escape(fon_data.get('url', ''))}" class="link" target="_blank">TEFAS'ta Görüntüle</a></span> | |
| </div> | |
| </div>""" | |
| # Portföy analizi ekle | |
| if portfolios: | |
| html += '<hr style="margin: 30px 0; border: none; border-top: 2px solid #ecf0f1;">' | |
| html += '<h2 style="color: #2c3e50; margin: 20px 0;">📊 Portföy Analizi</h2>' | |
| for portfolio_key, portfolio in portfolios.items(): | |
| summary = portfolio['summary'] | |
| html += f""" | |
| <div style="background: linear-gradient(135deg, #3498db 0%, #2980b9 100%); color: white; padding: 20px; margin: 20px 0; border-radius: 8px;"> | |
| <h3 style="margin: 0 0 15px 0; color: white; font-size: 20px;">{escape(portfolio['name'])}</h3> | |
| <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 10px;"> | |
| <div> | |
| <p style="margin: 0; font-size: 12px; opacity: 0.9;">Euro Kuru</p> | |
| <p style="margin: 5px 0 0 0; font-size: 16px; font-weight: bold;">₺{portfolio['euroRate']:,.4f}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 5px;"> | |
| <p style="margin: 0; font-size: 12px; opacity: 0.9;">🔥 Günlük Değişim (TL)</p> | |
| <p style="margin: 5px 0 0 0; font-size: 18px; font-weight: bold;">₺{summary['totalGunlukDegisim']:,.2f}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.2); padding: 10px; border-radius: 5px;"> | |
| <p style="margin: 0; font-size: 12px; opacity: 0.9;">🔥 Günlük Değişim (%)</p> | |
| <p style="margin: 5px 0 0 0; font-size: 18px; font-weight: bold;">%{summary['totalGunlukDegisimYuzde']:.2f}</p> | |
| </div> | |
| </div> | |
| </div> | |
| <table> | |
| <thead> | |
| <tr> | |
| <th>Fon</th> | |
| <th>Adet</th> | |
| <th>Giriş Maliyet</th> | |
| <th>Güncel Fiyat</th> | |
| <th>Mevcut Değer</th> | |
| <th>Fark</th> | |
| <th>Fark %</th> | |
| <th style="background: #f39c12;">🔥 Günlük TL</th> | |
| <th style="background: #e67e22;">🔥 Günlük %</th> | |
| <th>Gün</th> | |
| <th>Yıllık %</th> | |
| </tr> | |
| </thead> | |
| <tbody>""" | |
| for pos in portfolio['positions']: | |
| fark_color = '#27ae60' if pos['fark'] >= 0 else '#e74c3c' | |
| html += f""" | |
| <tr style="background: white;"> | |
| <td><strong>{escape(pos['fonKod'])}</strong></td> | |
| <td>{pos['adet']:,.2f}</td> | |
| <td>₺{pos['girisMaliyeti']:,.2f}</td> | |
| <td>{pos['guncelFiyat']:,.6f}</td> | |
| <td>₺{pos['mevcutDeger']:,.2f}</td> | |
| <td style="color: {fark_color}; font-weight: bold;">₺{pos['fark']:,.2f}</td> | |
| <td style="color: {fark_color}; font-weight: bold;">%{pos['farkYuzde']:.2f}</td> | |
| <td style="background: #fff3cd; font-weight: bold; color: {fark_color};"> | |
| ₺{pos['gunlukDegisimTL']:,.2f} | |
| </td> | |
| <td style="background: #ffe0b3; font-weight: bold; color: {fark_color};"> | |
| %{pos['gunlukDegisimYuzde']:.2f} | |
| </td> | |
| <td>{pos['kacGundur']:.0f}</td> | |
| <td style="color: {fark_color};"> | |
| %{pos['yillikGetiri']:.2f} | |
| </td> | |
| </tr>""" | |
| total_fark_color = '#27ae60' if summary['totalFark'] >= 0 else '#e74c3c' | |
| html += f""" | |
| <tr style="background: #ecf0f1; font-weight: bold; font-size: 14px;"> | |
| <td colspan="2">TOPLAM</td> | |
| <td>₺{summary['totalGirisMaliyeti']:,.2f}<br><small>€{summary['totalGirisMaliyetiEuro']:,.2f}</small></td> | |
| <td></td> | |
| <td>₺{summary['totalMevcutDeger']:,.2f}<br><small>€{summary['totalMevcutDegerEuro']:,.2f}</small></td> | |
| <td style="color: {total_fark_color};">₺{summary['totalFark']:,.2f}</td> | |
| <td style="color: {total_fark_color};">%{summary['totalFarkYuzde']:.2f}</td> | |
| <td style="background: #ffc107; color: {total_fark_color}; font-size: 16px;"> | |
| ₺{summary['totalGunlukDegisim']:,.2f} | |
| </td> | |
| <td style="background: #ff9800; color: {total_fark_color}; font-size: 16px;"> | |
| %{summary['totalGunlukDegisimYuzde']:.2f} | |
| </td> | |
| <td colspan="2"></td> | |
| </tr> | |
| </tbody> | |
| </table>""" | |
| # İ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""" | |
| <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; margin: 20px 0; border-radius: 8px;"> | |
| <h3 style="margin: 0 0 15px 0; color: white;">📊 Senaryolar Arası Karşılaştırma</h3> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 15px; margin-bottom: 15px;"> | |
| <div style="background: rgba(255,255,255,0.1); padding: 15px; border-radius: 5px;"> | |
| <p style="margin: 0 0 5px 0; font-size: 13px; opacity: 0.9;">Giriş Maliyeti Farkı</p> | |
| <p style="margin: 0; font-size: 18px; font-weight: bold;">₺{abs(giris_maliyet_fark):,.2f}</p> | |
| </div> | |
| <div style="background: rgba(255,255,255,0.15); padding: 15px; border-radius: 5px;"> | |
| <p style="margin: 0 0 5px 0; font-size: 13px; opacity: 0.9;">Mevcut Değer Farkı</p> | |
| <p style="margin: 0; font-size: 22px; font-weight: bold;">{fark_icon} ₺{abs(mevcut_deger_fark):,.2f}</p> | |
| <p style="margin: 5px 0 0 0; font-size: 14px; opacity: 0.9;">€{abs(mevcut_deger_fark_euro):,.2f}</p> | |
| </div> | |
| <div style="background: rgba(255,193,7,0.3); padding: 15px; border-radius: 5px; border: 2px solid rgba(255,193,7,0.5);"> | |
| <p style="margin: 0 0 5px 0; font-size: 13px; opacity: 0.9;">🔥 Günlük Değişim Farkı</p> | |
| <p style="margin: 0; font-size: 22px; font-weight: bold;">{gunluk_fark_icon} ₺{abs(gunluk_degisim_fark):,.2f}</p> | |
| <p style="margin: 5px 0 0 0; font-size: 16px; opacity: 0.9;">{gunluk_fark_icon} %{abs(gunluk_degisim_fark_yuzde):.2f}</p> | |
| </div> | |
| </div> | |
| <p style="margin: 15px 0 0 0; font-size: 14px; opacity: 0.95; background: rgba(255,255,255,0.1); padding: 12px; border-radius: 5px;"> | |
| <strong>{escape(portfolio2['name'])}</strong> portföyü, | |
| <strong>{escape(portfolio1['name'])}</strong> portföyüne göre | |
| mevcut değerde <span style="font-size: 16px; font-weight: bold;">{'daha yüksek' if mevcut_deger_fark >= 0 else 'daha düşük'}</span>, | |
| günlük değişim de <span style="font-size: 16px; font-weight: bold;">{'daha iyi' if gunluk_degisim_fark >= 0 else 'daha kötü'}</span>. | |
| </p> | |
| </div>""" | |
| html += """ | |
| </div> | |
| <div class="footer"> | |
| <p>Bu rapor otomatik olarak oluşturulmuştur.</p> | |
| <p><a href="https://www.tefas.gov.tr" class="link" target="_blank">www.tefas.gov.tr</a></p> | |
| </div> | |
| </div> | |
| </body> | |
| </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"<div style='color: #e74c3c; padding: 20px; background: #ffe6e6; border-radius: 8px;'>❌ <strong>Veri çekilemedi!</strong><br><br>Selenium hatası: {escape(str(e))}<br>HTTP hatası: {escape(str(e2))}</div>" | |
| # 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""" | |
| <div style="background: #ecf0f1; padding: 15px; margin: 20px 0; border-radius: 8px; border-left: 4px solid #3498db;"> | |
| <h3 style="margin-top: 0; color: #2c3e50;">📊 Rapor Özeti</h3> | |
| <p><strong>Başarılı fon:</strong> {basarili} | <strong>Hatalı fon:</strong> {hatali}</p> | |
| <p><strong>Portföy sayısı:</strong> {len(portfolios)}</p> | |
| {f"<p style='color: #27ae60;'><strong>✅ E-posta gönderildi:</strong> {config['email']['to_email']}</p>" if send_email and email_result and email_result.get('success') else ""} | |
| {f"<p style='color: #e74c3c;'><strong>⚠️ E-posta gönderilemedi:</strong> {escape(email_result.get('message', 'Bilinmeyen hata') if email_result else 'E-posta gönderilmedi')}<br><small>HuggingFace Spaces dış SMTP bağlantılarını engelliyor olabilir. Sonuçlar aşağıda görüntülenebilir.</small></p>" if send_email and (not email_result or not email_result.get('success')) else ""} | |
| {f"<p style='color: #7f8c8d;'><small>E-posta gönderilmedi (opsiyonel kapatıldı)</small></p>" if not send_email else ""} | |
| </div> | |
| """ | |
| # 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"<div style='color: #e74c3c; padding: 20px; background: #ffe6e6; border-radius: 8px;'>❌ <strong>Hata oluştu:</strong><br><br>{escape(str(e))}</div>" | |
| 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""" | |
| <div style="font-family: Arial, sans-serif; padding: 20px; border: 2px solid #3498db; border-radius: 8px; background: #f8f9fa;"> | |
| <h2 style="color: #2c3e50; margin-top: 0;">📊 {data['fonAdi']}</h2> | |
| <p style="font-size: 14px; color: #7f8c8d;">Fon Kodu: <strong>{data['fonKod']}</strong></p> | |
| <hr style="border: 1px solid #ecf0f1;"> | |
| <div style="margin: 15px 0;"> | |
| <p style="font-size: 18px; margin: 10px 0;"> | |
| <span style="color: #555;">Son Fon Fiyatı:</span> | |
| <strong style="color: #2c3e50; font-size: 22px;">{data['sonFonFiyati']}</strong> | |
| </p> | |
| <p style="font-size: 18px; margin: 10px 0;"> | |
| <span style="color: #555;">Günlük Değişim:</span> | |
| <strong style="color: {'#27ae60' if '%' in data['gunlukDegisim'] and '-' not in data['gunlukDegisim'] else '#e74c3c' if '-' in data['gunlukDegisim'] else '#2c3e50'}; font-size: 22px;"> | |
| {data['gunlukDegisim']} | |
| </strong> | |
| </p> | |
| <p style="font-size: 14px; color: #7f8c8d; margin-top: 15px;"> | |
| 📅 {data['tarih']} | |
| </p> | |
| <p style="margin-top: 15px;"> | |
| <a href="{data['url']}" target="_blank" style="color: #3498db; text-decoration: none;"> | |
| 🔗 TEFAS'ta Görüntüle → | |
| </a> | |
| </p> | |
| </div> | |
| </div> | |
| """ | |
| 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) |