""" ๐Ÿ“ฐ Aandelen Nieuwsbrief via AI + E-mail (Groq - gratis) --------------------------------------------------------- Haalt elke ochtend om 8u nieuws op over je aandelen, laat AI een samenvatting maken via Groq, en stuurt dit per mail. Toont ook het analistenadvies, de P/E ratio, en 5 AI-gekozen low-risk aandelen. Vereisten: pip install yfinance schedule groq Groq API key aanmaken (gratis): Ga naar https://console.groq.com โ†’ "API Keys" Plak je key hieronder bij GROQ_API_KEY """ import yfinance as yf import schedule import time import smtplib import logging import json import os from groq import Groq from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from datetime import datetime # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ“ LOGBESTAND # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ logging.basicConfig( filename=os.getenv("LOG_FILE", "/data/nieuws_log.txt"), level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%d/%m/%Y %H:%M:%S", encoding="utf-8", ) def log(bericht: str): logging.info(bericht) print(bericht) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # โš™๏ธ CONFIGURATIE โ€” pas dit aan # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "").strip() # verplicht GMAIL_ADRES = os.getenv("GMAIL_ADRES", "").strip() # verplicht GMAIL_APP_WACHTWOORD = os.getenv("GMAIL_APP_WACHTWOORD", "").strip() # verplicht (Google App Password) ONTVANGER = os.getenv("ONTVANGER", "").strip() # verplicht DEFAULT_WATCHLIST = { "HIMS": "Hims & Hers Health", "AMD": "Advanced Micro Devices", "AGNC": "AGNC Investment", "TSLA": "Tesla", "EUNL.DE": "iShares Core MSCI World ETF", "CCEP.AS": "Coca-Cola Europacific Partners", } WATCHLIST_FILE = os.getenv("WATCHLIST_FILE", "").strip() def _normaliseer_watchlist(data) -> dict[str, str]: if isinstance(data, dict): out = {} for k, v in data.items(): ticker = str(k).strip() naam = str(v).strip() if v is not None else "" if not ticker: continue out[ticker] = naam or ticker return out if isinstance(data, list): out = {} for item in data: if not isinstance(item, dict): continue ticker = str(item.get("ticker", "")).strip() naam = str(item.get("naam", "")).strip() if not ticker: continue out[ticker] = naam or ticker return out return {} def laad_watchlist() -> dict[str, str]: if not WATCHLIST_FILE: return DEFAULT_WATCHLIST try: with open(WATCHLIST_FILE, "r", encoding="utf-8") as f: data = json.load(f) wl = _normaliseer_watchlist(data) if wl: return wl log(f"โš ๏ธ WATCHLIST_FILE bevat geen geldige tickers, gebruik DEFAULT_WATCHLIST. Pad: {WATCHLIST_FILE}") return DEFAULT_WATCHLIST except FileNotFoundError: log(f"โš ๏ธ WATCHLIST_FILE niet gevonden, gebruik DEFAULT_WATCHLIST. Pad: {WATCHLIST_FILE}") return DEFAULT_WATCHLIST except Exception as e: log(f"โš ๏ธ Fout bij lezen WATCHLIST_FILE, gebruik DEFAULT_WATCHLIST. Pad: {WATCHLIST_FILE} fout: {e}") return DEFAULT_WATCHLIST WATCHLIST = laad_watchlist() VERSTUUR_OM = os.getenv("VERSTUUR_OM", "08:00").strip() if __name__ != "__main__": # Bij importeren (bv. tests) willen we niet hard falen op ontbrekende envs. pass else: ontbrekend = [naam for naam, waarde in { "GROQ_API_KEY": GROQ_API_KEY, "GMAIL_ADRES": GMAIL_ADRES, "GMAIL_APP_WACHTWOORD": GMAIL_APP_WACHTWOORD, "ONTVANGER": ONTVANGER, }.items() if not waarde] if ontbrekend: raise SystemExit( "Ontbrekende environment variables: " + ", ".join(ontbrekend) + ". Zet ze in je omgeving of via docker-compose (.env)." ) # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ“Š ANALISTENADVIES # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ ADVIES_KLEUREN = { "strong buy": ("#1a7a1a", "๐ŸŸข Strong Buy"), "buy": ("#4caf50", "๐ŸŸฉ Buy"), "hold": ("#f5a623", "๐ŸŸก Hold"), "underperform": ("#e67e22", "๐ŸŸ  Underperform"), "sell": ("#c0392b", "๐Ÿ”ด Sell"), "strong sell": ("#7b0000", "๐Ÿ”ด Strong Sell"), } def haal_advies_op(ticker: str) -> tuple: try: info = yf.Ticker(ticker).info advies = info.get("recommendationKey", "").lower() if advies in ADVIES_KLEUREN: return ADVIES_KLEUREN[advies] return ("#888888", "โšช Geen advies") except: return ("#888888", "โšช Niet beschikbaar") # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ“‰ P/E RATIO # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def haal_pe_op(ticker: str) -> str: try: info = yf.Ticker(ticker).info pe = info.get("trailingPE", None) if pe is None: return 'P/E: niet beschikbaar' if pe < 0: kleur, uitleg = "#c0392b", "verlies" elif pe < 15: kleur, uitleg = "#1a7a1a", "goedkoop" elif pe < 30: kleur, uitleg = "#f5a623", "gemiddeld" else: kleur, uitleg = "#c0392b", "duur" return f'P/E ratio: {pe:.1f} ({uitleg})' except: return 'P/E: niet beschikbaar' # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ“ฐ NIEUWS OPHALEN # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def haal_nieuws_op(ticker: str, max_artikels: int = 5) -> list: try: nieuws = yf.Ticker(ticker).news koppen = [] for artikel in nieuws[:max_artikels]: titel = artikel.get("content", {}).get("title", "") if titel: koppen.append(titel) return koppen except Exception as e: log(f"Fout bij nieuws ophalen voor {ticker}: {e}") return [] # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿค– AI SAMENVATTING # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def maak_samenvatting(naam: str, ticker: str, koppen: list) -> str: if not koppen: return "Geen recent nieuws gevonden." koppen_tekst = "\n".join(f"- {k}" for k in koppen) prompt = f""" Dit zijn recente nieuwskoppen over het aandeel {naam} ({ticker}): {koppen_tekst} Schrijf een korte samenvatting van 2-3 zinnen in het Nederlands. Wees concreet en to-the-point. Geen inleiding of afsluiting nodig. """ client = Groq(api_key=GROQ_API_KEY) response = client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[{"role": "user", "content": prompt}], max_tokens=300, ) return response.choices[0].message.content.strip() # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ’ก AI KIEST 5 LOW-RISK AANDELEN # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def haal_lowrisk_aandelen() -> list: """ Vraagt aan Groq om 5 low-risk aandelen te selecteren. Geeft een lijst van dicts terug: [{ticker, naam, reden}, ...] """ log(" AI selecteert 5 low-risk aandelen...") datum = datetime.now().strftime("%d %B %Y") prompt = f""" Vandaag is het {datum}. Selecteer 5 interessante, low-risk aandelen voor een voorzichtige belegger. Criteria: stabiele bedrijven, voorkeur voor dividend, lage volatiliteit, sterke balans. Denk aan sectoren zoals consumentengoederen, gezondheidszorg, nutsbedrijven, ETFs. Geef je antwoord ALLEEN als een JSON-array, zonder uitleg of extra tekst, in dit formaat: [ {{"ticker": "JNJ", "naam": "Johnson & Johnson", "reden": "Stabiel dividendaandeel met sterke cashflow."}}, {{"ticker": "PG", "naam": "Procter & Gamble", "reden": "Defensief consumentengoederen bedrijf."}}, ... ] """ client = Groq(api_key=GROQ_API_KEY) response = client.chat.completions.create( model="llama-3.3-70b-versatile", messages=[{"role": "user", "content": prompt}], max_tokens=500, ) tekst = response.choices[0].message.content.strip() try: # Verwijder eventuele markdown backticks tekst = tekst.replace("```json", "").replace("```", "").strip() aandelen = json.loads(tekst) return aandelen[:5] except Exception as e: log(f"Fout bij parsen van low-risk aandelen: {e}") return [] def bouw_lowrisk_sectie(aandelen: list) -> str: """Bouwt de HTML-sectie op voor de 5 low-risk aandelen.""" if not aandelen: return "

