Add trading journal and portfolio tracking

This commit is contained in:
kai
2026-09-09 19:01:30 +02:00
parent 1db3b008ec
commit c683bc74c0
24 changed files with 1043 additions and 8 deletions
+7
View File
@@ -0,0 +1,7 @@
<div class="table-scroll"><table><thead><tr><th>Datum</th><th>Position</th><th>Typ</th><th>Stückzahl</th><th>Kurs</th><th>Währung</th><th>Gebühren</th><th>Gesamtbetrag</th><th>Source</th><th>Strategie</th>{% if show_balance %}<th>Bestand vorher</th><th>Bestand danach</th>{% endif %}<th>Notiz</th><th>Aktionen</th></tr></thead>
<tbody>{% for entry in entries %}<tr>
<td class="nowrap">{{ entry.date[8:10] }}.{{ entry.date[5:7] }}.{{ entry.date[:4] }}</td><td><a href="/trading/assets/{{ entry.asset_id }}">{{ entry.asset }}</a></td><td><span class="badge {{ 'positive' if entry.transaction_type == 'buy' else 'muted' }}">{{ trade_types[entry.transaction_type] }}</span></td>
<td class="numeric">{{ entry.quantity|replace('.', ',') }}</td><td class="numeric">{{ entry.price_per_unit|replace('.', ',') }}</td><td>{{ entry.currency }}</td><td class="numeric">{{ entry.fees|decimal }}</td><td class="numeric">{{ entry.total_amount|decimal }}</td><td><span class="tag">{{ sources[entry.source] }}</span></td><td><span class="tag">{{ strategies.get(entry.strategy_tag, 'Ohne Tag') }}</span></td>
{% if show_balance %}<td class="numeric">{{ entry.quantity_before|replace('.', ',') }}</td><td class="numeric">{{ entry.quantity_after|replace('.', ',') }}</td>{% endif %}
<td class="note">{{ entry.note or '' }}</td><td><div class="actions"><a href="/trading/transactions/{{ entry.id }}/edit">Bearbeiten</a><form method="post" action="/trading/transactions/{{ entry.id }}/delete" data-confirm="Diese Transaktion wirklich löschen? Der Bestand wird neu berechnet."><button class="danger small">Löschen</button></form></div></td></tr>
{% else %}<tr><td colspan="{{ 14 if show_balance else 12 }}" class="empty">Noch keine Transaktionen. Trage einen Kauf mit bekanntem Datum und Kurs ein.</td></tr>{% endfor %}</tbody></table></div>
+1
View File
@@ -0,0 +1 @@
<nav class="trading-nav" aria-label="Trading-Navigation"><a href="/trading">Trading-Übersicht</a><a href="/trading/positions">Positionen</a><a href="/trading/transactions">Alle Transaktionen</a><a href="/export/trading.csv">Trading-CSV</a></nav>
+3
View File
@@ -0,0 +1,3 @@
<div class="table-scroll"><table><thead><tr><th>Position</th><th>Stückzahl</th><th>Ø Einstand</th><th>Investiertes Kapital</th><th>Währung</th><th>Käufe</th><th>Verkäufe</th><th>Realisiert G/V</th></tr></thead><tbody>
{% for position in positions %}<tr><td><a href="/trading/assets/{{ position.asset_id }}">{{ position.asset }}</a></td><td class="numeric">{{ position.quantity|replace('.', ',') }}</td><td class="numeric">{{ position.average_cost|decimal(8) }}</td><td class="numeric">{{ position.invested_capital|decimal }}</td><td>{{ position.currency }}</td><td>{{ position.buy_count }}</td><td>{{ position.sell_count }}</td><td class="numeric {{ 'negative' if position.realized_profit_loss.startswith('-') else 'positive' }}">{{ position.realized_profit_loss|decimal }}</td></tr>
{% else %}<tr><td colspan="8" class="empty">Noch keine offenen Positionen aus erfassten Käufen.</td></tr>{% endfor %}</tbody></table></div>
+3 -2
View File
@@ -7,13 +7,14 @@
<script defer src="{{ url_for('static', path='js/forms.js') }}"></script>
</head>
<body>
<header class="topbar"><a class="brand" href="/">💶 Finance Dashboard</a><nav aria-label="Hauptnavigation"><a href="/">Übersicht</a><a href="/income">Alle Zahlungen</a><a href="/export/income.csv">CSV-Export</a></nav></header>
<header class="topbar"><a class="brand" href="/">💶 Finance Dashboard</a><nav aria-label="Hauptnavigation"><a href="/">Übersicht</a><a href="/income">Alle Zahlungen</a><a href="/trading">Trading-Buch</a><a href="/export/income.csv">CSV-Export</a></nav></header>
<main>
{% set message = request.query_params.get('message') %}
{% if message in ['saved', 'deleted'] %}<p class="notice success" role="status">{{ 'Zahlung gespeichert.' if message == 'saved' else 'Zahlung gelöscht.' }}</p>{% endif %}
{% if message in ['trade_saved', 'trade_deleted'] %}<p class="notice success" role="status">{{ 'Transaktion gespeichert.' if message == 'trade_saved' else 'Transaktion gelöscht.' }}</p>{% endif %}
{% block content %}{% endblock %}
</main>
<footer>pinguAurora meldet sich zum Dienst. <span>Private Finanzen · Beträge in EUR</span></footer>
<footer>pinguAurora meldet sich zum Dienst. <span>Private Finanzen · Währungen separat</span></footer>
{% block scripts %}{% endblock %}
</body>
</html>
+25
View File
@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block title %}Trading-Buch · Finance Dashboard{% endblock %}
{% block content %}
{% include '_trading_nav.html' %}
<div class="page-heading"><div><p class="eyebrow">DEPOT & TRANSAKTIONEN</p><h1>Trading-Buch</h1><p class="muted">Deine Käufe, Verkäufe und offenen Einstandswerte.</p></div><a class="button" href="/trading/transactions/new">+ Transaktion eintragen</a></div>
<form class="filters" method="get"><label>Auswertungswährung<select name="currency">{% for currency in stats.currencies %}<option {{ 'selected' if currency == stats.currency else '' }}>{{ currency }}</option>{% endfor %}</select></label><button>Auswerten</button><span class="muted">Alle Kennzahlen und Diagramme nur in {{ stats.currency }} · keine Währungsumrechnung</span></form>
<div class="kpi-grid">
<article class="kpi"><h2>Investiertes Kapital</h2><strong>{{ stats.invested_capital|decimal }} {{ stats.currency }}</strong><small>Einstand der offenen Positionen inkl. Gebühren</small></article>
<article class="kpi"><h2>Aktive Positionen</h2><strong>{{ stats.active_positions }}</strong><small>Mit positivem Bestand</small></article>
<article class="kpi"><h2>Käufe dieses Jahr</h2><strong>{{ stats.buys_current_year }}</strong><small>Anzahl Kauftransaktionen</small></article>
<article class="kpi"><h2>Verkäufe dieses Jahr</h2><strong>{{ stats.sells_current_year }}</strong><small>Anzahl Verkaufstransaktionen</small></article>
<article class="kpi"><h2>Realisiert G/V dieses Jahr</h2><strong class="{{ 'negative' if stats.realized_profit_loss_current_year.startswith('-') else 'positive' }}">{{ stats.realized_profit_loss_current_year|decimal }} {{ stats.currency }}</strong><small>Nach Gebühren · Durchschnittseinstand</small></article>
<article class="kpi"><h2>Transaktionen gesamt</h2><strong>{{ stats.transactions_total }}</strong><small>Alle Jahre · {{ stats.currency }}</small></article>
</div>
<p class="notice warning">Portfolioanalyse mit gleitendem Durchschnittseinstand. Keine deutsche steuerliche FIFO-Berechnung.</p>
<section class="panel"><div class="section-heading"><h2>Aktuelle Positionen · {{ stats.currency }}</h2><a href="/trading/positions">Alle Währungen →</a></div>{% set positions = stats.positions %}{% include '_trading_positions.html' %}</section>
<div class="chart-grid">
{% for field,title in [('by_source','Investiertes Kapital nach Source'),('by_strategy','Investiertes Kapital nach Strategie')] %}
<section class="panel"><h2>{{ title }}</h2><p class="muted">Offener Einstand · Verkäufe reduzieren ursprüngliche Anteile proportional.</p><div class="chart"><canvas id="{{ field }}" role="img" aria-label="{{ title }}; Werte in der nachfolgenden Tabelle"></canvas></div><div class="table-scroll"><table><thead><tr><th>{{ 'Source' if field == 'by_source' else 'Strategie' }}</th><th>Betrag ({{ stats.currency }})</th><th>Anteil</th></tr></thead><tbody>{% for row in stats[field] %}<tr><td><span class="tag">{{ sources[row.key] if field == 'by_source' else strategies.get(row.key, 'Ohne Tag') }}</span></td><td class="numeric">{{ row.amount|decimal }}</td><td class="numeric">{{ row.percentage|decimal if row.percentage is not none else '' }}{{ ' %' if row.percentage is not none else '' }}</td></tr>{% endfor %}</tbody></table></div></section>
{% endfor %}
<section class="panel chart-wide"><h2>Käufe pro Monat und Jahr</h2><div class="chart"><canvas id="trading-monthly" role="img" aria-label="Anzahl Käufe nach Monat und Jahr"></canvas></div><details><summary>Monatswerte anzeigen</summary><div class="table-scroll"><table><thead><tr><th>Jahr</th>{% for month in months %}<th>{{ month }}</th>{% endfor %}</tr></thead><tbody>{% for year in stats.monthly %}<tr><th>{{ year.year }}</th>{% for count in year.counts %}<td>{{ count }}</td>{% endfor %}</tr>{% endfor %}</tbody></table></div></details></section>
</div><p class="muted" id="trading-chart-status">Diagramme werden geladen. Alle Werte sind auch als Tabellen verfügbar.</p>
<section class="panel"><div class="section-heading"><h2>Letzte Transaktionen · alle Währungen</h2><a href="/trading/transactions">Vollständige Historie →</a></div>{% include '_trading_entries.html' %}</section>
{% endblock %}
{% block scripts %}<script id="trading-data" type="application/json">{{ {'stats':stats, 'sources':sources, 'strategies':strategies, 'months':months}|tojson }}</script><script defer src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script><script defer src="{{ url_for('static', path='js/trading.js') }}"></script>{% endblock %}
+4
View File
@@ -0,0 +1,4 @@
{% extends 'base.html' %}{% block title %}{{ asset.name }} · Trading-Buch{% endblock %}{% block content %}
{% include '_trading_nav.html' %}<div class="page-heading"><div><h1>{{ asset.name }}</h1><p class="muted">{{ asset.ticker or 'Kein Ticker' }} · {{ asset_types[asset.asset_type] }}</p></div><a class="button" href="/trading/transactions/new?asset_id={{ asset.id }}">+ Transaktion eintragen</a></div>
{% if position %}<div class="kpi-grid">{% for label,value in [('Stückzahl',position.quantity|replace('.',',')),('Ø Einstand',position.average_cost|decimal(8)),('Investiertes Kapital',position.invested_capital|decimal),('Realisiert G/V',position.realized_profit_loss|decimal),('Summe Käufe inkl. Gebühren',position.total_buys|decimal),('Summe Verkäufe nach Gebühren',position.total_sells|decimal)] %}<article class="kpi"><h2>{{ label }}</h2><strong>{{ value }}</strong><small>{{ position.currency if label != 'Stückzahl' else 'Stück' }}</small></article>{% endfor %}</div><p class="muted">Erste Buchung: {{ position.first_transaction }} · Letzte Buchung: {{ position.last_transaction }} · {{ position.buy_count }} Käufe / {{ position.sell_count }} Verkäufe</p>{% else %}<p class="notice">Noch keine erfassten Trades. Es wurden keine Anfangsbestände oder Preise angenommen.</p>{% endif %}
<section class="panel"><h2>Vollständige Positionshistorie</h2>{% set show_balance = true %}{% include '_trading_entries.html' %}</section>{% endblock %}
+1
View File
@@ -0,0 +1 @@
{% extends 'base.html' %}{% block content %}{% include '_trading_nav.html' %}<section class="panel form-panel"><h1>Neue Trading-Position</h1>{% if error %}<p class="notice negative">{{ error }}</p>{% endif %}<form method="post" class="entry-form"><label>Name<input name="name" required maxlength="150" value="{{ data.get('name','') }}"></label><label>Ticker (optional)<input name="ticker" maxlength="30" value="{{ data.get('ticker','') }}"></label><label>Asset-Typ<select name="asset_type">{% for key in ['stock','etf','bond','crypto'] %}<option value="{{ key }}" {{ 'selected' if data.get('asset_type') == key else '' }}>{{ asset_types[key] }}</option>{% endfor %}</select></label><div class="actions"><button>Position speichern</button><a href="/trading/transactions/new">Zurück</a></div></form></section>{% endblock %}
+1
View File
@@ -0,0 +1 @@
{% extends 'base.html' %}{% block content %}<section class="panel form-panel"><h1>Transaktion nicht geändert</h1><p class="notice negative" role="alert">{{ error }}</p><a href="/trading/transactions">Zurück zur Historie</a></section>{% endblock %}
+15
View File
@@ -0,0 +1,15 @@
{% extends 'base.html' %}{% block title %}Transaktion {{ 'bearbeiten' if transaction_id else 'eintragen' }}{% endblock %}
{% block content %}{% include '_trading_nav.html' %}<section class="form-panel panel"><h1>Transaktion {{ 'bearbeiten' if transaction_id else 'eintragen' }}</h1>{% if error %}<p class="notice negative" role="alert">{{ error }}</p>{% endif %}
<form class="entry-form" method="post">
<label>Datum<input type="date" name="date" required value="{{ data.get('date','') }}"></label>
<label>Position<select name="asset_id" required><option value="">Bitte auswählen</option>{% for asset in assets %}<option value="{{ asset.id }}" {{ 'selected' if asset.id|string == data.get('asset_id')|string else '' }}>{{ asset.name }}{{ ' (inaktiv nur Verkauf/Bestandskorrektur)' if not asset.active else '' }}</option>{% endfor %}</select></label><a href="/trading/assets/new">+ Neue Position</a>
<label>Typ<select name="transaction_type">{% for key,label in trade_types.items() %}<option value="{{ key }}" {{ 'selected' if data.get('transaction_type') == key else '' }}>{{ label }}</option>{% endfor %}</select></label>
<label>Stückzahl<input name="quantity" required inputmode="decimal" maxlength="25" value="{{ data.get('quantity','') }}" placeholder="0,092262"></label>
<label>Kurs je Stück<input name="price_per_unit" required inputmode="decimal" maxlength="25" value="{{ data.get('price_per_unit','') }}" placeholder="Bekannten Kauf-/Verkaufskurs eingeben"></label>
<label>Währung<input name="currency" required minlength="3" maxlength="3" value="{{ data.get('currency','EUR') }}" placeholder="EUR"><small>Einheitliche Währung je Position. Keine automatische Umrechnung.</small></label>
<label>Gebühren<input name="fees" required inputmode="decimal" maxlength="15" value="{{ data.get('fees','0') }}"></label>
<label>Source<select name="source">{% for key,label in sources.items() %}<option value="{{ key }}" {{ 'selected' if data.get('source') == key else '' }}>{{ label }}</option>{% endfor %}</select></label>
<label>Strategie-Tag<select name="strategy_tag"><option value="">Ohne Tag</option>{% for key,label in strategies.items() %}<option value="{{ key }}" {{ 'selected' if data.get('strategy_tag') == key else '' }}>{{ label }}</option>{% endfor %}</select></label>
<label>Notiz<textarea name="note" maxlength="2000" rows="3">{{ data.get('note') or '' }}</textarea></label>
<p class="muted">Stückzahl und Kurs: bis zu 12 Nachkommastellen; Gebühren: maximal 2. Komma und Punkt werden akzeptiert. Käufe/Verkäufe am gleichen Tag werden nach Erfassungsreihenfolge verrechnet.</p>
<div class="actions"><button>Transaktion speichern</button><a href="/trading">Abbrechen</a></div></form></section>{% endblock %}
+9
View File
@@ -0,0 +1,9 @@
{% extends 'base.html' %}{% block title %}Transaktionshistorie · Trading-Buch{% endblock %}{% block content %}
{% include '_trading_nav.html' %}<div class="page-heading"><h1>Alle Transaktionen</h1><a class="button" href="/trading/transactions/new">+ Transaktion eintragen</a></div>
<section class="panel"><form method="get" class="filters">
<label>Jahr<select name="year"><option value="">Alle Jahre</option>{% for year in years %}<option value="{{ year }}" {{ 'selected' if filters.get('year') == year else '' }}>{{ year }}</option>{% endfor %}</select></label>
<label>Monat<select name="month"><option value="">Alle Monate</option>{% for month in months %}<option value="{{ loop.index }}" {{ 'selected' if filters.get('month') == loop.index else '' }}>{{ month }}</option>{% endfor %}</select></label>
<label>Position<select name="asset_id"><option value="">Alle Positionen</option>{% for asset in assets %}<option value="{{ asset.id }}" {{ 'selected' if filters.get('asset_id') == asset.id else '' }}>{{ asset.name }}</option>{% endfor %}</select></label>
{% for field,label,options in [('transaction_type','Typ',trade_types), ('source','Source',sources), ('strategy_tag','Strategie',strategies)] %}<label>{{ label }}<select name="{{ field }}"><option value="">Alle</option>{% for key,title in options.items() %}<option value="{{ key }}" {{ 'selected' if filters.get(field) == key else '' }}>{{ title }}</option>{% endfor %}{% if field == 'strategy_tag' %}<option value="untagged" {{ 'selected' if filters.get(field) == 'untagged' else '' }}>Ohne Tag</option>{% endif %}</select></label>{% endfor %}
<button>Filtern</button><a href="/trading/transactions">Zurücksetzen</a></form>
{% include '_trading_entries.html' %}<div class="pagination">{% if page > 1 %}<a href="{{ request.url.include_query_params(page=page-1) }}">← Zurück</a>{% endif %}<span>Seite {{ page }} · bis zu 100 Transaktionen</span>{% if more %}<a href="{{ request.url.include_query_params(page=page+1) }}">Weiter →</a>{% endif %}</div></section>{% endblock %}
+2
View File
@@ -0,0 +1,2 @@
{% extends 'base.html' %}{% block title %}Positionen · Trading-Buch{% endblock %}
{% block content %}{% include '_trading_nav.html' %}<div class="page-heading"><div><h1>Aktuelle Positionen</h1><p class="muted">Offene Bestände in ihrer jeweiligen Währung · Durchschnittseinstand inklusive Kaufgebühren</p></div><a class="button" href="/trading/transactions/new">+ Transaktion eintragen</a></div><section class="panel">{% include '_trading_positions.html' %}</section>{% endblock %}