From d6293ba6dd0729b975d62d90f37d99f29b735299 Mon Sep 17 00:00:00 2001 From: Anton Stavnichiy Date: Fri, 24 May 2024 14:00:42 +0300 Subject: [PATCH] Add histogram chart and ETF fetchers for chart. --- .../project.pbxproj | 16 ++++- .../UnstoppableWallet/Models/Stats.swift | 1 + .../Modules/Chart/ChartUiView.swift | 30 ++++++++- .../MarketCards/MarketWideCardCell.swift | 2 +- .../CoinAnalyticsViewController.swift | 2 +- .../Modules/Coin/CoinChart/ChartModule.swift | 12 +++- .../Modules/Coin/CoinChartFactory.swift | 5 +- .../Modules/Market/Etf/MarketEtfView.swift | 4 +- .../MarketGlobal/MarketGlobalModule.swift | 2 + .../Etf/MarketEtfFetcher.swift | 49 ++++++++++++++ .../MetricChart/MetricChartFactory.swift | 65 +++++++++++++++++-- .../MetricChart/MetricChartModule.swift | 1 + .../MetricChart/MetricChartViewModel.swift | 12 ++++ .../UserInterface/Components/DiffLabel.swift | 5 ++ .../Extensions/ChartConfiguration.swift | 54 ++++++++++++--- .../en.lproj/Localizable.strings | 1 + 16 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/Etf/MarketEtfFetcher.swift diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 082916c4c1..0d078c21c8 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -2132,6 +2132,8 @@ 6BAAF34B2B9B245C00EFE5B2 /* SlideButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BAAF3462B9B245C00EFE5B2 /* SlideButton.swift */; }; 6BB14F6B2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */; }; 6BB14F6C2BF49E7100E879B2 /* WalletButtonHiddenManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */; }; + 6BB14F722BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */; }; + 6BB14F732BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */; }; 6BBCE4A32BDA419200ABBD55 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */; }; 6BBCE4A52BDA419B00ABBD55 /* Web3Wallet in Frameworks */ = {isa = PBXBuildFile; productRef = 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */; }; 6BCD53002A161F4100993F20 /* BackupCloudModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCD52F72A161F4100993F20 /* BackupCloudModule.swift */; }; @@ -4489,6 +4491,7 @@ 6BAAF3452B9B245C00EFE5B2 /* SlideButtonStyling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideButtonStyling.swift; sourceTree = ""; }; 6BAAF3462B9B245C00EFE5B2 /* SlideButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SlideButton.swift; sourceTree = ""; }; 6BB14F6A2BF49E7100E879B2 /* WalletButtonHiddenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletButtonHiddenManager.swift; sourceTree = ""; }; + 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketEtfFetcher.swift; sourceTree = ""; }; 6BCD52F72A161F4100993F20 /* BackupCloudModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupCloudModule.swift; sourceTree = ""; }; 6BCD52F92A161F4100993F20 /* ICloudBackupTermsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudBackupTermsViewModel.swift; sourceTree = ""; }; 6BCD52FA2A161F4100993F20 /* ICloudBackupTermsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ICloudBackupTermsViewController.swift; sourceTree = ""; }; @@ -7862,6 +7865,7 @@ 58AAAC5B31C9DD58D57A3EA9 /* MarketGlobalMetric */ = { isa = PBXGroup; children = ( + 6BB14F702BFE54F200E879B2 /* Etf */, 58AAA7B3EA0C8B9FDEC41837 /* MarketGlobalMetricService.swift */, 58AAA775FE9B46DA2910F508 /* MarketGlobalMetricModule.swift */, 58AAA02D981360FF0CC50A19 /* MarketGlobalMetricViewController.swift */, @@ -7975,6 +7979,14 @@ path = WidgetCoinAppShowWorker; sourceTree = ""; }; + 6BB14F702BFE54F200E879B2 /* Etf */ = { + isa = PBXGroup; + children = ( + 6BB14F712BFE550600E879B2 /* MarketEtfFetcher.swift */, + ); + path = Etf; + sourceTree = ""; + }; 6BCD52F62A161F4100993F20 /* ICloud */ = { isa = PBXGroup; children = ( @@ -10264,6 +10276,7 @@ D02A67C9272A7460009B2C1C /* CoinTweetsViewController.swift in Sources */, D36DE0DF272FD887000BC916 /* OneInchViewModel.swift in Sources */, D02A67C6272A7460009B2C1C /* CoinTweetsViewModel.swift in Sources */, + 6BB14F732BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, D09200C6293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */, 11B35F20127C070137781ED5 /* AddTokenModule.swift in Sources */, 11B35D722A70E8B4776AB5A8 /* AddBep2TokenBlockchainService.swift in Sources */, @@ -11832,6 +11845,7 @@ D36DE0C0272FD864000BC916 /* UniswapService.swift in Sources */, 11B35ED22837284580055F0A /* BalanceData.swift in Sources */, 11B3534B567884E30A871F32 /* AddTokenModule.swift in Sources */, + 6BB14F722BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, 11B35758262A961566ABB87F /* AddBep2TokenBlockchainService.swift in Sources */, 58AAADEE16D9605E4FA0390A /* UniswapSettingsViewModel.swift in Sources */, D09200C5293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */, @@ -13853,7 +13867,7 @@ repositoryURL = "https://github.com/horizontalsystems/Chart.Swift"; requirement = { kind = exactVersion; - version = 3.0.0; + version = 3.0.1; }; }; D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift b/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift index d07ba103ae..b679f245c0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/Stats.swift @@ -69,6 +69,7 @@ enum StatPage: String { case globalMetricsDefiCap = "global_metrics_defi_cap" case globalMetricsMarketCap = "global_metrics_market_cap" case globalMetricsTvlInDefi = "global_metrics_tvl_in_defi" + case globalMetricsEtf = "global_metrics_etf" case globalMetricsVolume = "global_metrics_volume" case guide case importFull = "import_full" diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift index 522e566005..07a7285035 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartUiView.swift @@ -223,11 +223,21 @@ class ChartUiView: UIView { CGSize(width: UIView.noIntrinsicMetric, height: totalHeight) } + func timePeriod(show: Bool) { + timePeriodView.isHidden = !show + timePeriodView.snp.updateConstraints { maker in + maker.height.equalTo(show ? CGFloat.heightCell48 : 0) + } + + updateConstraints() + } + var totalHeight: CGFloat { .heightDoubleLineCell + configuration.mainHeight + (configuration.showIndicatorArea ? configuration.indicatorHeight : 0) - + .margin8 + .heightCell48 + .margin8 + + (timePeriodView.isHidden ? 0 : (.margin8 + .heightCell48)) + + .margin8 } private func updateIntervals() { @@ -235,6 +245,8 @@ class ChartUiView: UIView { if viewModel.showAll { viewItems.append(.item(title: "chart.time_duration.all".localized)) } + + timePeriod(show: !viewItems.isEmpty) timePeriodView.reload(filters: viewItems) } @@ -284,6 +296,14 @@ class ChartUiView: UIView { } else { currentSecondaryDiffLabel.isHidden = true } + case let .custom(title, value): + currentSecondaryTitleLabel.isHidden = false + currentSecondaryValueLabel.isHidden = false + currentSecondaryDiffLabel.isHidden = true + + currentSecondaryTitleLabel.text = title + currentSecondaryValueLabel.text = value + currentSecondaryValueLabel.textColor = .themeLeah } if !chartView.isPressed { @@ -349,6 +369,14 @@ class ChartUiView: UIView { } else { chartSecondaryDiffLabel.isHidden = true } + case let .custom(title, value): + currentSecondaryTitleLabel.isHidden = false + currentSecondaryValueLabel.isHidden = false + chartSecondaryDiffLabel.isHidden = true + + chartSecondaryTitleLabel.text = title + chartSecondaryValueLabel.text = value + chartSecondaryValueLabel.textColor = .themeLeah case let .indicators(top, bottom): chartSecondaryTitleLabel.isHidden = false chartSecondaryValueLabel.isHidden = false diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift index 9348474f23..392cfb2df8 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/MarketCards/MarketWideCardCell.swift @@ -90,7 +90,7 @@ class MarketWideCardCell: BaseSelectableThemeCell { let chartConfiguration: ChartConfiguration switch chartCurveType { case .line: chartConfiguration = .previewChart - case .bars: chartConfiguration = .previewBarChart + case .bars, .histogram: chartConfiguration = .previewBarChart } showChartView(configuration: chartConfiguration) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift index 3ce9b747a9..9acebb325d 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift @@ -475,7 +475,7 @@ extension CoinAnalyticsViewController: SectionsDataSource { switch chartCurveType { case .line: chartTrend = viewItem.chartTrend - case .bars: chartTrend = .neutral + case .bars, .histogram: chartTrend = .neutral } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift index 1bde6072de..56e4ee1d30 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChart/ChartModule.swift @@ -12,18 +12,18 @@ enum ChartModule { let chartData: ChartData let indicators: [ChartIndicator] let chartTrend: MovementTrend - let chartDiff: Decimal? + let chartDiff: ValueDiff? let limitFormatter: ((Decimal) -> String?)? } struct SelectedPointViewItem { let value: String? - let diff: Decimal? + let diff: ValueDiff? let date: String let rightSideMode: RightSideMode - init(value: String?, diff: Decimal? = nil, date: String, rightSideMode: RightSideMode) { + init(value: String?, diff: ValueDiff? = nil, date: String, rightSideMode: RightSideMode) { self.value = value self.diff = diff self.date = date @@ -35,6 +35,7 @@ enum ChartModule { case none case volume(value: String?) case dominance(value: Decimal?, diff: Decimal?) + case custom(title: String, value: String?) case indicators(top: NSAttributedString?, bottom: NSAttributedString?) } } @@ -55,6 +56,11 @@ enum MovementTrend { } } +struct ValueDiff { + let value: String + let trend: MovementTrend +} + protocol IChartViewModel { var showAll: Bool { get } var intervals: [String] { get } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift index 2504427c04..a7745fb3c0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift @@ -51,6 +51,9 @@ class CoinChartFactory { } let chartData = ChartData(items: items, startWindow: firstPoint.timestamp, endWindow: lastPoint.timestamp) + let diff = (lastPoint.value - firstPoint.value) / firstPoint.value * 100 + let diffString = ValueFormatter.instance.format(percentValue: diff, showSign: true) + let valueDiff = diffString.map { ValueDiff(value: $0, trend: diff.isSignMinus ? .down : .up) } return ChartModule.ViewItem( value: ValueFormatter.instance.formatFull(currencyValue: CurrencyValue(currency: currency, value: item.rate)), valueDescription: nil, @@ -58,7 +61,7 @@ class CoinChartFactory { chartData: chartData, indicators: item.indicators, chartTrend: lastPoint.value > firstPoint.value ? .up : .down, - chartDiff: (lastPoint.value - firstPoint.value) / firstPoint.value * 100, + chartDiff: valueDiff, limitFormatter: { value in ValueFormatter.instance.formatFull(currency: currency, value: value) } ) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift index dee414e461..70dc8d5f94 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/Etf/MarketEtfView.swift @@ -12,7 +12,7 @@ struct MarketEtfView: View { init(isPresented: Binding) { _viewModel = StateObject(wrappedValue: MarketEtfViewModel()) - _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.instance(type: .totalMarketCap)) + _chartViewModel = StateObject(wrappedValue: MetricChartViewModel.etfInstance) _isPresented = isPresented } @@ -72,7 +72,7 @@ struct MarketEtfView: View { } @ViewBuilder private func chart() -> some View { - ChartView(viewModel: chartViewModel, configuration: .marketCapChart) + ChartView(viewModel: chartViewModel, configuration: .baseHistogramChart) .frame(maxWidth: .infinity) .onFirstAppear { chartViewModel.start() diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift index ff2fedff46..3244f5d978 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobal/MarketGlobalModule.swift @@ -4,6 +4,8 @@ import UIKit enum MarketGlobalModule { static let dominance = "dominance" + static let totalAssets = "total_assets" + static let totalInflow = "total_inflow" enum MetricsType: Identifiable { case totalMarketCap, volume24h, defiCap, tvlInDefi diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/Etf/MarketEtfFetcher.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/Etf/MarketEtfFetcher.swift new file mode 100644 index 0000000000..be4eef18f2 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Market/MarketGlobalMetric/Etf/MarketEtfFetcher.swift @@ -0,0 +1,49 @@ +import Chart +import Combine +import Foundation +import MarketKit + +class MarketEtfFetcher { + private let marketKit: MarketKit.Kit + private let currencyManager: CurrencyManager + + private let needUpdateSubject = PassthroughSubject() + + init(marketKit: MarketKit.Kit, currencyManager: CurrencyManager) { + self.marketKit = marketKit + self.currencyManager = currencyManager + } +} + +extension MarketEtfFetcher: IMetricChartFetcher { + var valueType: MetricChartModule.ValueType { + .compactCurrencyValue(currencyManager.baseCurrency) + } + + var needUpdatePublisher: AnyPublisher { + Empty().eraseToAnyPublisher() + } + + var intervals: [HsPeriodType] { [] } + + func fetch(interval _: HsPeriodType) async throws -> MetricChartModule.ItemData { + let points = try await marketKit.etfPoints(currencyCode: currencyManager.baseCurrency.code) + + var items = [MetricChartModule.Item]() + var totalInflow = [Decimal]() + var totalAssets = [Decimal]() + for point in points.sorted(by: { p1, p2 in p1.date < p2.date }) { + items.append(MetricChartModule.Item(value: point.dailyInflow, timestamp: point.date.timeIntervalSince1970)) + totalInflow.append(point.totalInflow) + totalAssets.append(point.totalAssets) + } + return MetricChartModule.ItemData( + items: items, + indicators: [ + MarketGlobalModule.totalInflow: totalInflow, + MarketGlobalModule.totalAssets: totalAssets, + ], + type: .etf + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift index e467a20489..bc15c90721 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartFactory.swift @@ -6,6 +6,7 @@ class MetricChartFactory { private static let noChangesLimitPercent: Decimal = 0.2 private let dateFormatter = DateFormatter() + private let currencyManager = App.shared.currencyManager init(currentLocale: Locale) { dateFormatter.locale = currentLocale @@ -37,7 +38,7 @@ class MetricChartFactory { return [valueString, coin.code].compactMap { $0 }.joined(separator: " ") case let .compactCurrencyValue(currency): if exactlyValue { - return ValueFormatter.instance.formatFull(currency: currency, value: value) + return ValueFormatter.instance.formatFull(currency: currency, value: value, showSign: true) } else { return ValueFormatter.instance.formatShort(currency: currency, value: value) } @@ -73,18 +74,36 @@ extension MetricChartFactory { let chartTrend: MovementTrend var value: String? - var valueDiff: Decimal? + var valueDiff: ValueDiff? var rightSideMode: ChartModule.RightSideMode = .none switch itemData.type { case .regular: value = Self.format(value: lastItem.value, valueType: valueType) - chartTrend = (lastItem.value - firstItem.value).isSignMinus ? .down : .up - valueDiff = (lastItem.value - firstItem.value) / firstItem.value * 100 + let diff = (lastItem.value - firstItem.value) / firstItem.value * 100 + chartTrend = diff.isSignMinus ? .down : .up + + let valueString = ValueFormatter.instance.format(percentValue: diff, showSign: true) + valueDiff = valueString.map { ValueDiff(value: $0, trend: chartTrend) } if let first = itemData.indicators[MarketGlobalModule.dominance]?.first, let last = itemData.indicators[MarketGlobalModule.dominance]?.last { rightSideMode = .dominance(value: last, diff: (last - first) / first * 100) } + case .etf: + if let last = itemData.indicators[MarketGlobalModule.totalInflow]?.last { // etf chart + value = Self.format(value: last, valueType: valueType) + } + + let valueString = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: lastItem.value) + valueDiff = valueString.map { ValueDiff(value: $0, trend: lastItem.value.isSignMinus ? .down : .up) } + chartTrend = .neutral + + if let last = itemData.indicators[MarketGlobalModule.totalAssets]?.last { + rightSideMode = .custom( + title: "market.etf.total_net_assets".localized, + value: ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: last) + ) + } case let .aggregated(aggregatedValue): value = Self.format(value: aggregatedValue, valueType: valueType) chartTrend = .neutral @@ -111,6 +130,24 @@ extension MetricChartFactory { indicators.append(dominanceIndicator) } + if let totalAssets = itemData.indicators[MarketGlobalModule.totalAssets] { + let totalIndicator = PrecalculatedIndicator( + id: MarketGlobalModule.totalAssets, + enabled: true, + values: totalAssets, + configuration: ChartIndicator.LineConfiguration.totalAssets + ) + indicators.append(totalIndicator) + } + if let totalInflow = itemData.indicators[MarketGlobalModule.totalInflow] { + let totalIndicator = PrecalculatedIndicator( + id: MarketGlobalModule.totalInflow, + enabled: false, + values: totalInflow, + configuration: ChartIndicator.LineConfiguration.totalInflow + ) + indicators.append(totalIndicator) + } return ChartModule.ViewItem( value: value, @@ -131,16 +168,34 @@ extension MetricChartFactory { let date = Date(timeIntervalSince1970: chartItem.timestamp) let formattedDate = DateHelper.instance.formatFullTime(from: date) - let formattedValue = Self.format(value: value, valueType: valueType) var rightSideMode: ChartModule.RightSideMode = .none if let dominance = chartItem.indicators[ChartIndicator.LineConfiguration.dominanceId] { rightSideMode = .dominance(value: dominance, diff: nil) + } else if let totalAssets = chartItem.indicators[ChartIndicator.LineConfiguration.totalAssetId] { + let value = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: totalAssets) + rightSideMode = .custom(title: "market.etf.total_net_assets".localized, value: value) } else if let volume = chartItem.indicators[ChartData.volume] { rightSideMode = .volume(value: Self.format(value: volume, valueType: valueType)) } + // if etf chart + if let totalInflow = chartItem.indicators[ChartIndicator.LineConfiguration.totalInflowId] { + let formattedValue = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: totalInflow) + let diffString = ValueFormatter.instance.formatShort(currency: currencyManager.baseCurrency, value: value) + let diff = diffString.map { ValueDiff(value: $0, trend: value.isSignMinus ? .down : .up) } + + return ChartModule.SelectedPointViewItem( + value: formattedValue, + diff: diff, + date: formattedDate, + rightSideMode: rightSideMode + ) + } + + let formattedValue = Self.format(value: value, valueType: valueType) + return ChartModule.SelectedPointViewItem( value: formattedValue, date: formattedDate, diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift index 18ef7b0948..f07e4ce333 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartModule.swift @@ -48,6 +48,7 @@ enum MetricChartModule { enum ItemType { case regular + case etf case aggregated(value: Decimal?) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift index 7c600e4e5a..8b2eea1c71 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/MetricChart/MetricChartViewModel.swift @@ -152,4 +152,16 @@ extension MetricChartViewModel { let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) return MetricChartViewModel(service: service, factory: factory) } + + static var etfInstance: MetricChartViewModel { + let fetcher = MarketEtfFetcher(marketKit: App.shared.marketKit, currencyManager: App.shared.currencyManager) + let service = MetricChartService( + chartFetcher: fetcher, + interval: .byPeriod(.day1), + statPage: StatPage.globalMetricsEtf + ) + + let factory = MetricChartFactory(currentLocale: LanguageManager.shared.currentLocale) + return MetricChartViewModel(service: service, factory: factory) + } } diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift index 3829540436..b897d91351 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Components/DiffLabel.swift @@ -18,6 +18,11 @@ class DiffLabel: UILabel { textColor = Self.color(value: value, highlight: highlightText) } + func set(value: ValueDiff?) { + text = value?.value + textColor = value?.trend == .down ? .themeLucian : .themeRemus + } + func set(text: String?, color: UIColor) { self.text = text textColor = color diff --git a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift index e563972abc..60c2670fa1 100644 --- a/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift +++ b/UnstoppableWallet/UnstoppableWallet/UserInterface/Extensions/ChartConfiguration.swift @@ -19,6 +19,10 @@ extension ChartConfiguration { ChartConfiguration().applyColors().applyBase().applyBars() } + static var baseHistogramChart: ChartConfiguration { + ChartConfiguration().applyColors().applyBase().applyHistogram() + } + static var volumeBarChart: ChartConfiguration { baseBarChart.applyVolume() } @@ -50,6 +54,15 @@ extension ChartConfiguration { return self } + @discardableResult private func clearGradients() -> Self { + let clear = [UIColor.clear] + trendUpGradient = clear + trendDownGradient = clear + pressedGradient = clear + neutralGradient = clear + return self + } + @discardableResult private func applyPreview(height: CGFloat, curveWidth: CGFloat = 2) -> Self { mainHeight = height indicatorHeight = 0 @@ -63,11 +76,7 @@ extension ChartConfiguration { showVerticalLines = false isInteractive = false - let clear = [UIColor.clear] - trendUpGradient = clear - trendDownGradient = clear - pressedGradient = clear - neutralGradient = clear + clearGradients() return self } @@ -76,10 +85,19 @@ extension ChartConfiguration { curveType = .bars curveBottomInset = 18 - trendUpGradient = [.clear] - trendDownGradient = [.clear] - pressedGradient = [.clear] - neutralGradient = [.clear] + clearGradients() + + return self + } + + @discardableResult private func applyHistogram() -> Self { + curveType = .histogram + curveBottomInset = 18 + + indicatorHeight = 0 + timelineHeight = 0 + + clearGradients() return self } @@ -137,4 +155,22 @@ public extension ChartIndicator.LineConfiguration { let indicator = PrecalculatedIndicator(id: MarketGlobalModule.dominance, enabled: true, values: [], configuration: dominance) return indicator.json } + + static var totalAssets: Self { + Self(color: ChartColor(.themeNina.withAlphaComponent(0.5)), width: 1) + } + + static var totalAssetId: String { + let indicator = PrecalculatedIndicator(id: MarketGlobalModule.totalAssets, enabled: true, values: [], configuration: totalAssets) + return indicator.json + } + + static var totalInflow: Self { + Self(color: ChartColor(.themeNina.withAlphaComponent(0.5)), width: 1) + } + + static var totalInflowId: String { + let indicator = PrecalculatedIndicator(id: MarketGlobalModule.totalInflow, enabled: false, values: [], configuration: totalAssets) + return indicator.json + } } diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 5db64ca3b2..6319079e6c 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -904,6 +904,7 @@ "market.etf.title" = "Total Net Inflow"; "market.etf.description" = "The net inflow of an ETF equals its cash inflows minus outflows."; +"market.etf.total_net_assets" = "Total Net Assets"; "market.etf.sort_by.highest_assets" = "Highest Assets"; "market.etf.sort_by.lowest_assets" = "Lowest Assets"; "market.etf.sort_by.inflow" = "Inflow";