Kon geen aanbevelingen ophalen vandaag.

" rijen = "" for a in aandelen: ticker = a.get("ticker", "") naam = a.get("naam", ticker) reden = a.get("reden", "") kleur, advies_label = haal_advies_op(ticker) pe_badge = haal_pe_op(ticker) rijen += f"""
{naam} {ticker}
{advies_label}
{pe_badge}

{reden}

""" return f"""

๐Ÿ’ก AI-tip: 5 Low-Risk Aandelen van Vandaag

Dagelijks geselecteerd door AI op basis van stabiliteit en dividendkracht

{rijen}
""" # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿ“ง NIEUWSBRIEF SAMENSTELLEN EN STUREN # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ def stuur_nieuwsbrief(): log("โ”€โ”€ Nieuwsbrief samenstellen โ”€โ”€") datum = datetime.now().strftime("%A %d %B %Y") secties_html = "" for ticker, naam in WATCHLIST.items(): log(f" Verwerken: {naam} ({ticker})") koppen = haal_nieuws_op(ticker) samenvatting = maak_samenvatting(naam, ticker, koppen) kleur, advies_label = haal_advies_op(ticker) pe_badge = haal_pe_op(ticker) secties_html += f"""

{naam}

{ticker}

{advies_label}
{pe_badge}

{samenvatting}

""" # Low-risk sectie lowrisk_aandelen = haal_lowrisk_aandelen() lowrisk_html = bouw_lowrisk_sectie(lowrisk_aandelen) legenda = """
P/E ratio legenda:
โ–  Onder 15 = goedkoop  |  โ–  15โ€“30 = gemiddeld  |  โ–  Boven 30 = duur  |  โ–  Negatief = verlies
""" html = f"""

๐Ÿ“ฐ Jouw Dagelijkse Aandelenupdate

{datum}

{secties_html} {lowrisk_html} {legenda}

Samengevat door Groq AI ยท Analistenadvies & P/E via Yahoo Finance ยท Geen financieel advies

""" onderwerp = f"๐Ÿ“ฐ Aandelenupdate {datetime.now().strftime('%d/%m/%Y')}" bericht = MIMEMultipart("alternative") bericht["Subject"] = onderwerp bericht["From"] = GMAIL_ADRES bericht["To"] = ONTVANGER bericht.attach(MIMEText(html, "html")) with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server: server.login(GMAIL_ADRES, GMAIL_APP_WACHTWOORD) server.sendmail(GMAIL_ADRES, ONTVANGER, bericht.as_string()) log("โœ… Nieuwsbrief verstuurd!") # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ # ๐Ÿš€ STARTEN # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if __name__ == "__main__": log(f"๐Ÿ“ฐ Nieuwsbrief-bot gestart! Verstuurt elke ochtend om {VERSTUUR_OM}.") # Meteen 1x uitvoeren voor de test (verwijder # hieronder om te testen) #stuur_nieuwsbrief() schedule.every().day.at(VERSTUUR_OM).do(stuur_nieuwsbrief) while True: schedule.run_pending() time.sleep(30)