Skip to content

Instantly share code, notes, and snippets.

@ChristianRiesen
Created September 28, 2024 17:19
Show Gist options
  • Save ChristianRiesen/f79b6328e363585fc433609e8863e187 to your computer and use it in GitHub Desktop.
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.
<?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