Initial project commit
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user