@@ -0,0 +1,380 @@
"""
📰 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 ' <span style= " color:#888; font-size:13px; " >P/E: niet beschikbaar</span> '
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 ' <span style= " font-size:13px; color:#555; " >P/E ratio: <strong style= " color: { kleur } ; " > { pe : .1f } </strong> <span style= " color:#aaa; " >( { uitleg } )</span></span> '
except :
return ' <span style= " color:#888; font-size:13px; " >P/E: niet beschikbaar</span> '
# ─────────────────────────────────────────────
# 📰 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 " <p style= ' color:#888; ' >Kon geen aanbevelingen ophalen vandaag.</p> "
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 """
<div style= " padding:14px 0; border-bottom:1px solid #e0e0e0; " >
<div style= " display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; " >
<div>
<strong style= " color:#1a1a2e; " > { naam } </strong>
<span style= " color:#888; font-size:12px; margin-left:6px; " > { ticker } </span>
</div>
<span style= " background: { kleur } ; color:white; padding:3px 8px; border-radius:10px; font-size:11px; font-weight:bold; " >
{ advies_label }
</span>
</div>
<div style= " margin:4px 0; " > { pe_badge } </div>
<p style= " margin:4px 0 0 0; font-size:13px; color:#555; font-style:italic; " > { reden } </p>
</div>
"""
return f """
<div style= " margin-top:32px; padding:20px; background:#eef6ee; border-left:4px solid #1a7a1a; border-radius:4px; " >
<h2 style= " margin:0 0 4px 0; font-size:16px; color:#1a7a1a; " >💡 AI-tip: 5 Low-Risk Aandelen van Vandaag</h2>
<p style= " margin:0 0 14px 0; font-size:12px; color:#888; " >Dagelijks geselecteerd door AI op basis van stabiliteit en dividendkracht</p>
{ rijen }
</div>
"""
# ─────────────────────────────────────────────
# 📧 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 """
<div style= " margin-bottom:28px; padding:20px; background:#f9f9f9; border-left:4px solid #1a1a2e; border-radius:4px; " >
<div style= " display:flex; justify-content:space-between; align-items:flex-start; flex-wrap:wrap; margin-bottom:8px; " >
<div>
<h2 style= " margin:0 0 2px 0; font-size:16px; color:#1a1a2e; " > { naam } </h2>
<p style= " margin:0; font-size:12px; color:#888; " > { ticker } </p>
</div>
<span style= " background: { kleur } ; color:white; padding:4px 10px; border-radius:12px; font-size:12px; font-weight:bold; white-space:nowrap; " >
{ advies_label }
</span>
</div>
<div style= " margin-bottom:10px; " > { pe_badge } </div>
<p style= " margin:0; font-size:14px; color:#333; line-height:1.6; " > { samenvatting } </p>
</div>
"""
# Low-risk sectie
lowrisk_aandelen = haal_lowrisk_aandelen ( )
lowrisk_html = bouw_lowrisk_sectie ( lowrisk_aandelen )
legenda = """
<div style= " margin-top:24px; padding:16px; background:#f0f0f0; border-radius:4px; font-size:12px; color:#555; " >
<strong>P/E ratio legenda:</strong><br>
<span style= " color:#1a7a1a; " >■</span> Onder 15 = goedkoop |
<span style= " color:#f5a623; " >■</span> 15– 30 = gemiddeld |
<span style= " color:#c0392b; " >■</span> Boven 30 = duur |
<span style= " color:#c0392b; " >■</span> Negatief = verlies
</div>
"""
html = f """
<html><body style= " font-family: Georgia, serif; max-width:600px; margin:auto; padding:20px; color:#222; " >
<div style= " border-bottom:2px solid #1a1a2e; padding-bottom:12px; margin-bottom:24px; " >
<h1 style= " margin:0; font-size:22px; color:#1a1a2e; " >📰 Jouw Dagelijkse Aandelenupdate</h1>
<p style= " margin:4px 0 0 0; font-size:13px; color:#888; " > { datum } </p>
</div>
{ secties_html }
{ lowrisk_html }
{ legenda }
<p style= " font-size:11px; color:#aaa; margin-top:16px; border-top:1px solid #eee; padding-top:12px; " >
Samengevat door Groq AI · Analistenadvies & P/E via Yahoo Finance · Geen financieel advies
</p>
</body></html>
"""
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 )