"""
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
"""
for fon_data in fon_dict.values():
if fon_data.get('error'):
html += f"""
❌ 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"""
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}
| Fon |
Adet |
Giriş Maliyet |
Güncel Fiyat |
Mevcut Değer |
Fark |
Fark % |
🔥 Günlük TL |
🔥 Günlük % |
Gün |
Yıllık % |
"""
for pos in portfolio['positions']:
fark_color = '#27ae60' if pos['fark'] >= 0 else '#e74c3c'
html += f"""
| {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}
|
"""
total_fark_color = '#27ae60' if summary['totalFark'] >= 0 else '#e74c3c'
html += f"""
| 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)