>, * einstand: float, * wert: float, * gewinn: float, * realisiert: float, * ertraege: float, * bewertbar: bool * } */ public static function bestand(?int $kontoId = null, ?string $stichtag = null): array { $stichtag ??= date('Y-m-d'); $kurse = Wertpapier::letzteKurse($stichtag); $positionen = []; $realisiert = 0.0; $ertraege = 0.0; foreach (self::buchungen($kontoId, $stichtag) as $buchung) { $schluessel = (int)$buchung['wertpapier_id'] . ':' . (int)$buchung['konto_id']; $position = $positionen[$schluessel] ?? [ 'wertpapier_id' => (int)$buchung['wertpapier_id'], 'konto_id' => (int)$buchung['konto_id'], 'name' => (string)$buchung['wertpapier'], 'isin' => (string)$buchung['isin'], 'wkn' => (string)$buchung['wkn'], 'depot' => (string)$buchung['depot'], 'stueck' => 0.0, 'einstand' => 0.0, 'realisiert' => 0.0, 'ertraege' => 0.0, ]; $stueck = (float)$buchung['stueck']; $betrag = (float)$buchung['betrag']; $kosten = (float)$buchung['gebuehren'] + (float)$buchung['steuern']; switch ((string)$buchung['vorgang']) { case 'kauf': case 'einlage': // Nebenkosten gehören zum Einstand: Sie sind Teil dessen, // was das Papier gekostet hat. $position['stueck'] += $stueck; $position['einstand'] += $betrag + $kosten; break; case 'verkauf': case 'entnahme': $abgang = abs($stueck); $anteil = $position['stueck'] > 0.0 ? min(1.0, $abgang / $position['stueck']) : 1.0; $anteiligerEinstand = round($position['einstand'] * $anteil, 2); $position['stueck'] -= $abgang; $position['einstand'] -= $anteiligerEinstand; $position['realisiert'] += round($betrag - $kosten - $anteiligerEinstand, 2); break; case 'dividende': $position['ertraege'] += $betrag - $kosten; break; } $positionen[$schluessel] = $position; } $einstandGesamt = 0.0; $wertGesamt = 0.0; $bewertbar = true; $fertig = []; foreach ($positionen as $position) { $realisiert += $position['realisiert']; $ertraege += $position['ertraege']; // Vollständig verkaufte Positionen verschwinden aus der Übersicht; // ihr realisierter Gewinn zählt weiter mit. if (round($position['stueck'], 6) <= 0.0) { continue; } $kurs = $kurse[$position['wertpapier_id']] ?? null; $position['stueck'] = round($position['stueck'], 6); $position['einstand'] = round($position['einstand'], 2); $position['kurs'] = $kurs['kurs'] ?? null; $position['kurs_datum'] = $kurs['datum'] ?? null; $position['einstand_je_stueck'] = $position['stueck'] > 0.0 ? round($position['einstand'] / $position['stueck'], 4) : null; if ($kurs === null) { // Ohne Kurs ist der Wert unbekannt. Der Einstand als Ersatz wäre // bequem, verstellte aber den Blick: Die Übersicht sagt lieber, // dass ein Kurs fehlt. $position['wert'] = null; $position['gewinn'] = null; $bewertbar = false; } else { $position['wert'] = round($position['stueck'] * $kurs['kurs'], 2); $position['gewinn'] = round($position['wert'] - $position['einstand'], 2); $wertGesamt += $position['wert']; } $einstandGesamt += $position['einstand']; $fertig[] = $position; } usort($fertig, static fn(array $a, array $b): int => strcasecmp($a['name'], $b['name'])); return [ 'positionen' => $fertig, 'einstand' => round($einstandGesamt, 2), 'wert' => round($wertGesamt, 2), 'gewinn' => round($wertGesamt - $einstandGesamt, 2), 'realisiert' => round($realisiert, 2), 'ertraege' => round($ertraege, 2), 'bewertbar' => $bewertbar, ]; } /** * Der bewertete Depotwert je Konto – die Kontenübersicht braucht ihn für * die Depotzeilen, wo ein Saldo aus Buchungen keinen Sinn ergäbe. * * @return array konto_id => Wert */ public static function werteJeDepot(?string $stichtag = null): array { $werte = []; foreach (self::depots() as $depot) { $bestand = self::bestand((int)$depot['id'], $stichtag); $werte[(int)$depot['id']] = $bestand['wert']; } return $werte; } /** Alle Konten der Kontoart „depot“. @return list> */ public static function depots(bool $nurAktive = false): array { return array_values(array_filter( Konto::alle($nurAktive), static fn(array $konto): bool => (string)$konto['kontoart'] === 'depot' )); } /** * Veräußerungsgeschäfte eines Zeitraums: je Verkauf der Erlös, der * anteilige Einstand und der Gewinn. * * Gerechnet wird über die ganze Historie bis zum Verkaufstag – der Einstand * eines im Mai verkauften Papiers hängt an allen Käufen davor, nicht nur an * denen im Berichtszeitraum. * * @return array{geschaefte: list>, gewinn: float, erloes: float} */ public static function veraeusserungen(string $von, string $bis, ?int $kontoId = null): array { $bestaende = []; $geschaefte = []; $gewinn = 0.0; $erloes = 0.0; foreach (self::buchungen($kontoId, $bis) as $buchung) { $schluessel = (int)$buchung['wertpapier_id'] . ':' . (int)$buchung['konto_id']; $stand = $bestaende[$schluessel] ?? ['stueck' => 0.0, 'einstand' => 0.0]; $betrag = (float)$buchung['betrag']; $kosten = (float)$buchung['gebuehren'] + (float)$buchung['steuern']; $stueck = (float)$buchung['stueck']; if (in_array((string)$buchung['vorgang'], Depotbuchung::ZUGAENGE, true)) { $stand['stueck'] += $stueck; $stand['einstand'] += $betrag + $kosten; } elseif (in_array((string)$buchung['vorgang'], Depotbuchung::ABGAENGE, true)) { $abgang = abs($stueck); $anteil = $stand['stueck'] > 0.0 ? min(1.0, $abgang / $stand['stueck']) : 1.0; $anteiligerEinstand = round($stand['einstand'] * $anteil, 2); $stand['stueck'] -= $abgang; $stand['einstand'] -= $anteiligerEinstand; $datum = (string)$buchung['datum']; if ($datum >= $von && $datum <= $bis && (string)$buchung['vorgang'] === 'verkauf') { $einzel = round($betrag - $kosten - $anteiligerEinstand, 2); $geschaefte[] = [ 'datum' => $datum, 'name' => (string)$buchung['wertpapier'], 'isin' => (string)$buchung['isin'], 'depot' => (string)$buchung['depot'], 'stueck' => $abgang, 'erloes' => round($betrag - $kosten, 2), 'einstand' => $anteiligerEinstand, 'gewinn' => $einzel, ]; $gewinn += $einzel; $erloes += round($betrag - $kosten, 2); } } $bestaende[$schluessel] = $stand; } return [ 'geschaefte' => $geschaefte, 'gewinn' => round($gewinn, 2), 'erloes' => round($erloes, 2), ]; } /** * Erträge (Dividenden und Ausschüttungen) eines Zeitraums, je Wertpapier. * * @return array{zeilen: list>, summe: float} */ public static function ertraege(string $von, string $bis, ?int $kontoId = null): array { $params = [Mandant::id(), $von, $bis]; $sql = "SELECT w.name, w.isin, SUM(d.betrag - d.gebuehren - d.steuern) AS summe, COUNT(*) AS anzahl FROM depot_buchungen d JOIN wertpapiere w ON w.id = d.wertpapier_id WHERE d.benutzer_id = ? AND d.vorgang = 'dividende' AND d.datum BETWEEN ? AND ?"; if ($kontoId !== null) { $sql .= ' AND d.konto_id = ?'; $params[] = $kontoId; } $zeilen = Database::fetchAll($sql . ' GROUP BY w.id ORDER BY summe DESC', $params); $summe = 0.0; foreach ($zeilen as $nummer => $zeile) { $zeilen[$nummer]['summe'] = round((float)$zeile['summe'], 2); $summe += $zeilen[$nummer]['summe']; } return ['zeilen' => $zeilen, 'summe' => round($summe, 2)]; } /** * Die Wertentwicklung eines Depots über die letzten Monate: je Monatsende * der bewertete Bestand. * * @return list */ public static function entwicklung(?int $kontoId = null, int $monate = 12, ?string $heute = null): array { $reihe = []; foreach (Vermoegensentwicklung::monate($monate, $heute) as $monat) { $bestand = self::bestand($kontoId, $monat['bis']); $reihe[] = [ 'monat' => sprintf('%04d-%02d', $monat['jahr'], $monat['monat']), 'kuerzel' => $monat['kuerzel'], 'wert' => $bestand['wert'], 'einstand' => $bestand['einstand'], ]; } return $reihe; } /** * Alle Depotbuchungen bis zum Stichtag, aufsteigend – die Reihenfolge * entscheidet über den gleitenden Durchschnitt. * * @return list> */ private static function buchungen(?int $kontoId, string $bis): array { $params = [Mandant::id(), $bis]; $sql = 'SELECT d.*, w.name AS wertpapier, w.isin, w.wkn, k.name AS depot FROM depot_buchungen d JOIN wertpapiere w ON w.id = d.wertpapier_id JOIN konten k ON k.id = d.konto_id WHERE d.benutzer_id = ? AND d.datum <= ?'; if ($kontoId !== null) { $sql .= ' AND d.konto_id = ?'; $params[] = $kontoId; } return Database::fetchAll($sql . ' ORDER BY d.datum, d.id', $params); } }