From 4532410911ddf30b2adf55c9b73739a181657a2f Mon Sep 17 00:00:00 2001 From: kakulukia Date: Fri, 13 Jun 2025 08:23:41 +0200 Subject: [PATCH 1/2] first attempt of fixing https://github.com/BitBoxSwiss/bitbox-wallet-app/issues/3147 --- .envrc | 4 + .gitignore | 5 +- Makefile | 4 + backend/accounts/transaction.go | 104 +++++++++++++- backend/accounts/transaction_test.go | 70 ++++++---- backend/chart.go | 102 +++++++++++--- frontends/web/src/api/account.ts | 5 +- .../routes/account/summary/chart.module.css | 18 +++ .../web/src/routes/account/summary/chart.tsx | 129 ++++++++++-------- .../src/routes/account/summary/filters.tsx | 10 +- .../web/src/routes/account/summary/types.ts | 2 + scripts/air.toml | 52 +++++++ 12 files changed, 394 insertions(+), 111 deletions(-) create mode 100644 .envrc create mode 100644 scripts/air.toml diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000..fb401b2bf1 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +export PATH="$PATH:$HOME/Qt/6.8.2/macos/bin" +export PATH="$PATH:$HOME/Qt/6.8.2/macos/libexec" +export PATH="$PATH:/usr/local/opt/go@1.23/bin" +export PATH="$PATH:$HOME/go/bin" \ No newline at end of file diff --git a/.gitignore b/.gitignore index b3169c5196..9849f5a1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,7 @@ tags !/frontends/web/src/locales/sl/ !/frontends/web/src/locales/tr/ !/frontends/web/src/locales/zh/ -!/frontends/web/src/locales/cs/ \ No newline at end of file +!/frontends/web/src/locales/cs/ +.aider* +build-errors.log +servewallet diff --git a/Makefile b/Makefile index 40f675f02b..9cd7b135af 100644 --- a/Makefile +++ b/Makefile @@ -30,6 +30,10 @@ gomobileinit: gomobile init servewallet: go run -mod=vendor ./cmd/servewallet +servewallet-reload: + @echo "Starting servewallet with auto-reload on Go file changes..." + @command -v air >/dev/null 2>&1 || { echo "Installing air for auto-reload..."; go install github.com/air-verse/air@latest; } + air -c scripts/air.toml servewallet-mainnet: go run -mod=vendor ./cmd/servewallet -mainnet servewallet-regtest: diff --git a/backend/accounts/transaction.go b/backend/accounts/transaction.go index 3278154417..4767c865b2 100644 --- a/backend/accounts/transaction.go +++ b/backend/accounts/transaction.go @@ -201,8 +201,15 @@ func NewOrderedTransactions(txs []*TransactionData) OrderedTransactions { // TimeseriesEntry contains the balance of the account at the given time. type TimeseriesEntry struct { - Time time.Time - Value coin.Amount + Time time.Time + Value coin.Amount + SumFiatAtTime *big.Rat +} + +// RatesProvider provides access to current and historical exchange rates. +type RatesProvider interface { + LatestPrice() map[string]map[string]float64 + HistoricalPriceAt(coinCode string, fiat string, timestamp time.Time) float64 } // MarshalJSON serializes the entry as JSON. @@ -238,12 +245,84 @@ func (txs OrderedTransactions) EarliestTime() (time.Time, error) { // Timeseries chunks the time between `start` and `end` into steps of `interval` duration, and // provides the balance of the account at each step. func (txs OrderedTransactions) Timeseries( - start, end time.Time, interval time.Duration) ([]TimeseriesEntry, error) { + start, end time.Time, + interval time.Duration, + ratesProvider RatesProvider, + coinCode string, + fiat string, + coinDecimals *big.Int) ([]TimeseriesEntry, error) { + for _, tx := range txs { if tx.isConfirmed() && tx.Timestamp == nil { return nil, errp.WithStack(errors.ErrNotAvailable) } } + + // Calculate sumFiatAtTime using historical prices (like in chart.go) - only if all parameters are provided + fiatTimeMap := make(map[int64]*big.Rat) // Map: timestamp -> SumFiatAtTime + if len(txs) > 0 && ratesProvider != nil && coinCode != "" && fiat != "" && coinDecimals != nil { + cumulativeFiatAtTime := new(big.Rat) + + // Iterate through transactions from oldest to newest (reverse order) + for i := len(txs) - 1; i >= 0; i-- { + tx := txs[i] + + // Skip unconfirmed transactions + if !tx.isConfirmed() || tx.Timestamp == nil { + continue + } + + // Get historical price at transaction time (like in chart.go) + historicalPrice := ratesProvider.HistoricalPriceAt(coinCode, fiat, *tx.Timestamp) + + // Calculate coin amount change for this transaction + var coinAmountChange *big.Rat + switch tx.Type { + case TxTypeReceive: + if tx.Status != TxStatusFailed { + coinAmountChange = new(big.Rat).SetFrac(tx.Amount.BigInt(), coinDecimals) + } else { + coinAmountChange = new(big.Rat) + } + case TxTypeSend: + if tx.Status != TxStatusFailed { + coinAmountChange = new(big.Rat).SetFrac(tx.Amount.BigInt(), coinDecimals) + coinAmountChange.Neg(coinAmountChange) // Negative for send + } else { + coinAmountChange = new(big.Rat) + } + case TxTypeSendSelf: + coinAmountChange = new(big.Rat) + default: + coinAmountChange = new(big.Rat) + } + + // Calculate fiat value at historical price (sumFiatAtTime) + fiatValueAtTime := new(big.Rat).Mul(coinAmountChange, new(big.Rat).SetFloat64(historicalPrice)) + + // Add to cumulative sumFiatAtTime + cumulativeFiatAtTime.Add(cumulativeFiatAtTime, fiatValueAtTime) + + // Rundungskorrektur: Wenn Coin-Balance praktisch 0 ist, setze auch Fiat auf 0 + currentBalance := txs[i].Balance + balanceInt, _ := currentBalance.Int64() + if balanceInt == 0 { + // Wenn die aktuelle Balance 0 ist, sollte auch der Fiat-Wert 0 sein + cumulativeFiatAtTime.SetInt64(0) + } else { + // Kleine Rundungsfehler korrigieren (< 1 Cent) + absValue := new(big.Rat).Abs(cumulativeFiatAtTime) + if absValue.Cmp(big.NewRat(1, 100)) < 0 { + cumulativeFiatAtTime.SetInt64(0) + } + } + + // Store in map for later lookup + // fmt.Printf("DEBUG Timeseries - timestamp: %v, SumFiatAtTime: %v\n", tx.Timestamp.UTC(), cumulativeFiatAtTime) + fiatTimeMap[tx.Timestamp.Unix()] = new(big.Rat).Set(cumulativeFiatAtTime) + } + } + currentTime := start if currentTime.IsZero() { return nil, nil @@ -260,15 +339,29 @@ func (txs OrderedTransactions) Timeseries( return tx.Timestamp.Before(currentTime) || tx.Timestamp.Equal(currentTime) }) var value coin.Amount + var sumFiatAtTime *big.Rat + if nextIndex == len(txs) { value = coin.NewAmountFromInt64(0) + sumFiatAtTime = new(big.Rat) // Zero value } else { value = txs[nextIndex].Balance + // Find the corresponding SumFiatAtTime value + if txs[nextIndex].Timestamp != nil { + if fiatValue, exists := fiatTimeMap[txs[nextIndex].Timestamp.Unix()]; exists { + sumFiatAtTime = new(big.Rat).Set(fiatValue) + } else { + sumFiatAtTime = new(big.Rat) // Zero if not found + } + } else { + sumFiatAtTime = new(big.Rat) // Zero for unconfirmed + } } result = append(result, TimeseriesEntry{ - Time: currentTime, - Value: value, + Time: currentTime, + Value: value, + SumFiatAtTime: sumFiatAtTime, }) currentTime = currentTime.Add(interval) @@ -276,5 +369,6 @@ func (txs OrderedTransactions) Timeseries( break } } + return result, nil } diff --git a/backend/accounts/transaction_test.go b/backend/accounts/transaction_test.go index 6c2ebcb828..32b1b7424e 100644 --- a/backend/accounts/transaction_test.go +++ b/backend/accounts/transaction_test.go @@ -17,6 +17,7 @@ package accounts import ( "testing" "time" + "math/big" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/coins/coin" "github.com/stretchr/testify/require" @@ -110,60 +111,77 @@ func TestOrderedTransactions(t *testing.T) { time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC), time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC), 24*time.Hour, + nil, // ratesProvider - nil für Tests + "", // coinCode - leer für Tests + "", // fiat - leer für Tests + nil, // coinDecimals - nil für Tests ) require.NoError(t, err) require.Equal(t, []TimeseriesEntry{ { - Time: time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(0), + Time: time.Date(2020, 9, 9, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(0), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 10, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(200), + Time: time.Date(2020, 9, 10, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(200), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 11, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(190), + Time: time.Date(2020, 9, 11, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(190), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 12, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(190), + Time: time.Date(2020, 9, 12, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(190), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 13, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(190), + Time: time.Date(2020, 9, 13, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(190), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 14, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(190), + Time: time.Date(2020, 9, 14, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(190), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 15, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(290), + Time: time.Date(2020, 9, 15, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(290), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 16, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(290), + Time: time.Date(2020, 9, 16, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(290), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 17, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(290), + Time: time.Date(2020, 9, 17, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(290), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 18, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(290), + Time: time.Date(2020, 9, 18, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(290), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 19, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(290), + Time: time.Date(2020, 9, 19, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(290), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 20, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(590), + Time: time.Date(2020, 9, 20, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(590), + SumFiatAtTime: new(big.Rat), }, { - Time: time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC), - Value: coin.NewAmountFromInt64(589), + Time: time.Date(2020, 9, 21, 13, 0, 0, 0, time.UTC), + Value: coin.NewAmountFromInt64(589), + SumFiatAtTime: new(big.Rat), }, }, timeseries) } diff --git a/backend/chart.go b/backend/chart.go index 5f61bee536..8f4f224e31 100644 --- a/backend/chart.go +++ b/backend/chart.go @@ -44,12 +44,16 @@ type ChartEntry struct { Time int64 `json:"time"` Value float64 `json:"value"` FormattedValue string `json:"formattedValue"` + Amount float64 `json:"amount"` // Coin-Betrag + Percent float64 `json:"percent"` // Wird 0 bleiben + FiatAtTime float64 `json:"fiatAtTime"` // Fiat-Wert basierend auf historischem Preis } // RatChartEntry exploits composition to extend ChartEntry and save high precision values. type RatChartEntry struct { ChartEntry - RatValue *big.Rat + RatValue *big.Rat + SumFiatAtTime *big.Rat // Umbenannt von FiatAtTime } // Chart has all data needed to show a time-based chart of their assets to the user. @@ -82,26 +86,50 @@ func (backend *Backend) addChartData( chartEntries map[int64]RatChartEntry, ) { for _, e := range timeseries { - price := backend.RatesUpdater().HistoricalPriceAt( - string(coinCode), - fiat, - e.Time) timestamp := e.Time.Unix() chartEntry := chartEntries[timestamp] chartEntry.Time = timestamp - fiatValue := new(big.Rat).Mul( - new(big.Rat).SetFrac( - e.Value.BigInt(), - coinDecimals, - ), - new(big.Rat).SetFloat64(price), - ) + + // Portfolio-Wert zu historischem Preis (wie bisher) + coinAmount := new(big.Rat).SetFrac(e.Value.BigInt(), coinDecimals) + price := backend.RatesUpdater().HistoricalPriceAt(string(coinCode), fiat, e.Time) + fiatValue := new(big.Rat).Mul(coinAmount, new(big.Rat).SetFloat64(price)) + + // Verwende SumFiatAtTime direkt aus TimeseriesEntry + var sumFiatAtTime *big.Rat + if e.SumFiatAtTime != nil { + sumFiatAtTime = new(big.Rat).Set(e.SumFiatAtTime) + } else { + sumFiatAtTime = new(big.Rat) // Fallback: 0 + } + + // Berechne Percent: currentFiatBalance / sumFiatAtTime + var percentValue float64 + fiatValueFloat, _ := fiatValue.Float64() + sumFiatAtTimeFloat, _ := sumFiatAtTime.Float64() + if sumFiatAtTimeFloat != 0 { + percentValue = (fiatValueFloat / sumFiatAtTimeFloat) * 100 + } if chartEntry.RatValue == nil { chartEntry.RatValue = new(big.Rat).Set(fiatValue) + chartEntry.SumFiatAtTime = sumFiatAtTime + chartEntry.Percent = percentValue } else { - chartEntry.RatValue.Add(fiatValue, chartEntry.RatValue) + chartEntry.RatValue.Add(chartEntry.RatValue, fiatValue) + if chartEntry.SumFiatAtTime == nil { + chartEntry.SumFiatAtTime = sumFiatAtTime + } else { + chartEntry.SumFiatAtTime.Add(chartEntry.SumFiatAtTime, sumFiatAtTime) + } + + // Recalculate percent for aggregated values + newFiatValueFloat, _ := chartEntry.RatValue.Float64() + newSumFiatAtTimeFloat, _ := chartEntry.SumFiatAtTime.Float64() + if newSumFiatAtTimeFloat != 0 { + chartEntry.Percent = (newFiatValueFloat / newSumFiatAtTimeFloat) * 100 + } } chartEntries[timestamp] = chartEntry } @@ -203,6 +231,10 @@ func (backend *Backend) ChartData() (*Chart, error) { earliestTxTime.Truncate(24*time.Hour), until, 24*time.Hour, + backend.RatesUpdater(), + string(account.Coin().Code()), + fiat, + coinDecimals, ) if errp.Cause(err) == errors.ErrNotAvailable { backend.log.WithField("coin", account.Coin().Code()).Info("ChartDataMissing") @@ -216,6 +248,10 @@ func (backend *Backend) ChartData() (*Chart, error) { hourlyFrom, until, time.Hour, + backend.RatesUpdater(), + string(account.Coin().Code()), + fiat, + coinDecimals, ) if errp.Cause(err) == errors.ErrNotAvailable { backend.log.WithField("coin", account.Coin().Code()).Info("ChartDataMissing") @@ -234,31 +270,61 @@ func (backend *Backend) ChartData() (*Chart, error) { toSortedSlice := func(s map[int64]RatChartEntry, fiat string) []ChartEntry { result := make([]ChartEntry, len(s)) i := 0 - // Discard the RatValue, which is not used anymore for _, entry := range s { floatValue, _ := entry.RatValue.Float64() + var marketPerformanceValue float64 + var percentValue float64 + + if entry.SumFiatAtTime != nil { + marketPerformanceValue, _ = entry.SumFiatAtTime.Float64() + // Berechne Prozent: (aktueller Wert / Marktwert) * 100 - 100 + if marketPerformanceValue != 0 { + percentValue = (floatValue / marketPerformanceValue * 100) - 100 + } + } + result[i] = ChartEntry{ Time: entry.Time, Value: floatValue, FormattedValue: coin.FormatAsCurrency(entry.RatValue, fiat), + Amount: floatValue, + Percent: percentValue, // Jetzt wird der echte Prozentwert berechnet! + FiatAtTime: marketPerformanceValue, } i++ } sort.Slice(result, func(i, j int) bool { return result[i].Time < result[j].Time }) // Manually add the last point with the current total, to make the last point match. - // The last point might not match the account total otherwise because: - // 1) unconfirmed tx are not in the timeseries - // 2) coingecko might not have rates yet up until after all transactions, so they'd also be - // missing form the timeseries (`until` is up to 2h in the past). if isUpToDate && !currentTotalMissing { total, _ := currentTotal.Float64() + + // Hole FiatAtTime vom letzten ChartEntry + var lastFiatAtTime float64 + var lastPercent float64 + + if len(result) > 0 { + lastEntry := result[len(result)-1] + lastFiatAtTime = lastEntry.FiatAtTime + + // Berechne Prozent: (aktueller Marktwert / FiatAtTime) * 100 - 100 + if lastFiatAtTime != 0 { + lastPercent = (total / lastFiatAtTime * 100) - 100 + } + } else { + lastFiatAtTime = total // Fallback + } + result = append(result, ChartEntry{ Time: time.Now().Unix(), Value: total, FormattedValue: coin.FormatAsCurrency(currentTotal, fiat), + Amount: total, + Percent: lastPercent, // Korrekt berechneter Prozentwert + FiatAtTime: lastFiatAtTime, }) } + // Truncate leading zeroes, if there are any keep the first one to start the chart with 0 for i, e := range result { if e.Value > 0 { diff --git a/frontends/web/src/api/account.ts b/frontends/web/src/api/account.ts index fb5614bc01..4e6e41af77 100644 --- a/frontends/web/src/api/account.ts +++ b/frontends/web/src/api/account.ts @@ -200,7 +200,10 @@ export const init = (code: AccountCode): Promise => { }; export type FormattedLineData = LineData & { - formattedValue: string; + formattedValue: string; // used for drawing tha chart + amount: number; // original amount for sum display + percent: number; // percent value for the percent display type + fiatAtTime: number; // Fiat at time value to use as base value for a shorter timeframe display than "all data" }; export type ChartData = FormattedLineData[]; diff --git a/frontends/web/src/routes/account/summary/chart.module.css b/frontends/web/src/routes/account/summary/chart.module.css index 5ee251b2be..4978279f62 100644 --- a/frontends/web/src/routes/account/summary/chart.module.css +++ b/frontends/web/src/routes/account/summary/chart.module.css @@ -137,3 +137,21 @@ white-space: nowrap; } +.toggleWrapper { + display: flex; + align-items: center; + margin: 0 var(--space-half); + height: 28.5px; +} + +.toggleSymbol { + font-size: 1.2rem; + font-weight: bold;; + margin: 0 var(--space-half); + color: var(--color-secondary); +} + +.toggleSymbolActive { + color: var(--color-primary); +} + diff --git a/frontends/web/src/routes/account/summary/chart.tsx b/frontends/web/src/routes/account/summary/chart.tsx index fbc04739e1..433b20ed67 100644 --- a/frontends/web/src/routes/account/summary/chart.tsx +++ b/frontends/web/src/routes/account/summary/chart.tsx @@ -31,7 +31,7 @@ import { AmountUnit } from '@/components/amount/amount-with-unit'; import styles from './chart.module.css'; type TProps = { - data?: TSummary; + data?: TSummary; // <- Hier kommen die Chart-Daten rein noDataPlaceholder?: JSX.Element; hideAmounts?: boolean; }; @@ -155,6 +155,7 @@ export const Chart = ({ const [tooltipData, setTooltipData] = useState<{ toolTipVisible: boolean; toolTipValue?: string; + toolTipPercent?: string; // added percent toolTipTop: number; toolTipLeft: number; toolTipTime: number; @@ -163,8 +164,19 @@ export const Chart = ({ toolTipTop: 0, toolTipLeft: 0, toolTipTime: 0, + toolTipPercent: undefined, // init }); + const [showPercent, setShowPercent] = useState(false); + + // Hilfsfunktion zum Filtern der Chart-Daten basierend auf showPercent + const getFilteredChartData = useCallback((chartData: ChartData) => { + return chartData.map(entry => ({ + ...entry, + value: showPercent ? entry.percent : entry.amount // <-- Hier setzen wir value basierend auf showPercent + })); + }, [showPercent]); + useEffect(() => { setTooltipData({ toolTipVisible: false, @@ -195,7 +207,7 @@ export const Chart = ({ const displayWeek = () => { if (source !== 'hourly' && lineSeries.current && data.chartDataHourly && chart.current) { - lineSeries.current.setData(data.chartDataHourly || []); + lineSeries.current.setData(getFilteredChartData(data.chartDataHourly || [])); // <-- Hier anwenden setFormattedData(data.chartDataHourly || []); chart.current.applyOptions({ timeScale: { timeVisible: true } }); } @@ -205,7 +217,7 @@ export const Chart = ({ const displayMonth = () => { if (source !== 'daily' && lineSeries.current && data.chartDataDaily && chart.current) { - lineSeries.current.setData(data.chartDataDaily || []); + lineSeries.current.setData(getFilteredChartData(data.chartDataDaily || [])); // <-- Hier anwenden setFormattedData(data.chartDataDaily || []); chart.current.applyOptions({ timeScale: { timeVisible: false } }); } @@ -215,7 +227,7 @@ export const Chart = ({ const displayYear = () => { if (source !== 'daily' && lineSeries.current && data.chartDataDaily && chart.current) { - lineSeries.current.setData(data.chartDataDaily); + lineSeries.current.setData(getFilteredChartData(data.chartDataDaily)); // <-- Hier anwenden setFormattedData(data.chartDataDaily); chart.current.applyOptions({ timeScale: { timeVisible: false } }); } @@ -225,7 +237,7 @@ export const Chart = ({ const displayAll = () => { if (source !== 'daily' && lineSeries.current && data.chartDataDaily && chart.current) { - lineSeries.current.setData(data.chartDataDaily); + lineSeries.current.setData(getFilteredChartData(data.chartDataDaily)); // <-- Hier anwenden setFormattedData(data.chartDataDaily); chart.current.applyOptions({ timeScale: { timeVisible: false } }); } @@ -275,39 +287,26 @@ export const Chart = ({ const logicalrange = chart.current.timeScale().getVisibleLogicalRange() as LogicalRange; const visiblerange = lineSeries.current.barsInLogicalRange(logicalrange); if (!visiblerange) { - // if the chart is empty, during first load, barsInLogicalRange is null return; } const rangeFrom = Math.max(Math.floor(visiblerange.barsBefore), 0); if (!chartData[rangeFrom]) { - // when data series have changed it triggers subscribeVisibleLogicalRangeChange - // but at this point the setVisibleRange has not executed what the new range - // should be and therefore barsBefore might still point to the old range - // so we have to ignore this call and expect setVisibleRange with correct range setDifference(0); setDiffSince(''); return; } - // data should always have at least two data points and when the first - // value is 0 we take the next value as valueFrom to calculate valueDiff + + // Portfolio-Performance (bestehende Logik) const valueFrom = chartData[rangeFrom].value === 0 ? chartData[rangeFrom + 1].value : chartData[rangeFrom].value; const valueTo = data.chartTotal; const valueDiff = valueTo ? valueTo - valueFrom : 0; setDifference(valueDiff / valueFrom); + setDiffSince(`${chartData[rangeFrom].formattedValue} (${renderDate(Number(chartData[rangeFrom].time) * 1000, i18n.language, source)})`); }, [data, i18n.language, source]); - const removeChart = useCallback(() => { - if (chartInitialized.current) { - chart.current?.timeScale().unsubscribeVisibleLogicalRangeChange(calculateChange); - chart.current?.unsubscribeCrosshairMove(handleCrosshair); - chart.current?.remove(); - chart.current = undefined; - chartInitialized.current = false; - } - }, [calculateChange]); - - const handleCrosshair = ({ + // Moved handleCrosshair before removeChart to satisfy hook dependencies + const handleCrosshair = useCallback(({ point, time, seriesData @@ -322,43 +321,37 @@ export const Chart = ({ || point.x < 0 || point.x > parent.clientWidth || point.y < 0 || point.y > parent.clientHeight ) { - setTooltipData((tooltipData) => ({ - ...tooltipData, - toolTipVisible: false - })); + setTooltipData(td => ({ ...td, toolTipVisible: false })); return; } const price = seriesData.get(lineSeries.current) as LineData