#!/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 "" _init_log if [ -f "$TR2GF/vars" ]; then _load_vars_file "$TR2GF/vars" _init_log _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