Created
September 28, 2024 17:19
-
-
Save ChristianRiesen/f79b6328e363585fc433609e8863e187 to your computer and use it in GitHub Desktop.
Ghostfolio is a great selfhosted solution if you want to keep track of some of your money related things in life. Not for everyone but I found it extremely helpful. However, there is no Importer to convert the CSV of my bank UBS into what Ghsotfolio takes. So I made one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
class UbsToGhostfolioConvertor | |
{ | |
private const VALID_TYPES = ['BUY', 'DIVIDEND', 'FEE', 'INTEREST', 'ITEM', 'LIABILITY', 'SELL']; | |
public function convertCsvToJson(string $csvContent): array | |
{ | |
$result = []; | |
try { | |
$lines = explode("\n", $csvContent); | |
$header = str_getcsv(array_shift($lines), ';'); | |
if (empty($header)) { | |
throw new \RuntimeException("Unable to read CSV header"); | |
} | |
if ($header[15] != 'Ccy.') { | |
throw new \RuntimeException("Header 15 is not Ccy. and that causes issues with currency parsing."); | |
} | |
// There are two currency headers that are identical. 15 is the second one, so it gets the 1 behind it. | |
$header[15] = 'Ccy.1'; | |
$columnMap = array_flip($header); | |
foreach ($lines as $lineNumber => $line) { | |
$lineNumber += 2; // Accounting for 0-based index and header line | |
if (empty(trim($line))) { | |
continue; // Skip empty lines | |
} | |
$row = str_getcsv($line, ';'); | |
// Check if the first column (Valuation date) is a valid date | |
if (!$this->validateDate($row[0])) { | |
if (strpos($row[0], 'Transaction list:') === 0) { | |
continue; // Skip the summary line | |
} | |
throw new \RuntimeException("Invalid date format in first column on line $lineNumber"); | |
} | |
$transaction = $this->createTransaction($row, $columnMap); | |
if (!$this->validateTransaction($transaction)) { | |
throw new \RuntimeException("Invalid or missing data in transaction on line $lineNumber"); | |
} | |
$result[] = $transaction; | |
} | |
} catch (\Throwable $e) { | |
throw new \RuntimeException("Error processing CSV: " . $e->getMessage(), 0, $e); | |
} | |
return $result; | |
} | |
private function validateDate(string $date): bool | |
{ | |
$dateTime = \DateTime::createFromFormat('d.m.Y', $date); | |
return $dateTime && $dateTime->format('d.m.Y') === $date; | |
} | |
private function validateTransaction(array $transaction): bool | |
{ | |
$requiredFields = ['currency', 'date', 'quantity', 'symbol', 'type', 'unitPrice']; | |
foreach ($requiredFields as $field) { | |
if (!isset($transaction[$field]) || $transaction[$field] === '') { | |
return false; | |
} | |
} | |
return true; | |
} | |
private function createTransaction(array $row, array $columnMap): array | |
{ | |
$type = $this->determineTransactionType($row, $columnMap); | |
$isNonTraditionalTransaction = in_array($type, ['DIVIDEND', 'INTEREST']); | |
$date = \DateTime::createFromFormat('!d.m.Y', $row[$columnMap['Trade date']], new \DateTimeZone('UTC')); | |
if ($date === false) { | |
// Handle parsing error | |
$errors = \DateTime::getLastErrors(); | |
throw new \RuntimeException("Error parsing date: $errors"); | |
} | |
return [ | |
'currency' => $isNonTraditionalTransaction ? $row[$columnMap['Ccy.']] : $row[$columnMap['Ccy.1']], | |
'dataSource' => 'MANUAL', | |
'date' => $date->format('Y-m-d\TH:i:s.000\Z'), | |
'fee' => 0, | |
'quantity' => $isNonTraditionalTransaction ? 1 : abs((float)str_replace(["'", ','], ['', '.'], $row[$columnMap['Number/Amt.']])), | |
'symbol' => $row[$columnMap['ISIN']] ?? '', | |
'type' => $type, | |
'unitPrice' => $isNonTraditionalTransaction | |
? (float)str_replace(["'", ','], ['', '.'], $row[$columnMap['Number/Amt.']]) | |
: (float)str_replace(["'", ','], ['', '.'], $row[$columnMap['Trans. price']]), | |
'comment' => $row[$columnMap['External reference']] ?: ($row[$columnMap['Order no.']] ?? null) | |
]; | |
} | |
private function determineTransactionType(array $row, array $columnMap): string | |
{ | |
$description = $row[$columnMap['Description 1']] ?? ''; | |
if (strpos($description, 'Purchase') !== false) { | |
return 'BUY'; | |
} elseif (strpos($description, 'Sale') !== false) { | |
return 'SELL'; | |
} elseif (strpos($description, 'Dividend') !== false) { | |
return 'DIVIDEND'; | |
} elseif (strpos($description, 'Capital gain') !== false) { | |
return 'INTEREST'; | |
} | |
throw new \RuntimeException("Unknown transaction type: $description"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment