Merge pull request #18 from GoncaloRodri/feature/nav-cleanup

Feature/nav cleanup
This commit is contained in:
Gonçalo Rodrigues 2026-06-13 19:31:12 +01:00 committed by GitHub
commit b1c9609f42
9 changed files with 157 additions and 60 deletions

View File

@ -186,6 +186,8 @@ type storeIface interface {
updateGoal(ctx context.Context, id, userID string, update bson.M) error updateGoal(ctx context.Context, id, userID string, update bson.M) error
deleteGoal(ctx context.Context, id, userID string) error deleteGoal(ctx context.Context, id, userID string) error
seedCategories(ctx context.Context, userID string) error seedCategories(ctx context.Context, userID string) error
getTickerMappings(ctx context.Context, userID string) ([]TickerMapping, error)
saveTickerMapping(ctx context.Context, userID, isin, ticker string) error
getHousehold(ctx context.Context, userID string) (*Household, error) getHousehold(ctx context.Context, userID string) (*Household, error)
createHousehold(ctx context.Context, h *Household) error createHousehold(ctx context.Context, h *Household) error
deleteHousehold(ctx context.Context, userID string) error deleteHousehold(ctx context.Context, userID string) error
@ -460,7 +462,7 @@ func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
var portfolioHoldings []Holding var portfolioHoldings []Holding
var portfolioPricesAvailable bool var portfolioPricesAvailable bool
if trades, err := h.store.getTrades(ctx, a.UserID); err == nil && len(trades) > 0 { if trades, err := h.store.getTrades(ctx, a.UserID); err == nil && len(trades) > 0 {
prices, _ := fetchPricesByISIN(uniqueISINs(trades)) prices, _ := fetchPricesByISIN(uniqueISINs(trades), nil)
holdings := computeHoldings(trades, prices) holdings := computeHoldings(trades, prices)
pr := aggregatePortfolio(holdings) pr := aggregatePortfolio(holdings)
portfolioHoldings = pr.Holdings portfolioHoldings = pr.Holdings
@ -1330,11 +1332,26 @@ func (h *Handler) Portfolio(w http.ResponseWriter, r *http.Request) {
return return
} }
prices, err := fetchPricesByISIN(isins) // load user-saved ISIN→ticker overrides
customMappings, _ := h.store.getTickerMappings(ctx, a.UserID)
custom := make(map[string]string, len(customMappings))
for _, m := range customMappings {
custom[m.ISIN] = m.Ticker
}
prices, err := fetchPricesByISIN(isins, custom)
if err != nil { if err != nil {
slog.Error("fetch prices", "err", err) slog.Error("fetch prices", "err", err)
} }
// collect ISINs for which we got no price
var missingPrices []string
for _, isin := range isins {
if prices[isin] == 0 {
missingPrices = append(missingPrices, isin)
}
}
holdings := computeHoldings(trades, prices) holdings := computeHoldings(trades, prices)
pr := aggregatePortfolio(holdings) pr := aggregatePortfolio(holdings)
@ -1348,9 +1365,27 @@ func (h *Handler) Portfolio(w http.ResponseWriter, r *http.Request) {
TotalCostCents: pr.TotalCost, TotalCostCents: pr.TotalCost,
TotalPCLCents: pr.TotalPCL, TotalPCLCents: pr.TotalPCL,
TotalPCLPct: pr.PCLPct, TotalPCLPct: pr.PCLPct,
MissingPrices: missingPrices,
}) })
} }
func (h *Handler) SaveTickerMapping(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
a := getAuth(r)
isin := strings.TrimSpace(r.FormValue("isin"))
ticker := strings.TrimSpace(r.FormValue("ticker"))
if isin == "" || ticker == "" {
http.Error(w, "isin and ticker required", http.StatusBadRequest)
return
}
if err := h.store.saveTickerMapping(ctx, a.UserID, isin, ticker); err != nil {
slog.Error("save ticker mapping", "err", err)
http.Error(w, "save error", http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/portfolio", http.StatusSeeOther)
}
func (h *Handler) ImportSecurities(w http.ResponseWriter, r *http.Request) { func (h *Handler) ImportSecurities(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
a := getAuth(r) a := getAuth(r)
@ -1884,7 +1919,7 @@ func (h *Handler) NetWorth(w http.ResponseWriter, r *http.Request) {
var portfolioCents int64 var portfolioCents int64
var pricesAvailable bool var pricesAvailable bool
if trades, err2 := h.store.getTrades(ctx, a.UserID); err2 == nil && len(trades) > 0 { if trades, err2 := h.store.getTrades(ctx, a.UserID); err2 == nil && len(trades) > 0 {
prices, _ := fetchPricesByISIN(uniqueISINs(trades)) prices, _ := fetchPricesByISIN(uniqueISINs(trades), nil)
holdings := computeHoldings(trades, prices) holdings := computeHoldings(trades, prices)
pr := aggregatePortfolio(holdings) pr := aggregatePortfolio(holdings)
for _, p := range prices { for _, p := range prices {
@ -2414,6 +2449,7 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("POST /import/preview", h.ImportPreview) mux.HandleFunc("POST /import/preview", h.ImportPreview)
mux.HandleFunc("POST /import/confirm", h.ImportConfirm) mux.HandleFunc("POST /import/confirm", h.ImportConfirm)
mux.HandleFunc("POST /import/securities", h.ImportSecurities) mux.HandleFunc("POST /import/securities", h.ImportSecurities)
mux.HandleFunc("POST /portfolio/ticker", h.SaveTickerMapping)
mux.HandleFunc("POST /accounts", h.Accounts) mux.HandleFunc("POST /accounts", h.Accounts)
mux.HandleFunc("DELETE /accounts/{id}", h.Accounts) mux.HandleFunc("DELETE /accounts/{id}", h.Accounts)
mux.HandleFunc("POST /categories", h.Categories) mux.HandleFunc("POST /categories", h.Categories)

View File

@ -154,6 +154,11 @@ func (m *mockStore) deleteGoal(_ context.Context, id, _ string) error {
} }
func (m *mockStore) seedCategories(_ context.Context, _ string) error { return nil } func (m *mockStore) seedCategories(_ context.Context, _ string) error { return nil }
func (m *mockStore) getTickerMappings(_ context.Context, _ string) ([]TickerMapping, error) {
return nil, nil
}
func (m *mockStore) saveTickerMapping(_ context.Context, _, _, _ string) error { return nil }
func (m *mockStore) getHousehold(_ context.Context, _ string) (*Household, error) { func (m *mockStore) getHousehold(_ context.Context, _ string) (*Household, error) {
return nil, fmt.Errorf("not found") return nil, fmt.Errorf("not found")
} }

View File

@ -207,6 +207,8 @@ type PortfolioData struct {
TotalPCLCents int64 TotalPCLCents int64
TotalPCLPct float64 TotalPCLPct float64
RealizedPCLCents int64 RealizedPCLCents int64
// ISINs for which no price could be fetched (so user can supply a ticker)
MissingPrices []string
} }
type SharingData struct { type SharingData struct {

View File

@ -13,6 +13,7 @@ import (
type TickerMapping struct { type TickerMapping struct {
ISIN string `bson:"_id" json:"isin"` ISIN string `bson:"_id" json:"isin"`
UserID string `bson:"user_id" json:"user_id"`
Ticker string `bson:"ticker" json:"ticker"` Ticker string `bson:"ticker" json:"ticker"`
} }
@ -24,6 +25,7 @@ var isinToTicker = map[string]string{
"IE00B3XXRP09": "VWRL.AS", // VWRL — All-World "IE00B3XXRP09": "VWRL.AS", // VWRL — All-World
"IE00BK5BQT80": "VWRA.L", // VWRA — All-World (acc, LSE) "IE00BK5BQT80": "VWRA.L", // VWRA — All-World (acc, LSE)
// iShares // iShares
"IE00B3WJKG14": "QDVE.DE", // QDVE — S&P 500 Information Technology
"IE00B4L5Y983": "EUNL.DE", // EUNL — MSCI World "IE00B4L5Y983": "EUNL.DE", // EUNL — MSCI World
"IE00B5BMR087": "SXR8.DE", // SXR8 — S&P 500 "IE00B5BMR087": "SXR8.DE", // SXR8 — S&P 500
"IE00B4K48X80": "SXRV.DE", // SXRV — S&P 500 EUR hedged "IE00B4K48X80": "SXRV.DE", // SXRV — S&P 500 EUR hedged
@ -105,7 +107,7 @@ func computeHoldings(trades []Trade, prices map[string]int64) []Holding {
currentPrice := prices[isin] currentPrice := prices[isin]
currentValue := int64(float64(currentPrice) * a.shares / 100) currentValue := int64(float64(currentPrice) * a.shares)
unrealizedPCL := currentValue - a.cost unrealizedPCL := currentValue - a.cost
pct := 0.0 pct := 0.0
if a.cost > 0 { if a.cost > 0 {
@ -149,10 +151,10 @@ type TickerStore interface {
Load() error Load() error
} }
// fetchPricesByISIN resolves each ISIN to a Yahoo Finance ticker (via isinToTicker), // fetchPricesByISIN resolves each ISIN to a Yahoo Finance ticker, checking
// fetches the current market price, and returns a map keyed by ISIN. // custom (user-saved) mappings first, then the hardcoded isinToTicker map,
// ISINs with no known ticker mapping are tried directly as a ticker (fallback). // then falling back to the raw ISIN as a last resort.
func fetchPricesByISIN(isins []string) (map[string]int64, error) { func fetchPricesByISIN(isins []string, custom map[string]string) (map[string]int64, error) {
if len(isins) == 0 { if len(isins) == 0 {
return map[string]int64{}, nil return map[string]int64{}, nil
} }
@ -161,13 +163,21 @@ func fetchPricesByISIN(isins []string) (map[string]int64, error) {
client := &http.Client{Timeout: 10 * time.Second} client := &http.Client{Timeout: 10 * time.Second}
for _, isin := range isins { for _, isin := range isins {
ticker, ok := isinToTicker[isin] ticker := custom[isin]
if !ok { if ticker == "" {
ticker = isinToTicker[isin]
}
if ticker == "" {
ticker = isin // last-resort fallback ticker = isin // last-resort fallback
} }
url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s", ticker) url := fmt.Sprintf("https://query1.finance.yahoo.com/v8/finance/chart/%s", ticker)
resp, err := client.Get(url) req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
continue
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; homelab-finance/1.0)")
resp, err := client.Do(req)
if err != nil { if err != nil {
slog.Warn("price fetch failed", "isin", isin, "ticker", ticker, "err", err) slog.Warn("price fetch failed", "isin", isin, "ticker", ticker, "err", err)
continue continue

View File

@ -328,6 +328,36 @@ var defaultCategories = []struct {
{"Other", "#9E9E9E"}, {"Other", "#9E9E9E"},
} }
func (s *Store) tickerMappings() *mgmongo.Collection {
return s.db.Collection("finance_ticker_mappings")
}
func (s *Store) getTickerMappings(ctx context.Context, userID string) ([]TickerMapping, error) {
ctx, span := mongo.StartSpan(ctx, "Store.getTickerMappings")
defer span.End()
cur, err := s.tickerMappings().Find(ctx, bson.M{"user_id": userID})
if err != nil {
return nil, err
}
defer cur.Close(ctx)
var items []TickerMapping
if err := cur.All(ctx, &items); err != nil {
return nil, err
}
return items, nil
}
func (s *Store) saveTickerMapping(ctx context.Context, userID, isin, ticker string) error {
ctx, span := mongo.StartSpan(ctx, "Store.saveTickerMapping")
defer span.End()
_, err := s.tickerMappings().UpdateOne(ctx,
bson.M{"_id": isin, "user_id": userID},
bson.M{"$set": bson.M{"ticker": ticker, "user_id": userID}},
options.UpdateOne().SetUpsert(true),
)
return err
}
func (s *Store) households() *mgmongo.Collection { func (s *Store) households() *mgmongo.Collection {
return s.db.Collection("finance_households") return s.db.Collection("finance_households")
} }

View File

@ -8,51 +8,51 @@
<style> <style>
/* ── Tokens ─────────────────────────────────────────────────────── */ /* ── Tokens ─────────────────────────────────────────────────────── */
:root { :root {
--bg: #0f1117; --bg: #080c10;
--bg2: #161b27; --bg2: #0d1318;
--bg3: #1e2535; --bg3: #131c24;
--surface: rgba(30, 37, 53, 0.85); --surface: rgba(13, 22, 32, 0.88);
--surface2: rgba(40, 50, 72, 0.7); --surface2: rgba(20, 32, 44, 0.75);
--border: rgba(255,255,255,0.07); --border: rgba(0,210,200,0.08);
--border2: rgba(255,255,255,0.12); --border2: rgba(0,210,200,0.15);
--text: #e8eaf6; --text: #dff4f2;
--text2: #9fa8c7; --text2: #7fb8b4;
--text3: #5c6585; --text3: #3d6e6a;
--accent: #6979f8; --accent: #00c9b8;
--accent2: #8b9ffc; --accent2: #33d9ca;
--accent-glow: rgba(105,121,248,0.25); --accent-glow: rgba(0,201,184,0.22);
--green: #4ade80; --green: #00e5b0;
--red: #f87171; --red: #f87171;
--green-dim: rgba(74,222,128,0.15); --green-dim: rgba(0,229,176,0.12);
--red-dim: rgba(248,113,113,0.15); --red-dim: rgba(248,113,113,0.13);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.4), 0 1px 2px rgba(0,0,0,0.3); --shadow-sm: 0 1px 3px rgba(0,0,0,0.5), 0 1px 2px rgba(0,0,0,0.4);
--shadow-md: 0 4px 16px rgba(0,0,0,0.5), 0 2px 6px rgba(0,0,0,0.3); --shadow-md: 0 4px 16px rgba(0,0,0,0.6), 0 2px 6px rgba(0,0,0,0.4);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.6), 0 4px 12px rgba(0,0,0,0.4); --shadow-lg: 0 12px 40px rgba(0,0,0,0.7), 0 4px 12px rgba(0,0,0,0.5);
--radius: 14px; --radius: 14px;
--radius-sm: 8px; --radius-sm: 8px;
--nav-h: 58px; --nav-h: 58px;
} }
[data-theme="light"] { [data-theme="light"] {
--bg: #f0f2f8; --bg: #edf6f5;
--bg2: #e4e8f4; --bg2: #dceeed;
--bg3: #d8ddf0; --bg3: #cae5e3;
--surface: rgba(255,255,255,0.9); --surface: rgba(255,255,255,0.92);
--surface2: rgba(240,242,248,0.8); --surface2: rgba(237,246,245,0.85);
--border: rgba(0,0,0,0.07); --border: rgba(0,150,140,0.1);
--border2: rgba(0,0,0,0.12); --border2: rgba(0,150,140,0.18);
--text: #1a1f36; --text: #0d2422;
--text2: #4a5275; --text2: #2a6460;
--text3: #8a92b0; --text3: #6aadaa;
--accent: #4355e8; --accent: #00897b;
--accent2: #6373f0; --accent2: #00a896;
--accent-glow: rgba(67,85,232,0.18); --accent-glow: rgba(0,137,123,0.15);
--green: #16a34a; --green: #00796b;
--red: #dc2626; --red: #dc2626;
--green-dim: rgba(22,163,74,0.1); --green-dim: rgba(0,121,107,0.1);
--red-dim: rgba(220,38,38,0.1); --red-dim: rgba(220,38,38,0.1);
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.05); --shadow-sm: 0 1px 3px rgba(0,0,0,0.07), 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 16px rgba(0,0,0,0.1), 0 2px 6px rgba(0,0,0,0.06); --shadow-md: 0 4px 16px rgba(0,0,0,0.09), 0 2px 6px rgba(0,0,0,0.05);
--shadow-lg: 0 12px 40px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.07); --shadow-lg: 0 12px 40px rgba(0,0,0,0.11), 0 4px 12px rgba(0,0,0,0.06);
} }
/* ── Reset & base ────────────────────────────────────────────────── */ /* ── Reset & base ────────────────────────────────────────────────── */
@ -74,8 +74,8 @@
position: fixed; position: fixed;
inset: 0; inset: 0;
background-image: background-image:
radial-gradient(ellipse 80% 60% at 20% 10%, rgba(105,121,248,0.08) 0%, transparent 60%), radial-gradient(ellipse 80% 60% at 20% 10%, rgba(0,201,184,0.07) 0%, transparent 60%),
radial-gradient(ellipse 60% 50% at 80% 80%, rgba(139,159,252,0.05) 0%, transparent 55%); radial-gradient(ellipse 60% 50% at 80% 80%, rgba(0,150,140,0.04) 0%, transparent 55%);
pointer-events: none; pointer-events: none;
z-index: 0; z-index: 0;
} }

View File

@ -4,6 +4,23 @@
{{if $d.Holdings}} {{if $d.Holdings}}
{{if $d.MissingPrices}}
<div style="background:rgba(245,158,11,0.08); border:1px solid rgba(245,158,11,0.35); border-radius:12px; padding:16px 20px; margin-bottom:20px;">
<div style="font-weight:600; font-size:0.9rem; margin-bottom:12px;">⚠ Live price unavailable for {{len $d.MissingPrices}} holding{{if gt (len $d.MissingPrices) 1}}s{{end}} — add a Yahoo Finance ticker to fix this</div>
{{range $d.MissingPrices}}
<form method="post" action="/portfolio/ticker" style="display:flex; align-items:center; gap:8px; margin-bottom:8px;">
<input type="hidden" name="isin" value="{{.}}">
<code style="font-size:0.8rem; color:var(--muted); min-width:140px;">{{.}}</code>
<input type="text" name="ticker" placeholder="e.g. QDVE.DE" required
style="padding:6px 10px; border:1px solid var(--border); border-radius:8px; background:var(--bg); color:var(--text); font-size:0.85rem; width:130px;">
<button type="submit" style="padding:6px 14px; background:var(--accent); color:#fff; border:none; border-radius:8px; font-size:0.85rem; font-weight:600; cursor:pointer;">Save</button>
<a href="https://finance.yahoo.com/lookup/" target="_blank" rel="noopener"
style="font-size:0.78rem; color:var(--accent);">Look up ↗</a>
</form>
{{end}}
</div>
{{end}}
<div class="grid"> <div class="grid">
<div class="card value-card animate-on-scroll"> <div class="card value-card animate-on-scroll">
<h2>Total Value</h2> <h2>Total Value</h2>
@ -93,16 +110,17 @@ import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const palette = [ const palette = [
'#6979f8','#f87171','#fbbf24','#34d399','#a78bfa', '#00d4ff','#0099cc','#00b894','#007a63','#005f8a',
'#f472b6','#38bdf8','#fb923c','#4ade80','#e879f9', '#00e5cc','#0077b6','#48cae4','#023e8a','#00b4d8',
]; ];
const holdings = [ const holdings = [
{{range $d.Holdings}}{ name: "{{.Name}}", value: {{.CurrentValueCents}} },{{end}} {{range $d.Holdings}}{ name: "{{.Name}}", value: {{.CurrentValueCents}} },{{end}}
]; ].filter(h => h.value > 0);
const total = holdings.reduce((s, h) => s + h.value, 0); const total = holdings.reduce((s, h) => s + h.value, 0);
if (total <= 0) return;
if (total > 0) {
const container = document.getElementById('allocation3d'); const container = document.getElementById('allocation3d');
const W = container.clientWidth, H = 380; const W = container.clientWidth, H = 380;
@ -211,6 +229,8 @@ window.addEventListener('resize', () => {
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
renderer.setSize(w2, H); renderer.setSize(w2, H);
}); });
} // end if (total > 0)
</script> </script>
{{else}} {{else}}

Binary file not shown.

View File

@ -1,6 +0,0 @@
"datetime","date","account_type","category","type","asset_class","name","symbol","shares","price","amount","fee","tax","currency","original_amount","original_currency","fx_rate","description","transaction_id","counterparty_name","counterparty_iban","payment_reference","mcc_code"
"2025-12-11T08:08:30.375Z","2025-12-11","DEFAULT","TRADING","BUY","FUND","S&P 500 Information Tech USD (Acc)","IE00B3WJKG14","0.8289580000","36.1900000000","-30.00","-1.00","","EUR","","","","Buy trade IE00B3WJKG14 iShares S&P 500 Info Tech UCITS ETF USD (Acc), quantity: 0.828958","905f97ce-8c20-4891-8713-cb084b0cecb1","","","",""
"2025-12-11T08:09:16.732Z","2025-12-11","DEFAULT","TRADING","BUY","FUND","Core S&P 500 USD (Acc)","IE00B5BMR087","0.0287900000","625.1800000000","-18.00","-1.00","","EUR","","","","Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.02879","8bf8206a-a90e-4df4-98d5-3473dcf1c7d6","","","",""
"2026-02-26T07:53:08.309Z","2026-02-26","DEFAULT","TRADING","BUY","FUND","Core S&P 500 USD (Acc)","IE00B5BMR087","0.1586540000","630.3000000000","-100.00","-1.00","","EUR","","","","Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.158654","c154e2fd-7e13-43dc-b78f-48a0bf4b9fc6","","","",""
"2026-02-26T07:54:29.927Z","2026-02-26","DEFAULT","TRADING","BUY","FUND","Core MSCI World USD (Acc)","IE00B4L5Y983","0.8578050000","114.2500000000","-98.00","-1.00","","EUR","","","","Buy trade IE00B4L5Y983 iShares Core MSCI World UCITS ETF USD (Acc), quantity: 0.857805","7c79b9aa-82ce-48e7-bf93-20786b7977b5","","","",""
"2026-04-15T07:28:37.604Z","2026-04-15","DEFAULT","TRADING","BUY","FUND","Core S&P 500 USD (Acc)","IE00B5BMR087","0.1575100000","634.8800000000","-100.00","-1.00","","EUR","","","","Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.15751","4062d871-f968-483a-8f1f-16447c877201","","","",""
1 datetime date account_type category type asset_class name symbol shares price amount fee tax currency original_amount original_currency fx_rate description transaction_id counterparty_name counterparty_iban payment_reference mcc_code
2 2025-12-11T08:08:30.375Z 2025-12-11 DEFAULT TRADING BUY FUND S&P 500 Information Tech USD (Acc) IE00B3WJKG14 0.8289580000 36.1900000000 -30.00 -1.00 EUR Buy trade IE00B3WJKG14 iShares S&P 500 Info Tech UCITS ETF USD (Acc), quantity: 0.828958 905f97ce-8c20-4891-8713-cb084b0cecb1
3 2025-12-11T08:09:16.732Z 2025-12-11 DEFAULT TRADING BUY FUND Core S&P 500 USD (Acc) IE00B5BMR087 0.0287900000 625.1800000000 -18.00 -1.00 EUR Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.02879 8bf8206a-a90e-4df4-98d5-3473dcf1c7d6
4 2026-02-26T07:53:08.309Z 2026-02-26 DEFAULT TRADING BUY FUND Core S&P 500 USD (Acc) IE00B5BMR087 0.1586540000 630.3000000000 -100.00 -1.00 EUR Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.158654 c154e2fd-7e13-43dc-b78f-48a0bf4b9fc6
5 2026-02-26T07:54:29.927Z 2026-02-26 DEFAULT TRADING BUY FUND Core MSCI World USD (Acc) IE00B4L5Y983 0.8578050000 114.2500000000 -98.00 -1.00 EUR Buy trade IE00B4L5Y983 iShares Core MSCI World UCITS ETF USD (Acc), quantity: 0.857805 7c79b9aa-82ce-48e7-bf93-20786b7977b5
6 2026-04-15T07:28:37.604Z 2026-04-15 DEFAULT TRADING BUY FUND Core S&P 500 USD (Acc) IE00B5BMR087 0.1575100000 634.8800000000 -100.00 -1.00 EUR Buy trade IE00B5BMR087 iShares Core S&P 500 UCITS ETF USD (Acc), quantity: 0.15751 4062d871-f968-483a-8f1f-16447c877201