Initial project commit

This commit is contained in:
2026-07-05 18:39:27 +02:00
commit cc55f55dab
6 changed files with 1231 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
# Ignore everything by default
*
# Allow directories
!*/
# Allow Git internals
!/.gitignore
# Allow shell scripts
!*.sh
# Allow Markdown files
!*.md
# Allow vars.example file
!vars.example
# Allow LICENSE and CHANGELOG
!LICENSE
!CHANGELOG.md
+20
View File
@@ -0,0 +1,20 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.0.0 - 2026-07-05
### Added
- Added `tr2gf.sh` as a POSIX-compliant Trade Republic transaction export converter for Ghostfolio.
- Added support for `ALL` and filtered selector modes.
- Added config-driven symbol, source and code mappings loaded from a local `vars` file.
- Added CLI symbol overrides via `-S`, `--symbol` and `--symbol=VALUE`.
- Added count-only mode for transaction inspection without export file generation.
- Added runtime verification for required files, directories, environment variables and tool dependencies.
- Added structured logging helpers and batch-aware notices.
- Added AWK-based CSV parsing, transaction filtering, cancellation matching and Ghostfolio CSV export generation.
- Added default source handling for Yahoo-backed instruments and CoinGecko-backed crypto symbols.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Stephan Düsterhaupt
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+172
View File
@@ -0,0 +1,172 @@
# tr2gf
Trade Republic transaction export converter for Ghostfolio.
`tr2gf.sh` converts a Trade Republic transaction export CSV into one Ghostfolio-compatible CSV file per selected ISIN or symbol. The script is POSIX-compliant, supports config-driven symbol and source mappings, optional CLI overrides, count-only runs and per-symbol export file naming based on the latest matching transaction date.
## Outline
- [Features](#features)
- [Repository layout](#repository-layout)
- [Requirements](#requirements)
- [Installation](#installation)
- [Configuration](#configuration)
- [Usage](#usage)
- [Examples](#examples)
- [Output format](#output-format)
- [Logging and troubleshooting](#logging-and-troubleshooting)
- [License](#license)
- [Authors](#authors)
- [Project Home](#project-home)
## Features
- Convert a Trade Republic CSV export into one Ghostfolio-compatible CSV file per matching symbol.
- Export `BUY` and `SELL` transactions and suppress matching `BUY_CANCELLED` records.
- Load runtime defaults and symbol/source mappings from a local `vars` file.
- Override symbol codes per selector on the command line via `-S` or `--symbol`.
- Run in count-only mode without writing export files.
- Keep the implementation portable with `#!/bin/sh` and standard Unix tools such as `awk`, `sed`, `grep` and `date`.
## Repository layout
```text
.
├── .gitignore
├── tr2gf.sh
├── vars.example
├── README.md
├── CHANGELOG.md
└── LICENSE
```
## Requirements
The script checks for the presence of `awk`, `sed`, `grep` and `date` at runtime and it expects a readable source CSV plus an output directory when not running in count-only mode.
## Installation
1. Clone the repository.
2. Copy `vars.example` to `vars`.
3. Adjust the runtime defaults and symbol mappings for the local environment.
4. Make `tr2gf.sh` executable.
5. Run a help or count-only command first to validate the setup.
```sh
git clone https://github.com/sduesterhaupt/tr2gf.git
cd tr2gf
cp vars.example vars
chmod 0750 tr2gf.sh
chmod 0640 vars
./tr2gf.sh help
```
The `tr2gf.sh` script is executable only for the repository owner, and the `vars` file is readable by owner and group to keep configuration and symbol mappings under tighter control.
## Configuration
The script initializes built-in defaults for the source file, output directory, account name, logging and default data sources, then loads overrides from a local `vars` file if present.
Supported configurable keys loaded from `vars` are:
- `TR2GF_SOURCE_FILE`
- `TR2GF_OUTPUT_DIR`
- `TR2GF_ACCOUNT_NAME`
- `TR2GF_YAHOO_SOURCE`
- `TR2GF_CRYPTO_SOURCE`
- `TR2GF_LOG_DIR`
- `TR2GF_LOG_FILE`
- `TR2GF_LOG_LEVEL`
- `TR2GF_BATCH`
Symbol mappings are read as colon-separated entries in the form `SYMBOL:CODE[:SOURCE]`. When no source is set, known crypto symbols default to the configured crypto data source and all other symbols default to the configured Yahoo source.
## Usage
General command form:
```sh
./tr2gf.sh [options] TARGET [target-options]
```
Global options implemented by the script:
- `-f FILE` define a specific Trade Republic source CSV.
- `-o DIR` define a specific export output directory.
- `-a NAME` define the account name written to the export CSV.
- `-c` count matching `BUY` and `SELL` transactions only.
- `-h`, `--help` show usage information.
Target modes implemented by the script:
- `ALL` export all matching `BUY` and `SELL` transactions found in the source CSV.
- `ISIN [target-opts]` export transactions only for the specified selectors (e.g. ISINs and crypto tickers).
Target options implemented by the script (per selector):
- `-S SYMBOL`
- `--symbol SYMBOL`
- `--symbol=SYMBOL`
## Examples
Export all supported transactions into one file per symbol:
```sh
./tr2gf.sh ALL
```
Count transactions for selected selectors without writing files:
```sh
./tr2gf.sh -c IE00B4L5Y983 BTC
```
Export a selected symbol with a CLI override for the target code:
```sh
./tr2gf.sh IE00BK5BQT80 -S VWCE.DE
```
Write exports into a dedicated directory and set the Ghostfolio account label:
```sh
./tr2gf.sh -o ./exports -a "Trade Republic" ALL
```
## Output format
Each generated file starts with the Ghostfolio-compatible header `Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Account,Note` and the filename is prefixed with the latest matching transaction date in `YYMMDD` form.
The generated `Note` field uses the pattern `ISIN <symbol>`, while the `Action` column is mapped to `buy` or `sell` and fees are written as absolute numeric values.
## Logging and troubleshooting
The script contains a file-based logging system with severity levels from `0` to `5`, configurable via `TR2GF_LOG_LEVEL` and writes to `TR2GF_LOG_FILE` when enabled.
For troubleshooting, useful first checks are:
```sh
./tr2gf.sh help
./tr2gf.sh -c ALL
sed -n '1,5p' ./TradeRepublic_TransactionExport.csv
```
Adjust the filename in the example above if your Trade Republic export has a date prefix or a different name.
Typical failure points are a missing source CSV, a wrong output directory, absent required commands or mismatched CSV headers such as missing `date`, `type`, `symbol`, `name`, `shares`, `price`, `fee` or `currency` columns.
## License
This project is published under the MIT License. See `LICENSE` for details.
## Authors
- [Stephan Düsterhaupt](xmpp:me@jabber.stephanduesterhaupt.de)
## Project Home
Project Home: [https://dev.town-square.de/sduesterhaupt/tr2gf](https://dev.town-square.de/sduesterhaupt/tr2gf)
Executable
+916
View File
@@ -0,0 +1,916 @@
#!/bin/sh
###############################################################################
# tr2gf.sh
# Trade Republic transaction export converter
# This script converts a Trade Republic transaction export CSV into one
# Ghostfolio-compatible CSV file per selected ISIN or symbol. It supports
# config-driven symbol/code mappings, optional CLI overrides, transaction
# filtering, and export file generation with the latest transaction date prefix.
# Features:
# - Export all matching BUY and SELL transactions per symbol
# - Load configuration defaults and symbol/code mappings from a vars file
# - Override per-ISIN symbol mappings via CLI parameters
# - Count matching transactions without writing export files
# - POSIX-compatible operation for repeatable local or batch execution
# Authors: Stephan Düsterhaupt
# Copyright (c) 2026 Stephan Düsterhaupt
# License: MIT
# Project Home: https://dev.town-square.de/sduesterhaupt/tr2gf
###############################################################################
# MIT License
# Copyright (c) 2026 Stephan Düsterhaupt
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
set -eu
# SDuesterhaupt: 2026-07-05 - Write DEBUG entry logs for function tracing.
_log_enter() {
_log "Enter the function '$1()'..." 1
return 0
} #=> _log_enter()
# SDuesterhaupt: 2026-07-05 - Write DEBUG exit logs for function tracing.
_log_leave() {
_log "Leave the function '$1()'..." 1
return 0
} #=> _log_leave()
# SDuesterhaupt: 2026-07-05 - Show general usage information and an overview
# of available target modes.
_usage() {
_log_enter "_usage"
err_source="Not defined: vars autodetect failed or disabled"
current_vars="$TR2GF/vars"
[ -f "$current_vars" ] || current_vars="$err_source"
source_file=${TR2GF_SOURCE_FILE:-$err_source}
output_dir=${TR2GF_OUTPUT_DIR:-$err_source}
account_name=${TR2GF_ACCOUNT_NAME:-$err_source}
printf '%s\n' \
"tr2gf usage and overview" \
"" \
"USAGE: tr2gf.sh [options] TARGET [target-options]" \
"" \
"A list of supported target modes is shown below. To get detailed usage and help, run:" \
" tr2gf.sh help TARGET" \
"" \
"For a listing of options that may be supplied before the target, use:" \
" tr2gf.sh help options" \
"" \
"Target modes:" \
" ALL" \
" ISIN [ target-opts ]" \
"" \
"RUNTIME STATUS (exports would use these locations and defaults)" \
" VARS: $current_vars" \
" SOURCE: $source_file" \
" OUTPUT: $output_dir" \
" ACCOUNT: $account_name" \
""
_log_leave "_usage"
return 0
} #=> _usage()
# SDuesterhaupt: 2026-07-05 - Show target-specific help text or global option help.
# When called without arguments, show general usage or target-specific help.
_cmd_help() {
_log_enter "_cmd_help"
text=""
opts=""
case "$1" in
ALL)
text="ALL\n Export all matching BUY and SELL transactions found in the source CSV."
;;
ISIN)
text="ISIN [ target-opts ]\n Export transactions only for the specified ISIN selectors."
opts="-S SYMBOL\n --symbol=VALUE\n symbol=VALUE"
;;
options)
_opt_usage
_log_leave "_cmd_help"
return 0
;;
"")
_usage
_log_leave "_cmd_help"
return 0
;;
*)
text="Unknown help target: '$1' (run without arguments for an overview)"
;;
esac
[ -n "$text" ] && printf '%b\n' "$text"
if [ -n "$opts" ]; then
printf '\n%s\n' "target-opts is an optional set of target options from this list:"
printf '%b\n' "$opts"
fi
_log_leave "_cmd_help"
return 0
} #=> _cmd_help()
# SDuesterhaupt: 2026-07-05 - Print descriptions of global command-line options.
_opt_usage() {
_log_enter "_opt_usage"
printf '%s\n' \
"tr2gf Global Option Flags" \
"" \
"The following options may be provided before the target mode. Options specified" \
"at runtime override built-in defaults and values loaded from the local vars" \
"file. Unless noted, non-empty values to options are mandatory." \
"" \
"General options:" \
" -f FILE define a specific Trade Republic source CSV" \
" -o DIR define a specific export output directory" \
" -a NAME define the account name written to the export CSV" \
" -c count matching BUY/SELL transactions per symbol only" \
" -h, --help show general usage information" \
"" \
"Target options:" \
" -S SYMBOL override Code for the immediately preceding ISIN" \
" --symbol=VALUE override Code for the immediately preceding ISIN" \
""
_log_leave "_opt_usage"
return 0
} #=> _opt_usage()
# SDuesterhaupt: 2026-07-05 - Abort execution with an error message and exit code.
_die() {
msg=$1
code=${2:-1}
printf '\n%s\n%s\n' "tr2gf error:" "$msg" >&2
if [ "$code" -ne 0 ] && [ -n "${TR2GF_LOG_FILE:-}" ]; then
ts=$(date '+%Y-%m-%d %H:%M:%S')
printf '%s tr2gf [ERROR] %s\n' "$ts" "$msg" >>"$TR2GF_LOG_FILE"
fi
exit "$code"
} #=> _die()
# SDuesterhaupt: 2026-07-05 - Print an informational notice unless batch mode is enabled.
_notice() {
if [ -z "${TR2GF_BATCH:-}" ]; then
printf '\n%s\n' "$1"
fi
return 0
} #=> _notice()
# SDuesterhaupt: 2026-07-05 - Verify essential runtime variables are defined.
# Checks the presence of critical env vars for tr2gf runtime.
_vars_source_check() {
_log_enter "_vars_source_check"
[ -n "${TR2GF:-}" ] || _die "TR2GF env-var undefined" 5
[ -n "${TR2GF_SOURCE_FILE:-}" ] || _die "TR2GF_SOURCE_FILE env-var undefined" 5
[ -n "${TR2GF_OUTPUT_DIR:-}" ] || _die "TR2GF_OUTPUT_DIR env-var undefined" 5
[ -n "${TR2GF_ACCOUNT_NAME:-}" ] || _die "TR2GF_ACCOUNT_NAME env-var undefined" 5
[ -n "${TR2GF_YAHOO_SOURCE:-}" ] || _die "TR2GF_YAHOO_SOURCE env-var undefined" 5
[ -n "${TR2GF_CRYPTO_SOURCE:-}" ] || _die "TR2GF_CRYPTO_SOURCE env-var undefined" 5
[ -n "${TR2GF_COUNT_ONLY:-}" ] || _die "TR2GF_COUNT_ONLY env-var undefined" 5
[ -n "${TR2GF_SYMBOL_MAPS+x}" ] || _die "TR2GF_SYMBOL_MAPS env-var undefined" 5
_log_leave "_vars_source_check"
return 0
} #=> _vars_source_check()
# SDuesterhaupt: 2026-07-05 - Verify tr2gf runtime directories, files and tools.
# Checks existence of required paths and command availability.
_verify_runtime_init() {
_log_enter "_verify_runtime_init"
help_note="Run tr2gf.sh without targets for usage and target help."
_vars_source_check
[ -f "$TR2GF_SOURCE_FILE" ] || _die "TR2GF_SOURCE_FILE missing. $help_note" 5
if [ "$TR2GF_COUNT_ONLY" -eq 0 ]; then
[ -d "$TR2GF_OUTPUT_DIR" ] || _die "TR2GF_OUTPUT_DIR missing. $help_note" 5
fi
command -v awk >/dev/null 2>&1 || _die "awk not available. $help_note" 5
command -v sed >/dev/null 2>&1 || _die "sed not available. $help_note" 5
command -v grep >/dev/null 2>&1 || _die "grep not available. $help_note" 5
command -v date >/dev/null 2>&1 || _die "date not available. $help_note" 5
_log "Runtime checks passed for source '$TR2GF_SOURCE_FILE'." 2
_log_leave "_verify_runtime_init"
return 0
} #=> _verify_runtime_init()
# SDuesterhaupt: 2026-07-05 - Ensure the log directory exists before logging starts.
_init_log() {
_log_enter "_init_log"
if [ ! -d "$TR2GF_LOG_DIR" ]; then
mkdir -p "$TR2GF_LOG_DIR" 2>/dev/null \
|| _die "Could not create log directory '$TR2GF_LOG_DIR'" 1
fi
_log_leave "_init_log"
return 0
} #=> _init_log()
# SDuesterhaupt: 2026-07-05 - Load configuration defaults and local vars file.
_vars_setup() {
_log_enter "_vars_setup"
prog_file=$0
prog_dir=$(dirname "$prog_file")
_set_var TR2GF "$prog_dir"
_set_var TR2GF_SOURCE_FILE "$TR2GF/TradeRepublic_TransactionExport.csv"
_set_var TR2GF_OUTPUT_DIR "$TR2GF/exports"
_set_var TR2GF_ACCOUNT_NAME "DEFAULT"
_set_var TR2GF_COUNT_ONLY 0
_set_var TR2GF_YAHOO_SOURCE "YAHOO"
_set_var TR2GF_CRYPTO_SOURCE "COINGECKO"
_set_var TR2GF_SYMBOL_MAPS ""
# Logging
_set_var TR2GF_LOG_DIR "$TR2GF/log"
_set_var TR2GF_LOG_FILE "$TR2GF_LOG_DIR/tr2gf.log"
_set_var TR2GF_LOG_LEVEL 2
_set_var TR2GF_BATCH ""
if [ -f "$TR2GF/vars" ]; then
_load_vars_file "$TR2GF/vars"
_notice "Note: using tr2gf configuration from: $TR2GF/vars"
_log "Loaded configuration from: $TR2GF/vars" 2
else
_log "No local vars file found. Using built-in defaults." 2
fi
_log "Runtime defaults prepared." 2
_log_leave "_vars_setup"
return 0
} #=> _vars_setup()
# SDuesterhaupt: 2026-07-05 - Set a variable to a default value if it is unset.
_set_var() {
var=$1
shift
eval "$var=\${$var:-\"\$*\"}"
eval "export $var"
return 0
} #=> _set_var()
# SDuesterhaupt: 2026-07-05 - Write a message to the tr2gf log file.
#
# 0 -> OFF (silent mode)
# 1 -> DEBUG
# 2 -> INFO
# 3 -> WARN
# 4 -> ERROR
# 5 -> CRITICAL
#
# @param1: Message string to log.
# @param2: Severity level (0-5) where higher is more severe.
# Writes message with timestamp to log file.
# Prints to stderr if level >= 3.
_log() {
msg=$1
level=${2:-0}
set -- "" "DEBUG" "INFO" "WARN" "ERROR" "CRITICAL"
shift
level_name=$([ "$level" -ge 1 ] && eval "printf '%s' \${$level}")
level_tag="[$level_name]"
level_tag_len=${#level_tag}
pad_width=10
pad_count=$(( pad_width - level_tag_len ))
[ "$pad_count" -lt 1 ] && pad_count=1
pad=$(printf '%*s' "$pad_count" '')
if [ -n "${TR2GF_LOG_LEVEL:-}" ] && [ "$level" -ge "$TR2GF_LOG_LEVEL" ] 2>/dev/null && [ -n "$msg" ] && [ -n "${TR2GF_LOG_FILE:-}" ]; then
ts=$(date '+%Y-%m-%d %H:%M:%S')
printf '%s tr2gf %s%s%s\n' "$ts" "$level_tag" "$pad" "$msg" >>"$TR2GF_LOG_FILE"
fi
if [ "$level" -ge 3 ] 2>/dev/null && [ -n "$msg" ] && [ -z "${TR2GF_BATCH:-}" ]; then
printf '%s\n' "$msg" >&2
fi
return 0
} #=> _log()
# SDuesterhaupt: 2026-07-05 - Strip one pair of outer quotes from a config value.
_strip_outer_quotes() {
value=$1
case "$value" in
\"*\")
value=${value#\"}
value=${value%\"}
;;
\'*\')
value=${value#\'}
value=${value%\'}
;;
esac
printf '%s\n' "$value"
return 0
} #=> _strip_outer_quotes()
# SDuesterhaupt: 2026-07-05 - Detect whether a symbol should use crypto defaults.
_is_crypto_symbol() {
case "$1" in
BTC|ETH|SOL|ADA|XRP|DOT|DOGE|AVAX|LINK|MATIC)
return 0
;;
*)
return 1
;;
esac
} #=> _is_crypto_symbol()
# SDuesterhaupt: 2026-07-05 - Load supported config values and mapping entries from a vars file.
_load_vars_file() {
_log_enter "_load_vars_file"
vars_file=$1
[ -f "$vars_file" ] || _die "vars file not found: $vars_file" 1
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
line=$raw_line
case "$line" in
''|'#'*)
continue
;;
esac
line=$(printf '%s' "$line" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$line" ] || continue
case "$line" in
*=*)
key=${line%%=*}
value=${line#*=}
key=$(printf '%s' "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
value=$(printf '%s' "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
value=$(_strip_outer_quotes "$value")
case "$key" in
TR2GF_SOURCE_FILE|TR2GF_OUTPUT_DIR|TR2GF_ACCOUNT_NAME|TR2GF_YAHOO_SOURCE|TR2GF_CRYPTO_SOURCE|TR2GF_LOG_DIR|TR2GF_LOG_FILE|TR2GF_LOG_LEVEL|TR2GF_BATCH)
eval "$key=\$value"
eval "export $key"
;;
*)
;;
esac
;;
*:*)
symbol=$(printf '%s' "$line" | awk -F: '{print $1}')
code=$(printf '%s' "$line" | awk -F: '{print $2}')
source=$(printf '%s' "$line" | awk -F: '{if (NF >= 3) {sub(/^[^:]*:[^:]*:/, ""); print} else {print ""}}')
symbol=$(printf '%s' "$symbol" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
code=$(printf '%s' "$code" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
source=$(printf '%s' "$source" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
[ -n "$symbol" ] || continue
[ -n "$code" ] || continue
if [ -z "$source" ]; then
if _is_crypto_symbol "$symbol"; then
source=$TR2GF_CRYPTO_SOURCE
else
source=$TR2GF_YAHOO_SOURCE
fi
fi
if [ -n "$TR2GF_SYMBOL_MAPS" ]; then
TR2GF_SYMBOL_MAPS="${TR2GF_SYMBOL_MAPS}|${symbol}:${source}:${code}"
else
TR2GF_SYMBOL_MAPS="${symbol}:${source}:${code}"
fi
export TR2GF_SYMBOL_MAPS
_log "Loaded symbol override: ${symbol} -> ${code} (${source})" 1
;;
*)
;;
esac
done < "$vars_file"
_log_leave "_load_vars_file"
return 0
} #=> _load_vars_file()
# SDuesterhaupt: 2026-07-05 - Print active runtime configuration values.
_print_runtime_info() {
_log_enter "_print_runtime_info"
current_vars="$TR2GF/vars"
[ -f "$current_vars" ] || current_vars="not loaded"
printf '%s\n' \
"Runtime configuration" \
"" \
"VARS FILE: $current_vars" \
"SOURCE FILE: $TR2GF_SOURCE_FILE" \
"OUTPUT DIR: $TR2GF_OUTPUT_DIR" \
"ACCOUNT NAME: $TR2GF_ACCOUNT_NAME" \
"DEFAULT YAHOO SOURCE: $TR2GF_YAHOO_SOURCE" \
"DEFAULT CRYPTO SOURCE: $TR2GF_CRYPTO_SOURCE" \
"COUNT ONLY: $TR2GF_COUNT_ONLY" \
""
_log_leave "_print_runtime_info"
return 0
} #=> _print_runtime_info()
# SDuesterhaupt: 2026-07-05 - Append a symbol selector to the target list.
_append_symbol() {
_log_enter "_append_symbol"
isin=$1
if [ -n "$SYMBOLS" ]; then
SYMBOLS="${SYMBOLS}|${isin}"
else
SYMBOLS=$isin
fi
_log "Queued symbol selector: $isin" 1
_log_leave "_append_symbol"
return 0
} #=> _append_symbol()
# SDuesterhaupt: 2026-07-05 - Append a source/code override entry.
_append_override() {
_log_enter "_append_override"
isin=$1
source=$2
code=$3
entry="${isin}:${source}:${code}"
if [ -n "$CLI_OVERRIDES" ]; then
CLI_OVERRIDES="${CLI_OVERRIDES}|${entry}"
else
CLI_OVERRIDES=$entry
fi
_log "Queued CLI override: ${isin} -> ${code} (${source})" 2
_log_leave "_append_override"
return 0
} #=> _append_override()
_vars_setup
_init_log
while [ $# -gt 0 ]; do
opt=${1%%=*}
case "$opt" in
-f)
shift
[ $# -gt 0 ] || _die "-f requires a file path" 2
TR2GF_SOURCE_FILE=$1
shift
;;
-o)
shift
[ $# -gt 0 ] || _die "-o requires a directory path" 2
TR2GF_OUTPUT_DIR=$1
shift
;;
-a)
shift
[ $# -gt 0 ] || _die "-a requires a value" 2
TR2GF_ACCOUNT_NAME=$1
shift
;;
-c)
TR2GF_COUNT_ONLY=1
shift
;;
-h|--help)
_usage
exit 0
;;
--)
shift
break
;;
-S|--symbol|--symbol=*)
break
;;
-*)
_die "unknown option: $1" 2
;;
*)
break
;;
esac
done
case "${1:-}" in
help)
shift
_cmd_help "${1:-}"
exit 0
;;
-h|--help)
_usage
exit 0
;;
esac
if [ $# -lt 1 ]; then
_print_runtime_info >&2
_usage >&2
exit 2
fi
if [ "$TR2GF_COUNT_ONLY" -eq 0 ]; then
mkdir -p -- "$TR2GF_OUTPUT_DIR"
_log "Ensured output directory exists: $TR2GF_OUTPUT_DIR" 2
else
_log "Count-only mode enabled. No export files will be written." 2
fi
_verify_runtime_init
MODE=FILTER
SYMBOLS=
CLI_OVERRIDES=
if [ "$1" = "ALL" ]; then
if [ $# -ne 1 ]; then
_die "ALL cannot be combined with selectors or symbol overrides" 2
fi
MODE=ALL
shift
set --
else
pending_isin=
pending_override_set=0
pending_override=
while [ $# -gt 0 ]; do
case "$1" in
--symbol=*)
[ -n "$pending_isin" ] || _die "$1 must follow an ISIN selector" 2
[ "$pending_override_set" -eq 0 ] || _die "multiple symbol overrides for ISIN $pending_isin" 2
pending_override_set=1
pending_override=${1#*=}
shift
;;
-S|--symbol)
[ -n "$pending_isin" ] || _die "$1 must follow an ISIN selector" 2
[ "$pending_override_set" -eq 0 ] || _die "multiple symbol overrides for ISIN $pending_isin" 2
shift
[ $# -gt 0 ] || _die "-S/--symbol requires a value" 2
pending_override_set=1
pending_override=$1
shift
;;
*)
if [ -n "$pending_isin" ]; then
_append_symbol "$pending_isin"
if [ "$pending_override_set" -eq 1 ]; then
if _is_crypto_symbol "$pending_isin"; then
_append_override "$pending_isin" "$TR2GF_CRYPTO_SOURCE" "$pending_override"
else
_append_override "$pending_isin" "$TR2GF_YAHOO_SOURCE" "$pending_override"
fi
fi
fi
pending_isin=$1
pending_override_set=0
pending_override=
shift
;;
esac
done
if [ -n "$pending_isin" ]; then
_append_symbol "$pending_isin"
if [ "$pending_override_set" -eq 1 ]; then
if _is_crypto_symbol "$pending_isin"; then
_append_override "$pending_isin" "$TR2GF_CRYPTO_SOURCE" "$pending_override"
else
_append_override "$pending_isin" "$TR2GF_YAHOO_SOURCE" "$pending_override"
fi
fi
fi
fi
MERGED_MAPS=$TR2GF_SYMBOL_MAPS
if [ -n "$CLI_OVERRIDES" ]; then
_log "Applying CLI symbol overrides: $CLI_OVERRIDES" 2
if [ -n "$MERGED_MAPS" ]; then
MERGED_MAPS="${MERGED_MAPS}|${CLI_OVERRIDES}"
else
MERGED_MAPS=$CLI_OVERRIDES
fi
fi
_log "Execution mode: $MODE" 2
if [ -n "$SYMBOLS" ]; then
_log "Selected symbols: $SYMBOLS" 2
fi
_log "Starting AWK conversion for source '$TR2GF_SOURCE_FILE'." 2
_log "Merged symbol/source mappings: $MERGED_MAPS" 1
TMP_AWK="${TMPDIR:-/tmp}/tr_export_$$.awk"
trap 'rm -f "$TMP_AWK"' EXIT HUP INT TERM
cat >"$TMP_AWK" <<'AWK'
function csv_parse(line, out, i, n, ch, nextch, field, quoted) {
delete out
n = 0
field = ""
quoted = 0
for (i = 1; i <= length(line); i++) {
ch = substr(line, i, 1)
if (quoted) {
if (ch == "\"") {
nextch = substr(line, i + 1, 1)
if (nextch == "\"") {
field = field "\""
i++
} else {
quoted = 0
}
} else {
field = field ch
}
} else {
if (ch == "\"") {
quoted = 1
} else if (ch == ",") {
out[++n] = field
field = ""
} else {
field = field ch
}
}
}
out[++n] = field
return n
}
function csv_quote(s, t) {
t = s
gsub(/"/, """", t)
return "\"" t "\""
}
function trim(s) {
sub(/^[[:space:]]+/, "", s)
sub(/[[:space:]]+$/, "", s)
return s
}
function sanitize_filename(s, t) {
t = s
gsub(/[[:space:]]+/, "-", t)
gsub(/[^[:alnum:].-]+/, "-", t)
gsub(/-+/, "-", t)
sub(/^-/, "", t)
sub(/-$/, "", t)
if (t == "") t = "unnamed"
return t
}
function absval(x) {
return (x < 0) ? -x : x
}
function shares_abs(s, x) {
x = trim(s) + 0
if (x < 0) x = -x
return sprintf("%.10f", x)
}
function crypto_default_code(symbol) {
if (symbol == "BTC") return "bitcoin"
else if (symbol == "ETH") return "ethereum"
else if (symbol == "SOL") return "solana"
else if (symbol == "ADA") return "cardano"
else if (symbol == "XRP") return "ripple"
else if (symbol == "DOT") return "polkadot"
else if (symbol == "DOGE") return "dogecoin"
else if (symbol == "AVAX") return "avalanche-2"
else if (symbol == "LINK") return "chainlink"
else if (symbol == "MATIC") return "matic-network"
return symbol
}
function detect_data_source(symbol) {
if (symbol in forced_source) return forced_source[symbol]
if (symbol == "BTC" || symbol == "ETH" || symbol == "SOL" || symbol == "ADA" || symbol == "XRP" || symbol == "DOT" || symbol == "DOGE" || symbol == "AVAX" || symbol == "LINK" || symbol == "MATIC") return default_crypto_source
return default_yahoo_source
}
function map_code(symbol, source, key) {
key = symbol SUBSEP source
if (key in forced_code && forced_code[key] != "") return forced_code[key]
if (source == default_crypto_source) return crypto_default_code(symbol)
return symbol
}
function append_record(symbol, line) {
if (!(symbol in record_count)) {
record_count[symbol] = 0
records[symbol] = ""
}
record_count[symbol]++
if (record_count[symbol] > 1) records[symbol] = records[symbol] "\n"
records[symbol] = records[symbol] line
}
BEGIN {
want_all = (mode == "ALL") ? 1 : 0
if (!want_all && symbols != "") {
nsel = split(symbols, selected, "|")
for (i = 1; i <= nsel; i++) wanted[selected[i]] = 1
}
if (maps != "") {
nmap = split(maps, map_entries, "|")
for (i = 1; i <= nmap; i++) {
n = split(map_entries[i], parts, ":")
if (n >= 3) {
symbol_key = parts[1]
source_key = parts[2]
code_value = substr(map_entries[i], length(parts[1]) + length(parts[2]) + 3)
forced_source[symbol_key] = source_key
forced_code[symbol_key SUBSEP source_key] = code_value
}
}
}
}
NR == 1 {
nf = csv_parse($0, hdr)
for (i = 1; i <= nf; i++) col[trim(hdr[i])] = i
required["date"] = 1
required["type"] = 1
required["symbol"] = 1
required["name"] = 1
required["shares"] = 1
required["price"] = 1
required["fee"] = 1
required["currency"] = 1
for (k in required) {
if (!(k in col)) {
printf("Error: missing required column: %s\n", k) > "/dev/stderr"
exit 1
}
}
next
}
{
nf = csv_parse($0, row)
date = trim(row[col["date"]])
typ = trim(row[col["type"]])
symbol = trim(row[col["symbol"]])
name = trim(row[col["name"]])
shares = trim(row[col["shares"]])
price = trim(row[col["price"]])
fee = trim(row[col["fee"]])
currency = trim(row[col["currency"]])
typ_u = toupper(typ)
if (symbol == "") next
if (!want_all && !(symbol in wanted)) next
cancel_key = symbol SUBSEP date SUBSEP shares_abs(shares)
if (typ_u == "BUY_CANCELLED") {
cancelled[cancel_key]++
next
}
if (typ_u != "BUY" && typ_u != "SELL") next
if (typ_u == "BUY" && cancelled[cancel_key] > 0) {
cancelled[cancel_key]--
next
}
count[symbol]++
last_name[symbol] = name
safe_name[symbol] = sanitize_filename(name)
if (count_only == 1) next
action = (typ_u == "BUY") ? "buy" : "sell"
fee_value = (fee == "") ? 0 : absval(fee + 0)
note = "ISIN " symbol
data_source = detect_data_source(symbol)
export_code = map_code(symbol, data_source)
line = sprintf("%s,%s,%s,%s,%s,%s,%s,%.2f,%s,%s", date, export_code, data_source, currency, price, shares, action, fee_value, csv_quote(account_name), csv_quote(note))
append_record(symbol, line)
if (!(symbol in latest_date) || date > latest_date[symbol]) latest_date[symbol] = date
}
END {
found = 0
for (s in count) found = 1
if (count_only == 1) {
if (want_all) {
for (s in count) printf("%s;%d;%s\n", s, count[s], last_name[s])
} else {
for (i = 1; i <= nsel; i++) {
s = selected[i]
printf("%s;%d;%s\n", s, ((s in count) ? count[s] : 0), ((s in last_name) ? last_name[s] : ""))
}
}
exit 0
}
if (!found) {
print "No matching BUY/SELL transactions found." > "/dev/stderr"
exit 1
}
for (s in records) {
prefix = latest_date[s]
gsub(/-/, "", prefix)
prefix = substr(prefix, 3, 6)
filename = output_dir "/" prefix "_" s "_" safe_name[s] ".csv"
print "Date,Code,DataSource,Currency,Price,Quantity,Action,Fee,Account,Note" > filename
print records[s] >> filename
close(filename)
printf("written;%s;%d;%s\n", filename, count[s], last_name[s])
}
}
AWK
if awk \
-v mode="$MODE" \
-v symbols="$SYMBOLS" \
-v maps="$MERGED_MAPS" \
-v count_only="$TR2GF_COUNT_ONLY" \
-v output_dir="$TR2GF_OUTPUT_DIR" \
-v account_name="$TR2GF_ACCOUNT_NAME" \
-v default_yahoo_source="$TR2GF_YAHOO_SOURCE" \
-v default_crypto_source="$TR2GF_CRYPTO_SOURCE" \
-f "$TMP_AWK" \
"$TR2GF_SOURCE_FILE"
then
_log "AWK conversion finished successfully." 2
exit 0
else
rc=$?
_log "AWK conversion failed with exit code $rc." 4
exit "$rc"
fi
+81
View File
@@ -0,0 +1,81 @@
# tr2gf configuration example
# Copy this file to 'vars' and adjust as needed.
# Warning: do not edit vars.example directly!
# ------------------------------------------------------------------
# GENERAL
# ------------------------------------------------------------------
# Trade Republic source CSV used as default input file
#TR2GF_SOURCE_FILE=./TradeRepublic_TransactionExport.csv
# Output directory for generated Ghostfolio import files
#TR2GF_OUTPUT_DIR=./exports
# Account name written to the export CSV
#TR2GF_ACCOUNT_NAME=DEFAULT
# Batch mode:
# Leave empty for interactive/normal console output.
# Set to any non-empty string to suppress notices on stdout.
#TR2GF_BATCH=
# ------------------------------------------------------------------
# DATA SOURCES
# ------------------------------------------------------------------
# Default data source for non-crypto assets
#TR2GF_YAHOO_SOURCE=YAHOO
# Default data source for supported crypto assets
#TR2GF_CRYPTO_SOURCE=COINGECKO
# ------------------------------------------------------------------
# LOGGING
# ------------------------------------------------------------------
# Directory for tr2gf log files
#TR2GF_LOG_DIR=./log
# Main log file
#TR2GF_LOG_FILE=./log/tr2gf.log
# Log level controls verbosity of logging output:
# 0 = off : Disable all logging output
# 1 = debug : Detailed diagnostics for troubleshooting and development
# 2 = info : Informational messages about normal operations
# 3 = warning : Warnings about unusual but non-fatal conditions
# 4 = error : Errors affecting functionality
# 5 = critical : Severe failures requiring immediate attention
#TR2GF_LOG_LEVEL=2
# ------------------------------------------------------------------
# SYMBOL / CODE OVERRIDES
# ------------------------------------------------------------------
# Format:
# SYMBOL:CODE:SOURCE
#
# SOURCE is optional. If missing or empty, tr2gf selects
# TR2GF_CRYPTO_SOURCE for supported crypto symbols and
# TR2GF_YAHOO_SOURCE for all other symbols.
#
# Examples:
# IE00BJ5JPG56:ICHN.AS:YAHOO
# IE00BHZRQZ17:FLXI.L:YAHOO
# BTC:bitcoin:COINGECKO
# ETH:ethereum:COINGECKO
# IE00BJ5JPG56:ICHN.AS
# BTC:bitcoin:
#
# Notes:
# - Empty lines and lines starting with '#' are ignored.
# - CLI parameters override values from this file.
# - For YAHOO assets without an override, the ISIN/symbol is used as Code.
# - For supported crypto assets without an override, built-in CoinGecko mappings apply.
#IE00BJ5JPG56:ICHN.AS:YAHOO
#IE00BHZRQZ17:FLXI.L:YAHOO
#BTC:bitcoin:COINGECKO
#ETH:ethereum:COINGECKO
#SOL:solana: