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:
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
scripts/daily_summary.py — Tages-Zusammenfassung für E-Mail
|
||||
============================================================
|
||||
Gibt eine HTML-Zusammenfassung aller Trades des gestrigen Tages aus.
|
||||
Aufruf: python daily_summary.py [YYYY-MM-DD]
|
||||
"""
|
||||
|
||||
import sys
|
||||
import sqlite3
|
||||
import datetime
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = Path(__file__).parent.parent / "oil_widget_history.db"
|
||||
|
||||
def run(target_date: datetime.date) -> str:
|
||||
ts_start = int(datetime.datetime(target_date.year, target_date.month, target_date.day, 0, 0, 0).timestamp())
|
||||
ts_end = ts_start + 86400
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
c = conn.cursor()
|
||||
|
||||
c.execute("""
|
||||
SELECT setup, direction, pnl, closed_by, exit_time, entry_price, exit_price
|
||||
FROM trades
|
||||
WHERE exit_time >= ? AND exit_time < ? AND exit_time IS NOT NULL
|
||||
ORDER BY exit_time ASC
|
||||
""", (ts_start, ts_end))
|
||||
trades = [dict(r) for r in c.fetchall()]
|
||||
conn.close()
|
||||
|
||||
if not trades:
|
||||
return f"<p>Keine Trades am {target_date.strftime('%d.%m.%Y')}.</p>"
|
||||
|
||||
total = len(trades)
|
||||
wins = sum(1 for t in trades if (t["pnl"] or 0) > 0)
|
||||
pnl_sum = sum(t["pnl"] or 0 for t in trades)
|
||||
wr = wins / total * 100
|
||||
|
||||
# Pro Setup
|
||||
setups: dict[str, dict] = {}
|
||||
for t in trades:
|
||||
s = t["setup"] or "NO_SETUP"
|
||||
if s not in setups:
|
||||
setups[s] = {"n": 0, "wins": 0, "pnl": 0.0}
|
||||
setups[s]["n"] += 1
|
||||
setups[s]["wins"] += 1 if (t["pnl"] or 0) > 0 else 0
|
||||
setups[s]["pnl"] += t["pnl"] or 0
|
||||
|
||||
best = max(trades, key=lambda t: t["pnl"] or 0)
|
||||
worst = min(trades, key=lambda t: t["pnl"] or 0)
|
||||
|
||||
def fmt_pnl(v):
|
||||
sign = "+" if v >= 0 else ""
|
||||
return f"{sign}{v:.2f}€"
|
||||
|
||||
def row_color(pnl):
|
||||
if pnl > 0: return "#1a4731"
|
||||
if pnl < 0: return "#3d1a1a"
|
||||
return "#1a1e24"
|
||||
|
||||
# ── HTML ──────────────────────────────────────────────────────────────────
|
||||
header_color = "#1f6feb" if pnl_sum >= 0 else "#b02020"
|
||||
pnl_color = "#3fb950" if pnl_sum >= 0 else "#f85149"
|
||||
|
||||
rows = ""
|
||||
for s, d in sorted(setups.items(), key=lambda x: -x[1]["pnl"]):
|
||||
wr_s = d["wins"] / d["n"] * 100
|
||||
bg = row_color(d["pnl"])
|
||||
rows += f"""
|
||||
<tr style="background:{bg};">
|
||||
<td style="padding:6px 12px;font-family:monospace;">{s}</td>
|
||||
<td style="padding:6px 12px;text-align:center;">{d['n']}</td>
|
||||
<td style="padding:6px 12px;text-align:center;">{wr_s:.0f}%</td>
|
||||
<td style="padding:6px 12px;text-align:right;font-weight:bold;">{fmt_pnl(d['pnl'])}</td>
|
||||
</tr>"""
|
||||
|
||||
import time as _time
|
||||
best_time = _time.strftime("%H:%M", _time.localtime(best["exit_time"]))
|
||||
worst_time = _time.strftime("%H:%M", _time.localtime(worst["exit_time"]))
|
||||
|
||||
html = f"""
|
||||
<html><body style="background:#0d1117;color:#e6edf3;font-family:sans-serif;padding:24px;">
|
||||
<h2 style="color:{header_color};margin-bottom:4px;">
|
||||
Oil Trading — {target_date.strftime('%d.%m.%Y')}
|
||||
</h2>
|
||||
|
||||
<table style="border-collapse:collapse;margin:16px 0;width:100%;max-width:400px;">
|
||||
<tr>
|
||||
<td style="padding:8px 16px;background:#161b22;color:#8b949e;">Trades</td>
|
||||
<td style="padding:8px 16px;background:#161b22;font-weight:bold;">{total}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 16px;background:#0d1117;color:#8b949e;">Win Rate</td>
|
||||
<td style="padding:8px 16px;background:#0d1117;font-weight:bold;">{wr:.0f}% ({wins}/{total})</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:8px 16px;background:#161b22;color:#8b949e;">PnL</td>
|
||||
<td style="padding:8px 16px;background:#161b22;font-weight:bold;color:{pnl_color};font-size:18px;">{fmt_pnl(pnl_sum)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3 style="color:#8b949e;margin-bottom:8px;">Pro Setup</h3>
|
||||
<table style="border-collapse:collapse;width:100%;max-width:500px;">
|
||||
<tr style="background:#161b22;color:#8b949e;">
|
||||
<th style="padding:6px 12px;text-align:left;">Setup</th>
|
||||
<th style="padding:6px 12px;">N</th>
|
||||
<th style="padding:6px 12px;">WR</th>
|
||||
<th style="padding:6px 12px;text-align:right;">PnL</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
|
||||
<table style="border-collapse:collapse;margin-top:16px;width:100%;max-width:500px;">
|
||||
<tr>
|
||||
<td style="padding:6px 12px;color:#8b949e;">Bester Trade</td>
|
||||
<td style="padding:6px 12px;color:#3fb950;font-weight:bold;">{fmt_pnl(best['pnl'])}</td>
|
||||
<td style="padding:6px 12px;color:#8b949e;font-family:monospace;">{best.get('setup','—')} {best_time}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding:6px 12px;color:#8b949e;">Schlechtester</td>
|
||||
<td style="padding:6px 12px;color:#f85149;font-weight:bold;">{fmt_pnl(worst['pnl'])}</td>
|
||||
<td style="padding:6px 12px;color:#8b949e;font-family:monospace;">{worst.get('setup','—')} {worst_time}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body></html>
|
||||
"""
|
||||
return html
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1:
|
||||
target = datetime.date.fromisoformat(sys.argv[1])
|
||||
else:
|
||||
target = datetime.date.today() - datetime.timedelta(days=1)
|
||||
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
print(run(target))
|
||||
@@ -0,0 +1,63 @@
|
||||
# send_daily_report.ps1
|
||||
# Sendet täglich um 0 Uhr eine HTML-Zusammenfassung aller Trades des Vortages.
|
||||
|
||||
$python = "C:\Users\ah\AppData\Local\Programs\Python\Python312\python.exe"
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$pyScript = Join-Path $scriptDir "daily_summary.py"
|
||||
|
||||
# ── Graph-Credentials aus oil_widget_config.ini ([graph]) ─────────────────
|
||||
# Secrets gehören nicht ins Skript — zentrale Config wie bei den API-Keys.
|
||||
$iniPath = Join-Path (Split-Path -Parent $scriptDir) "oil_widget_config.ini"
|
||||
$graph = @{}
|
||||
$inGraph = $false
|
||||
foreach ($line in Get-Content $iniPath -Encoding UTF8) {
|
||||
if ($line -match '^\s*\[(.+)\]') { $inGraph = ($Matches[1] -eq 'graph'); continue }
|
||||
if ($inGraph -and $line -match '^\s*(\w+)\s*=\s*(.+?)\s*$') {
|
||||
$graph[$Matches[1]] = $Matches[2]
|
||||
}
|
||||
}
|
||||
$tenantId = $graph['tenant_id']
|
||||
$clientId = $graph['client_id']
|
||||
$clientSecret = $graph['client_secret']
|
||||
if (-not $tenantId -or -not $clientId -or -not $clientSecret) {
|
||||
Write-Error "Abschnitt [graph] in $iniPath fehlt oder ist unvollständig."
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ── Zusammenfassung aus Python holen ────────────────────────────────────────
|
||||
$yesterday = (Get-Date).AddDays(-1).ToString("yyyy-MM-dd")
|
||||
|
||||
# -join: Python gibt mehrere Zeilen aus → PowerShell macht ein String-Array daraus.
|
||||
# Ohne -join würde nur die erste Zeile (<html>) als Content ankommen → leere Mail.
|
||||
$htmlBody = (& $python $pyScript $yesterday 2>&1) -join "`n"
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($htmlBody) -or $htmlBody -match "Traceback|Error:") {
|
||||
$htmlBody = "<p>Fehler beim Generieren des Reports für $yesterday.</p><pre>$htmlBody</pre>"
|
||||
}
|
||||
|
||||
$subject = "Oil Trading $((Get-Date).AddDays(-1).ToString('dd.MM.yyyy'))"
|
||||
|
||||
# ── Microsoft Graph verbinden ────────────────────────────────────────────────
|
||||
$secureSecret = ConvertTo-SecureString $clientSecret -AsPlainText -Force
|
||||
$cred = New-Object System.Management.Automation.PSCredential($clientId, $secureSecret)
|
||||
Connect-MgGraph -TenantId $tenantId -ClientSecretCredential $cred -NoWelcome
|
||||
|
||||
# ── E-Mail senden ────────────────────────────────────────────────────────────
|
||||
$params = @{
|
||||
Message = @{
|
||||
Subject = $subject
|
||||
Body = @{
|
||||
ContentType = "HTML"
|
||||
Content = $htmlBody
|
||||
}
|
||||
ToRecipients = @(
|
||||
@{ EmailAddress = @{ Address = "axel@hocks.eu" } }
|
||||
)
|
||||
From = @{
|
||||
EmailAddress = @{ Address = "mailagent@hocks.eu" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Send-MgUserMail -UserId "mailagent@hocks.eu" -BodyParameter $params
|
||||
Write-Output "Report gesendet: $subject"
|
||||
Reference in New Issue
Block a user