Initial commit: Oil Trading Bot (MT5, WTI)

Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-24 08:29:23 +02:00
co-authored by Claude Opus 4.8
commit 75d28827e8
104 changed files with 21059 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
"""
core/mailer.py — E-Mail via Microsoft Graph (client-credentials)
=================================================================
Sendet HTML-Mails über die Graph-App-Registrierung in `[graph]`
(tenant_id/client_id/client_secret). Absender = `[graph] sender` oder
Default mailagent@hocks.eu. Ersetzt den PowerShell-Umweg (send_daily_report.ps1)
durch reines Python — so kann die Engine den Tagesreport selbst verschicken.
"""
from __future__ import annotations
from core.logger import get_logger
log = get_logger("mail")
_TOKEN_URL = "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
_SENDMAIL = "https://graph.microsoft.com/v1.0/users/{sender}/sendMail"
_DEFAULT_SENDER = "mailagent@hocks.eu"
def send_graph_mail(graph_cfg, subject: str, html: str, to_addr: str) -> bool:
"""True bei Erfolg. `to_addr` darf mehrere Adressen kommagetrennt enthalten.
Fail-safe: loggt + gibt False zurück, wirft nie."""
import requests
g = graph_cfg or {}
tenant = (g.get("tenant_id") or "").strip()
cid = (g.get("client_id") or "").strip()
secret = (g.get("client_secret") or "").strip()
sender = (g.get("sender") or _DEFAULT_SENDER).strip()
recipients = [{"emailAddress": {"address": a.strip()}}
for a in (to_addr or "").split(",") if a.strip()]
if not (tenant and cid and secret) or not recipients:
log.warning("Graph-Config/Empfänger unvollständig — keine E-Mail")
return False
try:
tok = requests.post(
_TOKEN_URL.format(tenant=tenant),
data={"client_id": cid, "client_secret": secret,
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials"}, timeout=30)
tok.raise_for_status()
access = tok.json().get("access_token")
if not access:
log.warning("Graph: kein access_token"); return False
r = requests.post(
_SENDMAIL.format(sender=sender),
headers={"Authorization": "Bearer " + access},
json={"message": {
"subject": subject,
"body": {"contentType": "HTML", "content": html},
"toRecipients": recipients,
"from": {"emailAddress": {"address": sender}}},
"saveToSentItems": True}, timeout=30)
if r.status_code in (200, 202):
log.info(f"E-Mail an {to_addr}: {subject}")
return True
log.warning(f"Graph sendMail {r.status_code}: {r.text[:200]}")
return False
except Exception as e:
log.warning(f"send_graph_mail: {e}")
return False