""" ๐ฐ 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"""{reden}
Dagelijks geselecteerd door AI op basis van stabiliteit en dividendkracht
{rijen}{ticker}
{samenvatting}
{datum}
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)