diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index 76fb67581a..446fcaf932 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -1882,6 +1882,7 @@ ABC9A0779C1107119CD3AF19 /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8CE84FA36438BE4D6B5 /* FileManager.swift */; }; ABC9A07A1D93791D09BEA9AF /* PredefinedBlockchainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1057AD189DA1CE31BF5 /* PredefinedBlockchainService.swift */; }; ABC9A08340695A0AFCE9C2F2 /* SendBitcoinViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D6C9D12C1F1F3A1218 /* SendBitcoinViewController.swift */; }; + ABC9A0866C672D2D560DA23C /* CoinDetailAdviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */; }; ABC9A08E14B61C478C342548 /* WalletConnectEvmChainParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAF99E1669B374FF256E /* WalletConnectEvmChainParser.swift */; }; ABC9A092DC0DEEF9838DB47A /* CellElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A776346AF62265896CA1 /* CellElement.swift */; }; ABC9A0A3A52AD41643D67D3D /* SingleLineFormTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */; }; @@ -1894,6 +1895,7 @@ ABC9A0D8D596A7122B2DDD97 /* WalletConnectV2ListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8DC6C2EEBD4FD453342 /* WalletConnectV2ListViewModel.swift */; }; ABC9A0E6EE31D5675542EE0B /* SessionRequestFilterManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA77C414AC06C41F9319 /* SessionRequestFilterManager.swift */; }; ABC9A0E743DDE8F4ADA483EB /* SwapPriceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA459E123B7053EC73F0 /* SwapPriceCell.swift */; }; + ABC9A0EE5E5B31405569BF3F /* IndicatorAdviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */; }; ABC9A0F42A6687705CAD1340 /* NftAssetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9C0D0174A5B6A91F13 /* NftAssetViewController.swift */; }; ABC9A0FE58CC65114E2B296F /* SimpleActivateService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF3E15CB67B44E7069E /* SimpleActivateService.swift */; }; ABC9A1117A41AB8CE00FDEDB /* WalletConnectV2AppShowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A845B2969166028BA5F0 /* WalletConnectV2AppShowView.swift */; }; @@ -1914,6 +1916,7 @@ ABC9A1EC656488FF79F458EC /* RestoreCloudViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5FE0EDA53E4D9B85DE1 /* RestoreCloudViewModel.swift */; }; ABC9A1FFFB4F9EC58BF78661 /* AccountRestoreWarningManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A7D665A025E95697C757 /* AccountRestoreWarningManager.swift */; }; ABC9A20A25C4C683A73CB994 /* ContactBookContactViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8080797194017F736AB /* ContactBookContactViewModel.swift */; }; + ABC9A20D2DDF8736293DE5C5 /* CoinIndicatorViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */; }; ABC9A20F6F7D5EA2A1A55A9E /* ContactLabelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB89F64056FFB98928E7 /* ContactLabelService.swift */; }; ABC9A227648FF076E9518703 /* ContactBookHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE12A5E8B9FB24FFE42F /* ContactBookHelper.swift */; }; ABC9A236A003CA276745D907 /* MetadataMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB8F4032F87355FD3693 /* MetadataMonitor.swift */; }; @@ -1940,6 +1943,7 @@ ABC9A2EEC77205793C21F9A1 /* WalletConnectV2MainPendingRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8D8072033A5AC7E4897 /* WalletConnectV2MainPendingRequestViewModel.swift */; }; ABC9A2FF431ACFA812F58AD1 /* WalletConnectV2RequestMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA31438063F7AB7BDDC8 /* WalletConnectV2RequestMapper.swift */; }; ABC9A30459A3D9922CCF2E26 /* WalletConnectV1ListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA2DC16F947607B1794E /* WalletConnectV1ListView.swift */; }; + ABC9A305CBB28F2B19EB00D2 /* CoinDetailAdviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */; }; ABC9A30629619D5BD6CEB952 /* ContactBookContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8A353E491AAD3EDA120 /* ContactBookContactViewController.swift */; }; ABC9A307AAD83C3ED0D591C7 /* ContactBookContactModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AB2ED4E48D4FCEDBE769 /* ContactBookContactModule.swift */; }; ABC9A3114AEA853305A33586 /* WalletConnectListModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC09A586D88BAB3B9C67 /* WalletConnectListModule.swift */; }; @@ -1996,6 +2000,7 @@ ABC9A57DE6436FB8795F50E4 /* ContactBookViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A2D87362E00FD9FB5688 /* ContactBookViewController.swift */; }; ABC9A57EB423CAD56190F36B /* ChartIndicatorSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACE2CCBDF21572F5600C /* ChartIndicatorSettingsViewModel.swift */; }; ABC9A59B465A9C59F93DFB96 /* ChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9F6635146BEBFB432D1 /* ChartCell.swift */; }; + ABC9A5A0C65184DF54C48C5A /* TechnicalIndicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */; }; ABC9A5A0E47C6A90F30382E2 /* WalletConnectV1MainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A857395F2521BA0A2602 /* WalletConnectV1MainService.swift */; }; ABC9A5BBFC1960B1DD8F62B7 /* SendBinanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3F41BDCD5F4146E6E06 /* SendBinanceService.swift */; }; ABC9A5C2E2976341520D2F6D /* WalletConnectListModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AC09A586D88BAB3B9C67 /* WalletConnectListModule.swift */; }; @@ -2039,6 +2044,7 @@ ABC9A733703BD9E28B90BECD /* MaIndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD2E1F25A5CED10DB81F /* MaIndicatorDataSource.swift */; }; ABC9A736DDFBF98473C065F7 /* WalletConnectV2RequestMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA31438063F7AB7BDDC8 /* WalletConnectV2RequestMapper.swift */; }; ABC9A739B2E6FC4DFBD3ABC9 /* UniswapV3Module.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6363DB5DAE5B58AFDC0 /* UniswapV3Module.swift */; }; + ABC9A74F192AB94CFD1D1649 /* IndicatorAdviceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */; }; ABC9A7584EFCD56C3AC586D2 /* CellElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A776346AF62265896CA1 /* CellElement.swift */; }; ABC9A75CF08214FBE3B327E5 /* SimpleActivateModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9AABCDDAE5E7D0244C /* SimpleActivateModule.swift */; }; ABC9A7655AE66379E42FE2A4 /* ContactBookSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A99184EE1D5D052C52E9 /* ContactBookSettingsViewController.swift */; }; @@ -2129,6 +2135,7 @@ ABC9AB86218564E4873F6428 /* WalletConnectUriHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1C31F5343EB2BEA4540 /* WalletConnectUriHandler.swift */; }; ABC9AB8A9028DC1488166ABC /* WalletConnectV2PendingRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AAD79FD756DA69A52578 /* WalletConnectV2PendingRequestsViewController.swift */; }; ABC9AB90B6DF671F213642B0 /* WalletConnectV2PairingService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AFB38C3D5494BBD2D56E /* WalletConnectV2PairingService.swift */; }; + ABC9AB9DCC782F2EC14A7031 /* TechnicalIndicatorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */; }; ABC9ABA70CEF664E8E01FA7A /* SendNftModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A82A1E9AE6CC0E24756B /* SendNftModule.swift */; }; ABC9ABAE470C553AE5E80A9F /* WalletConnectSessionKiller.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1667330B8DBC1A8FE52 /* WalletConnectSessionKiller.swift */; }; ABC9ABC085B0733DD4EF1FCD /* RestoreCloudPassphraseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */; }; @@ -2158,6 +2165,7 @@ ABC9AC87FA11BA3478A8E801 /* TransactionsContactLabelService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AE89A5925C2026AB6B69 /* TransactionsContactLabelService.swift */; }; ABC9AC900545DC0DD2201DEE /* UniswapV3TradeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ACF418357FF7AFC64B3F /* UniswapV3TradeService.swift */; }; ABC9ACA04EBC7AF903A01FE3 /* SendEip1155ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A9C09ECB9B0CCBAD8C21 /* SendEip1155ViewController.swift */; }; + ABC9ACADAE8CFCAC777D048B /* CoinIndicatorViewItemFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */; }; ABC9ACADCCE9CEB2588D91D5 /* SendMemoInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A099941AA53B68270D55 /* SendMemoInputViewModel.swift */; }; ABC9ACBBC8958230424CFFC4 /* IndicatorDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A12E4155640075755699 /* IndicatorDataSource.swift */; }; ABC9ACC4CA8C9CBCA7C1A182 /* ContactBookContactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A5E6F7C6887DD5DFF6E4 /* ContactBookContactService.swift */; }; @@ -2174,6 +2182,7 @@ ABC9AD05E7B986179310D6D7 /* SwapInputAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A56611CF5E7B3F25CD5C /* SwapInputAccessoryView.swift */; }; ABC9AD12C685D7260D65F914 /* SimpleActivateModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AF9AABCDDAE5E7D0244C /* SimpleActivateModule.swift */; }; ABC9AD1C8D0CE88A604D5250 /* SendBinanceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AD0DD32AB4B9BAB79F11 /* SendBinanceFactory.swift */; }; + ABC9AD27E074CF3FA292C647 /* IndicatorAdviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */; }; ABC9AD3276132B33F6045AFF /* MarketCategoryMarketCapFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9ADC1A3B17225B6CC0869 /* MarketCategoryMarketCapFetcher.swift */; }; ABC9AD41E7C88963F6512905 /* ChartIndicatorsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A3758FE2D56036DF27FF /* ChartIndicatorsRepository.swift */; }; ABC9AD49CCD14F97CD912454 /* SendBitcoinAdapterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A1BD3B1B53C72DDF923A /* SendBitcoinAdapterService.swift */; }; @@ -2241,6 +2250,7 @@ ABC9AFA89983A9BCB78E4575 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AEAD18F73D4FBE05783D /* Contact.swift */; }; ABC9AFAB4E58875090F60014 /* AmountData.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9AA52B8C4E6834843D552 /* AmountData.swift */; }; ABC9AFC2E1155B5C87DC9F0E /* WalletConnectV1MainRequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A6CFF0208D48FE778028 /* WalletConnectV1MainRequestViewModel.swift */; }; + ABC9AFE47A405844612EB01A /* IndicatorAdviceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */; }; ABC9AFF2B9B16116FE1EB549 /* InputIntegerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A21A8154277AF08399A8 /* InputIntegerSection.swift */; }; ABC9AFFD730AC3274811DA61 /* ContactBookContactViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC9A8A353E491AAD3EDA120 /* ContactBookContactViewController.swift */; }; D00267B92A57E6CE00D6B2D5 /* ResendPastInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00267B82A57E6CE00D6B2D5 /* ResendPastInputCell.swift */; }; @@ -3573,6 +3583,7 @@ 6BCD531B2A16203F00993F20 /* CloudAccountBackupManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudAccountBackupManager.swift; sourceTree = ""; }; ABC9A021D71EDD24DFB6BA62 /* CoinProChartModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinProChartModule.swift; sourceTree = ""; }; ABC9A03401172C4C65D66764 /* SingleLineFormTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineFormTextView.swift; sourceTree = ""; }; + ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceView.swift; sourceTree = ""; }; ABC9A0483AEAEB88DFBDD873 /* WalletConnectV2SocketConnectionService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2SocketConnectionService.swift; sourceTree = ""; }; ABC9A06866150862CEDEB5DE /* RestoreCloudService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudService.swift; sourceTree = ""; }; ABC9A06A4A02C5E889265463 /* ContactBookSettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookSettingsViewModel.swift; sourceTree = ""; }; @@ -3582,6 +3593,7 @@ ABC9A0C131342CC764890C2B /* ChartIndicatorsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChartIndicatorsViewController.swift; sourceTree = ""; }; ABC9A0F966294A4E629CCB65 /* WalletConnectModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectModule.swift; sourceTree = ""; }; ABC9A1057AD189DA1CE31BF5 /* PredefinedBlockchainService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PredefinedBlockchainService.swift; sourceTree = ""; }; + ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinDetailAdviceViewController.swift; sourceTree = ""; }; ABC9A1136889E6976E17B347 /* WalletConnectV2Service.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2Service.swift; sourceTree = ""; }; ABC9A11FA28631EE6EB4CA06 /* WalletConnectV2PairingViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2PairingViewController.swift; sourceTree = ""; }; ABC9A12529DC8DE5D46D9776 /* ContactBookAddressViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookAddressViewModel.swift; sourceTree = ""; }; @@ -3615,6 +3627,7 @@ ABC9A3C708CD81CFE4C5BC5C /* ContactBookViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactBookViewModel.swift; sourceTree = ""; }; ABC9A3DBB89D0AE0C127742B /* WalletConnectScanQrViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectScanQrViewModel.swift; sourceTree = ""; }; ABC9A3DC5DA5B7BFDBF72B5D /* SendBitcoinFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBitcoinFactory.swift; sourceTree = ""; }; + ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TechnicalIndicatorService.swift; sourceTree = ""; }; ABC9A3F41BDCD5F4146E6E06 /* SendBinanceService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendBinanceService.swift; sourceTree = ""; }; ABC9A3FB680357E569B6DB5F /* WalletConnectV2AppShowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2AppShowViewModel.swift; sourceTree = ""; }; ABC9A413A1015882E90F7675 /* WalletConnectV1MainRequestView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV1MainRequestView.swift; sourceTree = ""; }; @@ -3642,6 +3655,7 @@ ABC9A6F1FB00B33D1896FC6B /* RestoreCloudPassphraseService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseService.swift; sourceTree = ""; }; ABC9A6F55A2C6777D25F57D5 /* SendZcashFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendZcashFactory.swift; sourceTree = ""; }; ABC9A7315E119F0B1581B70C /* SendEip721ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendEip721ViewController.swift; sourceTree = ""; }; + ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoinIndicatorViewItemFactory.swift; sourceTree = ""; }; ABC9A776346AF62265896CA1 /* CellElement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellElement.swift; sourceTree = ""; }; ABC9A7809A4A33BEBCFA3194 /* WalletConnectV1ListViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV1ListViewModel.swift; sourceTree = ""; }; ABC9A785F86A11CA6BCE2190 /* SimpleActivateViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleActivateViewModel.swift; sourceTree = ""; }; @@ -3693,6 +3707,7 @@ ABC9AA99463E646706E8E36D /* RestoreCloudPassphraseViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreCloudPassphraseViewModel.swift; sourceTree = ""; }; ABC9AAB6BA03FFE92F247FF6 /* ProChartFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProChartFetcher.swift; sourceTree = ""; }; ABC9AAC741F9A54293CD21B1 /* RestoreTypeModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RestoreTypeModule.swift; sourceTree = ""; }; + ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IndicatorAdviceCell.swift; sourceTree = ""; }; ABC9AAD55B8932EE75E3C037 /* SwapInputModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapInputModule.swift; sourceTree = ""; }; ABC9AAD79FD756DA69A52578 /* WalletConnectV2PendingRequestsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectV2PendingRequestsViewController.swift; sourceTree = ""; }; ABC9AAEA86EF9D14503A4791 /* WalletBackupCrypto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletBackupCrypto.swift; sourceTree = ""; }; @@ -5310,6 +5325,7 @@ 11B3540B41309A446C1DDB83 /* CoinAnalyticsViewController.swift */, 11B359CE35C7483CCB956D13 /* CoinAnalyticsHoldersCell.swift */, 11B35FDC67CE58FBE44A4107 /* CoinAnalyticsRatingScaleViewController.swift */, + ABC9A3513B75BB6271C87BFE /* TechnicalIndicators */, ); path = Analytics; sourceTree = ""; @@ -6653,6 +6669,18 @@ path = ContactBookAddress; sourceTree = ""; }; + ABC9A3513B75BB6271C87BFE /* TechnicalIndicators */ = { + isa = PBXGroup; + children = ( + ABC9A3EE670713BA4B6110F4 /* TechnicalIndicatorService.swift */, + ABC9A76776AD840DBFAA1804 /* CoinIndicatorViewItemFactory.swift */, + ABC9AACC40370E1E0CFC7639 /* IndicatorAdviceCell.swift */, + ABC9A044BFF4E76CD17835CA /* IndicatorAdviceView.swift */, + ABC9A10A83A43DCAFA709472 /* CoinDetailAdviceViewController.swift */, + ); + path = TechnicalIndicators; + sourceTree = ""; + }; ABC9A35EBE2AA1A07619F607 /* InputCard */ = { isa = PBXGroup; children = ( @@ -8896,6 +8924,11 @@ 11B35D2054F2B7C685074A0A /* LoginCoinzixVerifyService.swift in Sources */, 11B355DB12EDA3FAF2F082FF /* WithdrawCoinzixVerifyService.swift in Sources */, 11B35BB8792B6677AD26E254 /* CoinAnalyticsRatingScaleViewController.swift in Sources */, + ABC9AB9DCC782F2EC14A7031 /* TechnicalIndicatorService.swift in Sources */, + ABC9ACADAE8CFCAC777D048B /* CoinIndicatorViewItemFactory.swift in Sources */, + ABC9A0EE5E5B31405569BF3F /* IndicatorAdviceCell.swift in Sources */, + ABC9AD27E074CF3FA292C647 /* IndicatorAdviceView.swift in Sources */, + ABC9A305CBB28F2B19EB00D2 /* CoinDetailAdviceViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10145,6 +10178,11 @@ 11B3528E740BC2E36EE7D0F2 /* LoginCoinzixVerifyService.swift in Sources */, 11B351D78899E4A487891981 /* WithdrawCoinzixVerifyService.swift in Sources */, 11B353DE48A4B088210D927D /* CoinAnalyticsRatingScaleViewController.swift in Sources */, + ABC9A5A0C65184DF54C48C5A /* TechnicalIndicatorService.swift in Sources */, + ABC9A20D2DDF8736293DE5C5 /* CoinIndicatorViewItemFactory.swift in Sources */, + ABC9A74F192AB94CFD1D1649 /* IndicatorAdviceCell.swift in Sources */, + ABC9AFE47A405844612EB01A /* IndicatorAdviceView.swift in Sources */, + ABC9A0866C672D2D560DA23C /* CoinDetailAdviceViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10612,7 +10650,7 @@ repositoryURL = "https://github.com/horizontalsystems/Chart.Swift"; requirement = { kind = exactVersion; - version = 2.1.2; + version = 2.1.3; }; }; D3604E8028F03C6B0066C366 /* XCRemoteSwiftPackageReference "FeeRateKit.Swift" */ = { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift index bcf460f618..7f450328ef 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Chart/ChartCell.swift @@ -272,7 +272,9 @@ class ChartCell: UITableViewCell { } } - chartView.setCurve(colorType: viewItem.chartTrend.chartColorType) + if !chartView.isPressed { + chartView.setCurve(colorType: viewItem.chartTrend.chartColorType) + } chartView.set(chartData: viewItem.chartData, indicators: viewItem.indicators, showIndicators: showIndicators, animated: true) chartView.set(highLimitText: viewItem.maxValue, lowLimitText: viewItem.minValue) } else { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift index d652441d2d..c357361bbe 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsModule.swift @@ -13,7 +13,17 @@ struct CoinAnalyticsModule { accountManager: App.shared.accountManager, appConfigProvider: App.shared.appConfigProvider ) - let viewModel = CoinAnalyticsViewModel(service: service) + let technicalIndicatorService = TechnicalIndicatorService( + coinUid: fullCoin.coin.uid, + currencyKit: App.shared.currencyKit, + marketKit: App.shared.marketKit + ) + let coinIndicatorViewItemFactory = CoinIndicatorViewItemFactory() + let viewModel = CoinAnalyticsViewModel( + service: service, + technicalIndicatorService: technicalIndicatorService, + coinIndicatorViewItemFactory: coinIndicatorViewItemFactory + ) return CoinAnalyticsViewController(viewModel: viewModel) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift index abd1af29bf..c31bf14ef3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewController.swift @@ -1,3 +1,4 @@ +import Combine import UIKit import RxSwift import ThemeKit @@ -9,10 +10,11 @@ import MarketKit import Chart class CoinAnalyticsViewController: ThemeViewController { - private let placeholderText = "•••" + private static let placeholderText = "•••" private let viewModel: CoinAnalyticsViewModel private let disposeBag = DisposeBag() + private var cancellables = Set() private let tableView = SectionsTableView(style: .grouped) @@ -23,6 +25,7 @@ class CoinAnalyticsViewController: ThemeViewController { weak var parentNavigationController: UINavigationController? private var viewItem: CoinAnalyticsViewModel.ViewItem? + private var indicatorViewItem: CoinAnalyticsViewModel.IndicatorViewItem? init(viewModel: CoinAnalyticsViewModel) { self.viewModel = viewModel @@ -79,6 +82,7 @@ class CoinAnalyticsViewController: ThemeViewController { tableView.registerCell(forClass: PlaceholderCell.self) tableView.registerCell(forClass: MarketWideCardCell.self) tableView.registerCell(forClass: CoinAnalyticsHoldersCell.self) + tableView.registerCell(forClass: IndicatorAdviceCell.self) tableView.sectionDataSource = self subscribe(disposeBag, viewModel.viewItemDriver) { [weak self] in @@ -93,6 +97,13 @@ class CoinAnalyticsViewController: ThemeViewController { subscribe(disposeBag, viewModel.emptyViewDriver) { [weak self] visible in self?.emptyView.isHidden = !visible } + viewModel.indicatorViewItemsPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in + self?.indicatorViewItem = $0 + self?.tableView.reload() + } + .store(in: &cancellables) viewModel.onLoad() } @@ -124,6 +135,17 @@ class CoinAnalyticsViewController: ThemeViewController { parentNavigationController?.present(viewController, animated: true) } + private func openTechnicalIndicatorInfo() { + let viewController = InfoModule.viewController(viewItems: [ + .header1(text: "coin_analytics.technical_indicators".localized), + .listItem(text: "coin_analytics.technical_indicators.info1".localized), + .listItem(text: "coin_analytics.technical_indicators.info2".localized), + .listItem(text: "coin_analytics.technical_indicators.info3".localized), + ]) + + parentNavigationController?.present(viewController, animated: true) + } + private func openCexVolumeInfo() { let viewController = InfoModule.viewController(viewItems: [ .header1(text: "coin_analytics.cex_volume".localized), @@ -260,6 +282,27 @@ class CoinAnalyticsViewController: ThemeViewController { parentNavigationController?.present(viewController, animated: true) } + private func openPeriodSelect() { + let viewController = SelectorModule.bottomSingleSelectorViewController( + title: "coin_analytics.period.select_title".localized, + viewItems: viewModel.periodViewItems, + onSelect: { [weak self] index in + self?.viewModel.onSelectPeriod(index: index) + } + ) + + present(viewController, animated: true) + } + + private func openDetailAdvices() { + guard let viewItems = viewModel.detailAdviceSectionViewItems else { + return + } + + let viewController = CoinDetailAdviceViewController(viewItems: viewItems) + parentNavigationController?.pushViewController(viewController, animated: true) + } + private func placeholderChartData() -> ChartData { var chartItems = [ChartItem]() @@ -287,13 +330,13 @@ class CoinAnalyticsViewController: ThemeViewController { extension CoinAnalyticsViewController: SectionsDataSource { private func chartRow(id: String, title: String, valueInfo: String, chartCurveType: ChartConfiguration.CurveType, viewItem: Previewable, isLast: Bool, infoAction: @escaping () -> (), action: @escaping () -> ()) -> RowProtocol { - let value: String? + let value: String let chartData: ChartData let chartTrend: MovementTrend switch viewItem { case .preview: - value = placeholderText + value = Self.placeholderText chartData = placeholderChartData() chartTrend = .ignored case .regular(let viewItem): @@ -334,7 +377,7 @@ extension CoinAnalyticsViewController: SectionsDataSource { if let value { switch value { - case .preview: rowValue = placeholderText + case .preview: rowValue = Self.placeholderText case .regular(let value): rowValue = value } } @@ -359,6 +402,58 @@ extension CoinAnalyticsViewController: SectionsDataSource { ) } + private func indicatorSection(viewItem: CoinAnalyticsViewModel.IndicatorViewItem) -> SectionProtocol { + let disableInfo = viewItem.loading || viewItem.error || viewItem.viewItems.isEmpty + return Section( + id: "indicator-section", + headerState: .margin(height: 12), + rows: [ + Row(id: "indicator-advices-row", + height: IndicatorAdviceCell.height, + autoDeselect: false, + bind: { [weak self] cell, _ in + cell.set(backgroundStyle: .lawrence, isFirst: true) + cell.set(loading: viewItem.loading) + + if viewItem.error { + cell.setEmpty(value: "n/a".localized) + return + } + + if viewItem.viewItems.isEmpty { + cell.setEmpty(value: Self.placeholderText) + } else { + cell.set(viewItems: viewItem.viewItems) + } + + cell.onTapInfo = { [weak self] in + self?.openTechnicalIndicatorInfo() + } + } + ), + tableView.universalRow48( + id: "period", + title: .subhead2("coin_analytics.period".localized, color: .themeGray), + value: .subhead2(viewModel.period, color: viewItem.switchEnabled ? .themeLeah : .themeGray), + accessoryType: .dropdown, + autoDeselect: true, + action: viewItem.switchEnabled ? { [weak self] in + self?.openPeriodSelect() + } : nil + ), + tableView.universalRow48( + id: "details", + title: .subhead2("coin_analytics.details".localized, color: disableInfo ? .themeGray50 : .themeGray), + accessoryType: .disclosure, + autoDeselect: true, + isLast: true, + action: !disableInfo ? { [weak self] in + self?.openDetailAdvices() + } : nil + ), + ]) + } + private func ratingRow(id: String, rating: Previewable, isFirst: Bool = false, isLast: Bool = false) -> RowProtocol { let titleElements: [CellBuilderNew.CellElement] = [ .textElement(text: .subhead2("coin_analytics.rating_scale".localized), parameters: .highHugging), @@ -370,7 +465,7 @@ extension CoinAnalyticsViewController: SectionsDataSource { case .preview: return CellBuilderNew.row( rootElement: .hStack(titleElements + [ - .textElement(text: .subhead1(placeholderText), parameters: .rightAlignment) + .textElement(text: .subhead1(Self.placeholderText), parameters: .rightAlignment) ]), tableView: tableView, id: id, @@ -747,11 +842,11 @@ extension CoinAnalyticsViewController: SectionsDataSource { switch viewItem { case .preview: - value = placeholderText + value = Self.placeholderText blockchains = [ - Blockchain(imageUrl: nil, name: "Blockchain 1", value: placeholderText, action: nil), - Blockchain(imageUrl: nil, name: "Blockchain 2", value: placeholderText, action: nil), - Blockchain(imageUrl: nil, name: "Blockchain 3", value: placeholderText, action: nil), + Blockchain(imageUrl: nil, name: "Blockchain 1", value: Self.placeholderText, action: nil), + Blockchain(imageUrl: nil, name: "Blockchain 2", value: Self.placeholderText, action: nil), + Blockchain(imageUrl: nil, name: "Blockchain 3", value: Self.placeholderText, action: nil), ] chartItems = [ (0.5, UIColor.themeGray.withAlphaComponent(0.8)), @@ -917,7 +1012,7 @@ extension CoinAnalyticsViewController: SectionsDataSource { switch viewItem.value { case .preview: - value = placeholderText + value = Self.placeholderText valueInfo = nil case .regular(let _value): value = _value @@ -1074,6 +1169,10 @@ extension CoinAnalyticsViewController: SectionsDataSource { sections.append(lockInfoSection(lockInfo: lockInfo)) } + if let indicatorViewItem { + sections.append(indicatorSection(viewItem: indicatorViewItem)) + } + if let viewItem = viewItem.cexVolume { sections.append(cexVolumeSection(viewItem: viewItem)) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewModel.swift index 14c9280242..21218b3b94 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/CoinAnalyticsViewModel.swift @@ -7,7 +7,11 @@ import MarketKit import Chart class CoinAnalyticsViewModel { + private let queue = DispatchQueue(label: "io.horizontalsystems.unstoppable.coin_analytics_view_model", qos: .userInitiated) + private let service: CoinAnalyticsService + private let technicalIndicatorService: TechnicalIndicatorService + private let coinIndicatorViewItemFactory: CoinIndicatorViewItemFactory private var cancellables = Set() private let viewItemRelay = BehaviorRelay(value: nil) @@ -15,6 +19,8 @@ class CoinAnalyticsViewModel { private let syncErrorRelay = BehaviorRelay(value: false) private let emptyViewRelay = BehaviorRelay(value: false) + private let indicatorViewItemsSubject = CurrentValueSubject(.empty) + private let ratioFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal @@ -33,28 +39,63 @@ class CoinAnalyticsViewModel { return formatter }() - init(service: CoinAnalyticsService) { + init(service: CoinAnalyticsService, technicalIndicatorService: TechnicalIndicatorService, coinIndicatorViewItemFactory: CoinIndicatorViewItemFactory) { self.service = service + self.technicalIndicatorService = technicalIndicatorService + self.coinIndicatorViewItemFactory = coinIndicatorViewItemFactory service.$state - .sink { [weak self] in self?.sync(state: $0) } + .receive(on: queue) + .sink { [weak self] _ in self?.sync() } + .store(in: &cancellables) + + technicalIndicatorService.$state + .receive(on: queue) + .sink { [weak self] _ in self?.sync() } .store(in: &cancellables) - sync(state: service.state) + sync() } - private func sync(state: CoinAnalyticsService.State) { + private func syncIndicators(enabled: Bool) { + var loading = false + var error: Bool = false + var switchEnabled = false + var viewItems = [CoinIndicatorViewItemFactory.ViewItem]() + + if enabled { + switch technicalIndicatorService.state { + case .loading: loading = true + case .failed: + error = true + switchEnabled = true + case .completed(let items): + switchEnabled = true + viewItems = coinIndicatorViewItemFactory.viewItems(items: items) + } + } + + let viewItem = IndicatorViewItem(loading: loading, error: error, switchEnabled: switchEnabled, viewItems: viewItems) + indicatorViewItemsSubject.send(viewItem) + } + + private func sync() { + let state = service.state switch state { case .loading: viewItemRelay.accept(nil) loadingRelay.accept(true) syncErrorRelay.accept(false) emptyViewRelay.accept(false) + + syncIndicators(enabled: false) case .failed: viewItemRelay.accept(nil) loadingRelay.accept(false) syncErrorRelay.accept(true) emptyViewRelay.accept(false) + + syncIndicators(enabled: false) case .preview(let analyticsPreview, let subscriptionAddress): let viewItem = previewViewItem(analyticsPreview: analyticsPreview, subscriptionAddress: subscriptionAddress) @@ -68,6 +109,8 @@ class CoinAnalyticsViewModel { loadingRelay.accept(false) syncErrorRelay.accept(false) + + syncIndicators(enabled: false) case .success(let analytics): let viewItem = viewItem(analytics: analytics) @@ -81,6 +124,8 @@ class CoinAnalyticsViewModel { loadingRelay.accept(false) syncErrorRelay.accept(false) + + syncIndicators(enabled: true) } } @@ -316,6 +361,35 @@ class CoinAnalyticsViewModel { } +extension CoinAnalyticsViewModel { + + var period: String { + technicalIndicatorService.period.title + } + + var periodViewItems: [SelectorModule.ViewItem] { + technicalIndicatorService.allPeriods.map { + .init(title: $0.title, selected: $0 == technicalIndicatorService.period) + } + } + + var detailAdviceSectionViewItems: [CoinIndicatorViewItemFactory.SectionDetailViewItem]? { + guard let items = technicalIndicatorService.state.data else { + return nil + } + return coinIndicatorViewItemFactory.detailViewItems(items: items) + } + + func onSelectPeriod(index: Int) { + guard let period = technicalIndicatorService.allPeriods.at(index: index) else { + return + } + + technicalIndicatorService.period = period + } + +} + extension CoinAnalyticsViewModel { var viewItemDriver: Driver { @@ -334,6 +408,10 @@ extension CoinAnalyticsViewModel { emptyViewRelay.asDriver() } + var indicatorViewItemsPublisher: AnyPublisher { + indicatorViewItemsSubject.eraseToAnyPublisher() + } + var coin: Coin { service.coin } @@ -378,6 +456,15 @@ extension CoinAnalyticsViewModel { } } + struct IndicatorViewItem { + static let empty = IndicatorViewItem(loading: false, error: false, switchEnabled: false, viewItems: []) + + let loading: Bool + let error: Bool + let switchEnabled: Bool + let viewItems: [CoinIndicatorViewItemFactory.ViewItem] + } + enum LockInfo { case notSubscribed case notActivated(address: String) @@ -469,3 +556,17 @@ enum Previewable { } } + +extension HsPointTimePeriod { + + var title: String { + switch self { + case .minute30, .hour8: return "" // not used + case .hour1: return "coin_analytics.period.1h".localized + case .hour4: return "coin_analytics.period.4h".localized + case .day1: return "coin_analytics.period.1d".localized + case .week1: return "coin_analytics.period.1w".localized + } + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinDetailAdviceViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinDetailAdviceViewController.swift new file mode 100644 index 0000000000..7eef9064df --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinDetailAdviceViewController.swift @@ -0,0 +1,69 @@ +import UIKit +import ComponentKit +import SectionsTableView +import ThemeKit + +class CoinDetailAdviceViewController: ThemeViewController { + + private let tableView = SectionsTableView(style: .grouped) + + private var viewItems: [CoinIndicatorViewItemFactory.SectionDetailViewItem] + + init(viewItems: [CoinIndicatorViewItemFactory.SectionDetailViewItem]) { + self.viewItems = viewItems + super.init() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + title = "coin_analytics.details".localized + navigationItem.largeTitleDisplayMode = .never + + view.addSubview(tableView) + tableView.snp.makeConstraints { maker in + maker.edges.equalToSuperview() + } + + tableView.sectionDataSource = self + + tableView.separatorStyle = .none + tableView.backgroundColor = .clear + tableView.showsVerticalScrollIndicator = false + + tableView.reload() + } + +} + +extension CoinDetailAdviceViewController: SectionsDataSource { + + private func row(viewItem: CoinIndicatorViewItemFactory.DetailViewItem, isFirst: Bool, isLast: Bool) -> RowProtocol { + tableView.universalRow48( + id: viewItem.name, + title: .subhead2(viewItem.name), + value: .subhead1(viewItem.advice, color: viewItem.color), + backgroundStyle: .lawrence, + isFirst: isFirst, + isLast: isLast + ) + } + + func buildSections() -> [SectionProtocol] { + viewItems.map { section in + Section( + id: "header-\(section.name)", + headerState: tableView.sectionHeader(text: section.name), + footerState: .margin(height: .margin12), + rows: section.viewItems.enumerated().map { index, item in + row(viewItem: item, isFirst: index == 0, isLast: index == section.viewItems.count - 1) + } + ) + } + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift new file mode 100644 index 0000000000..8f62fbfedd --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/CoinIndicatorViewItemFactory.swift @@ -0,0 +1,132 @@ +import Combine +import Foundation +import UIKit +import Chart +import ThemeKit + +class CoinIndicatorViewItemFactory { + static let sectionNames = ["coin_analytics.indicators.summary".localized] + ChartIndicator.Category.allCases.map { $0.title } + + private func advice(items: [TechnicalIndicatorService.Item]) -> Advice { + let rating = items.map { $0.advice.rating }.reduce(0, +) + let adviceCount = items.filter { $0.advice != .noData }.count + let variations = 2 * adviceCount + 1 + + let baseDelta = variations / 5 + let remainder = variations % 5 + let neutralAddict = remainder % 3 // how much variations will be added to neutral zone + let sideAddict = remainder / 3 // how much will be added to sell/buy zone + + let deltas = [baseDelta, baseDelta + sideAddict, baseDelta + sideAddict + neutralAddict, baseDelta + sideAddict, baseDelta] + + var current = -adviceCount + var ranges = [Range]() + for delta in deltas { + ranges.append(current..<(current + delta)) + current += delta + } + let index = ranges.firstIndex { $0.contains(rating) } ?? 0 + + return Advice(rawValue: index) ?? .neutral + } + +} + +extension CoinIndicatorViewItemFactory { + + func viewItems(items: [TechnicalIndicatorService.SectionItem]) -> [ViewItem] { + var viewItems = [ViewItem]() + + var allAdviceItems = [TechnicalIndicatorService.Item]() + for item in items { + allAdviceItems.append(contentsOf: item.items) + viewItems.append(ViewItem(name: item.name, advice: advice(items: item.items))) + } + + let viewItem = ViewItem(name: Self.sectionNames.first ?? "", advice: advice(items: allAdviceItems)) + if viewItems.count > 0 { + viewItems.insert(viewItem, at: 0) + } + return viewItems + } + + func detailViewItems(items: [TechnicalIndicatorService.SectionItem]) -> [SectionDetailViewItem] { + items.map { + SectionDetailViewItem( + name: $0.name, + viewItems: $0.items.map { DetailViewItem(name: $0.name, advice: $0.advice.title, color: $0.advice.color) } + ) + } + } + +} + +extension CoinIndicatorViewItemFactory { + + enum Advice: Int, CaseIterable { + case strongSell + case sell + case neutral + case buy + case strongBuy + + var color: UIColor { + switch self { + case .strongSell: return UIColor(hex: 0xF43A4F) + case .sell: return UIColor(hex: 0xF4503A) + case .neutral: return .themeJacob + case .buy: return UIColor(hex: 0xB5C405) + case .strongBuy: return .themeRemus + } + } + + var title: String { + switch self { + case .strongBuy: return "coin_analytics.indicators.strong_buy".localized + case .buy: return "coin_analytics.indicators.buy".localized + case .neutral: return "coin_analytics.indicators.neutral".localized + case .sell: return "coin_analytics.indicators.sell".localized + case .strongSell: return "coin_analytics.indicators.strong_sell".localized + } + } + } + + struct ViewItem { + let name: String + let advice: Advice + } + + struct DetailViewItem { + let name: String + let advice: String + let color: UIColor + } + + struct SectionDetailViewItem { + let name: String + let viewItems: [DetailViewItem] + } + +} + +extension TechnicalIndicatorService.Advice { + + var title: String { + switch self { + case .noData: return "coin_analytics.indicators.no_data".localized + case .buy: return "coin_analytics.indicators.buy".localized + case .neutral: return "coin_analytics.indicators.neutral".localized + case .sell: return "coin_analytics.indicators.sell".localized + } + } + + var color: UIColor { + switch self { + case .noData: return .themeGray + case .buy: return CoinIndicatorViewItemFactory.Advice.buy.color + case .neutral: return CoinIndicatorViewItemFactory.Advice.neutral.color + case .sell: return CoinIndicatorViewItemFactory.Advice.sell.color + } + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceCell.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceCell.swift new file mode 100644 index 0000000000..9b5341e479 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceCell.swift @@ -0,0 +1,118 @@ +import UIKit +import SnapKit +import ComponentKit +import ThemeKit +import HUD + +class IndicatorAdviceCell: BaseThemeCell { + static let height: CGFloat = 229 + + private let headerWrapperView = UIView() + private let nameLabel = UILabel() + private let infoButton = SecondaryCircleButton() + + private var adviceViews = [IndicatorAdviceView]() + private let spinner = HUDActivityView.create(with: .medium24) + + var onTapInfo: (() -> ())? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + wrapperView.backgroundColor = .clear + backgroundColor = .clear + + wrapperView.addSubview(headerWrapperView) + headerWrapperView.snp.makeConstraints { maker in + maker.top.equalToSuperview() + maker.leading.trailing.equalToSuperview() + } + + headerWrapperView.addSubview(nameLabel) + nameLabel.snp.makeConstraints { maker in + maker.top.equalToSuperview().offset(CGFloat.margin12) + maker.leading.equalToSuperview().inset(CGFloat.margin16) + maker.bottom.equalToSuperview().inset(9) + } + + nameLabel.font = .subhead1 + nameLabel.textColor = .gray + nameLabel.text = "coin_analytics.indicators.title".localized + + headerWrapperView.addSubview(infoButton) + infoButton.snp.makeConstraints { make in + make.leading.equalTo(nameLabel.snp.trailing).offset(CGFloat.margin12) + make.top.equalToSuperview().inset(CGFloat.margin12) + make.trailing.equalToSuperview().inset(CGFloat.margin16) + } + + infoButton.set(image: UIImage(named: "circle_information_20"), style: .transparent) + infoButton.addTarget(self, action: #selector(onTapInfoButton), for: .touchUpInside) + + var lastView: UIView = headerWrapperView + for _ in 0..<3 { + let view = IndicatorAdviceView() + adviceViews.append(view) + + wrapperView.addSubview(view) + view.snp.makeConstraints { maker in + maker.top.equalTo(lastView.snp.bottom).offset(CGFloat.margin24) + maker.leading.trailing.equalToSuperview().inset(CGFloat.margin16) + } + + lastView = view + } + + wrapperView.addSubview(spinner) + spinner.snp.makeConstraints { maker in + maker.center.equalToSuperview() + } + + spinner.set(hidden: true) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc private func onTapInfoButton() { + onTapInfo?() + } + +} + +extension IndicatorAdviceCell { + + func set(loading: Bool) { + adviceViews.forEach { $0.isHidden = loading } + spinner.isHidden = !loading + if loading { + spinner.startAnimating() + } else { + spinner.stopAnimating() + } + } + + func setEmpty(value: String) { + CoinIndicatorViewItemFactory.sectionNames.enumerated().forEach { index, element in + guard let view = adviceViews.at(index: index) else { + return + } + + view.title = element + view.setEmpty(title: element, value: value) + } + } + + func set(viewItems: [CoinIndicatorViewItemFactory.ViewItem]) { + viewItems.enumerated().forEach { index, element in + guard let view = adviceViews.at(index: index) else { + return + } + + view.title = element.name + view.set(advice: element.advice) + } + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceView.swift new file mode 100644 index 0000000000..be904f0147 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/IndicatorAdviceView.swift @@ -0,0 +1,99 @@ +import UIKit +import SnapKit +import ThemeKit +import ComponentKit + +class IndicatorAdviceView: UIView { + static private let blockHeight: CGFloat = 6 + + private let titleLabel = UILabel() + private let valueLabel = UILabel() + private let stackView = UIStackView() + + private var blocks = [UIView]() + + var title: String? { + didSet { + titleLabel.text = title + } + } + + init() { + super.init(frame: .zero) + + addSubview(titleLabel) + titleLabel.snp.makeConstraints { make in + make.top.leading.equalToSuperview() + } + + titleLabel.font = .subhead1 + titleLabel.textColor = .gray + + addSubview(valueLabel) + valueLabel.snp.makeConstraints { make in + make.top.trailing.equalToSuperview() + make.leading.equalTo(titleLabel.snp.trailing) + } + + valueLabel.textAlignment = .right + valueLabel.font = .subhead1 + valueLabel.textColor = .gray + valueLabel.setContentHuggingPriority(.required, for: .horizontal) + valueLabel.setContentCompressionResistancePriority(.required, for: .horizontal) + + addSubview(stackView) + stackView.snp.makeConstraints { make in + make.top.equalTo(titleLabel.snp.bottom).offset(CGFloat.margin8) + make.leading.trailing.equalToSuperview() + make.height.equalTo(Self.blockHeight) + make.bottom.equalToSuperview() + } + + stackView.spacing = 1 + stackView.distribution = .fillEqually + + for _ in 0..= i ? blockColor : blockColor.withAlphaComponent(0.2) + } + } + +} + +extension IndicatorAdviceView { + + func set(advice: CoinIndicatorViewItemFactory.Advice?) { + valueLabel.text = advice?.title + valueLabel.textColor = advice?.color ?? .themeGray + updateBlocks(advice: advice) + } + + func setEmpty(title: String?, value: String?) { + titleLabel.text = title + valueLabel.text = value + valueLabel.textColor = .themeGray + updateBlocks(advice: nil) + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/TechnicalIndicatorService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/TechnicalIndicatorService.swift new file mode 100644 index 0000000000..0e94208ce8 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Analytics/TechnicalIndicators/TechnicalIndicatorService.swift @@ -0,0 +1,215 @@ +import Combine +import Foundation +import Chart +import CurrencyKit +import HsExtensions +import MarketKit + +class TechnicalIndicatorService { + static let maPeriods: [Int] = [9, 25, 50, 100, 200] + + // sometimes backend can return points with gaps (5-15 point) but we need calculating all indicators + private static let additionalPoints = 20 + private static let pointCount = 200 + + private var tasks = Set() + + private let coinUid: String + private let currencyKit: CurrencyKit.Kit + private let marketKit: MarketKit.Kit + + let allPeriods: [HsPointTimePeriod] = [.hour1, .hour4, .day1, .week1] + var period: HsPointTimePeriod = .day1 { + didSet { + fetch() + } + } + + @PostPublished private(set) var state: DataStatus<[SectionItem]> = .loading + + init(coinUid: String, currencyKit: CurrencyKit.Kit, marketKit: MarketKit.Kit) { + self.coinUid = coinUid + self.currencyKit = currencyKit + self.marketKit = marketKit + + fetch() + } + + func fetch() { + let currency = currencyKit.baseCurrency + + tasks.forEach { task in task.cancel() } + tasks = Set() + + Task { [weak self, marketKit, coinUid, currency, period] in + do { + let points = try await marketKit.chartPoints( + coinUid: coinUid, + currencyCode: currency.code, + interval: period, + pointCount: Self.pointCount + Self.additionalPoints + ) + + self?.handle(chartPoints: points, period: period) + } catch { + self?.state = .failed(error) + } + }.store(in: &tasks) + } + + private func cross(_ value1: Decimal, _ value2: Decimal) -> Advice { + if value1 > value2 { + return .buy + } else if value1 < value2 { + return .sell + } else { + return .neutral + } + } + + private func handle(chartPoints: [ChartPoint], period: HsPointTimePeriod) { + do { + var sectionItems = [SectionItem]() + let chartData = try calculateIndicators(chartPoints: chartPoints) + + guard let lastRate = chartData.last(name: ChartData.rate) else { + throw IndicatorCalculator.IndicatorError.notEnoughData + } + + var maItems = [Item]() + + // Calculate ma advices + let types: [String] = ["ema", "sma"] + for type in types { + for period in Self.maPeriods { + let advice: Advice + if let maValue = chartData.last(name: "\(type)_\(period)") { + advice = cross(lastRate, maValue) + } else { + advice = .noData + } + maItems.append(Item(name: "\(type.uppercased()) \(period)", advice: advice)) + } + } + + // Calculate cross advices + let crossAdvice: Advice + if let ema25 = chartData.last(name: "ema_25"), + let ema50 = chartData.last(name: "ema_50") { + crossAdvice = cross(ema25, ema50) + } else { + crossAdvice = .noData + } + maItems.append(Item(name: "EMA Cross 25,50", advice: crossAdvice)) + sectionItems.append(SectionItem(name: ChartIndicator.Category.movingAverage.title, items: maItems)) + + var oscillatorItems = [Item]() + + // Calculate oscillators + let rsiAdvice: Advice + if let rsi = chartData.last(name: "rsi") { + if rsi > 70 { + rsiAdvice = .sell // overbought + } else if rsi < 30 { + rsiAdvice = .buy // oversold + } else { + rsiAdvice = .neutral + } + } else { + rsiAdvice = .noData + } + oscillatorItems.append(Item(name: "RSI", advice: rsiAdvice)) + + // Calculate MACD + let macdAdvice: Advice + if let macdMacd = chartData.last(name: "macd_macd"), + let macdSignal = chartData.last(name: "macd_signal") { + macdAdvice = cross(macdSignal, macdMacd) + } else { + macdAdvice = .noData + } + oscillatorItems.append(Item(name: "MACD", advice: macdAdvice)) + sectionItems.append(SectionItem(name: ChartIndicator.Category.oscillator.title, items: oscillatorItems)) + state = .completed(sectionItems) + } catch { + state = .failed(error) + } + } + + private func calculateIndicators(chartPoints: [ChartPoint]) throws -> ChartData { + guard let startTimestamp = chartPoints.first?.timestamp, let endTimestamp = chartPoints.last?.timestamp else { + throw IndicatorCalculator.IndicatorError.notEnoughData + } + + let values = chartPoints.map { $0.value } + + var items = [ChartItem]() + for point in chartPoints { + let chartItem = ChartItem(timestamp: point.timestamp) + chartItem.added(name: ChartData.rate, value: point.value) + items.append(chartItem) + } + + let chartData = ChartData(items: items, startWindow: startTimestamp, endWindow: endTimestamp) + + for period in Self.maPeriods { + if let emaValues = try? IndicatorCalculator.ma(period: period, values: values) { + chartData.add(name: "ema_\(period)", values: emaValues) + } + if let smaValues = try? IndicatorCalculator.ma(period: period, values: values) { + chartData.add(name: "sma_\(period)", values: smaValues) + } + } + + if let rsiValues = try? IndicatorCalculator.rsi(period: ChartIndicatorFactory.rsiPeriod, values: values) { + chartData.add(name: "rsi", values: rsiValues) + } + if let macdData = try? IndicatorCalculator.macd( + fast: ChartIndicatorFactory.macdPeriod[0], + long: ChartIndicatorFactory.macdPeriod[1], + signal: ChartIndicatorFactory.macdPeriod[2], + values: values) { + + chartData.add(name: "macd_macd", values: macdData.macd) + chartData.add(name: "macd_signal", values: macdData.signal) + chartData.add(name: "macd_histogram", values: macdData.histogram) + } + + return chartData + } + +} + +extension TechnicalIndicatorService { + + enum Advice { + case noData + case buy + case sell + case neutral + + var rating: Int { + switch self { + case .noData, .neutral: return 0 + case .buy: return 1 + case .sell: return -1 + } + } + } + + struct Item { + let name: String + let advice: Advice + } + + struct SectionItem { + let name: String + let items: [Item] + } + + struct Indicator { + let category: ChartIndicator.Category + let indicatorName: String + } + +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift index acedf1b728..7f4f4b129b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/CoinChartFactory.swift @@ -119,9 +119,12 @@ class CoinChartFactory { } // build top-line string let topLineString = NSMutableAttributedString() + let paragraphStyle: NSMutableParagraphStyle = NSMutableParagraphStyle() + paragraphStyle.alignment = NSTextAlignment.right + for (index, pair) in maPairs.enumerated() { let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, showSign: pair.0 < 0) - topLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1)])) + topLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) if index < maPairs.count - 1 { topLineString.append(NSAttributedString(string: " ")) } @@ -134,7 +137,7 @@ class CoinChartFactory { let formatted = value.flatMap { ValueFormatter.instance.formatFull(value: $0, decimalCount: 2, showSign: $0 < 0) } - bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: rsi.configuration.color.value.withAlphaComponent(1)])) + bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: rsi.configuration.color.value.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) case let macd as MacdIndicator: var pairs = [(Decimal, UIColor)]() // histogram pair @@ -145,24 +148,22 @@ class CoinChartFactory { } let signalName = MacdIndicator.MacdType.signal.name(id: macd.json) if let signalValue = chartItem.indicators[signalName] { - let color = macd.configuration.fastColor pairs.append((signalValue, macd.configuration.fastColor.value)) } let macdName = MacdIndicator.MacdType.macd.name(id: macd.json) if let macdValue = chartItem.indicators[macdName] { - let color = macd.configuration.fastColor pairs.append((macdValue, macd.configuration.longColor.value)) } for (index, pair) in pairs.enumerated() { let formatted = ValueFormatter.instance.formatFull(value: pair.0, decimalCount: 8, showSign: pair.0 < 0) - bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1)])) + bottomLineString.append(NSAttributedString(string: formatted ?? "", attributes: [.foregroundColor: pair.1.withAlphaComponent(1), .paragraphStyle: paragraphStyle])) if index < pairs.count - 1 { bottomLineString.append(NSAttributedString(string: " ")) } } default: if let volume = volumeString { - bottomLineString.append(NSAttributedString(string: volume, attributes: [.foregroundColor: UIColor.themeGray])) + bottomLineString.append(NSAttributedString(string: volume, attributes: [.foregroundColor: UIColor.themeGray, .paragraphStyle: paragraphStyle])) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorFactory.swift index 6636e03297..0ef10aed57 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorFactory.swift @@ -6,6 +6,8 @@ class ChartIndicatorFactory { static let precalculatedColor = [UIColor.themeYellowD] static let maColors = [UIColor(hex: 0xF54900), UIColor(hex: 0xBF5AF2), UIColor(hex: 0x09C1AB)] static let maPeriods = [9, 25, 50] + static let rsiPeriod = 12 + static let macdPeriod = [12, 26, 9] static func maConfiguration(_ index: Int) -> ChartIndicator.LineConfiguration { let index = index % maColors.count @@ -33,7 +35,7 @@ class ChartIndicatorFactory { var indicators = [ChartIndicator]() let maIndicators = maPeriods.enumerated().map { index, period in MaIndicator( - id: MaIndicator.MaType.ema.rawValue, + id: "MA", index: index, enabled: true, period: period, @@ -46,15 +48,15 @@ class ChartIndicatorFactory { id: ChartIndicator.AbstractType.rsi.rawValue, index: 0, enabled: true, - period: 12, + period: rsiPeriod, configuration: rsiConfiguration), MacdIndicator( id: ChartIndicator.AbstractType.macd.rawValue, index: 0, enabled: false, - fast: 12, - slow: 26, - signal: 9, + fast: macdPeriod[0], + slow: macdPeriod[1], + signal: macdPeriod[2], configuration: macdConfiguration ) ]) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsViewController.swift index f9ffdb9654..b70e414766 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Main/ChartIndicatorsViewController.swift @@ -29,7 +29,7 @@ class ChartIndicatorsViewController: ThemeViewController { title = "chart_indicators.title".localized navigationItem.largeTitleDisplayMode = .never - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.close".localized, style: .plain, target: self, action: #selector(onTapClose)) + navigationItem.rightBarButtonItem = UIBarButtonItem(title: "button.done".localized, style: .done, target: self, action: #selector(onTapClose)) navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil) view.addSubview(tableView) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Settings/ChartIndicatorSettingsViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Settings/ChartIndicatorSettingsViewModel.swift index fd739c7af2..1e8c44fb19 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Settings/ChartIndicatorSettingsViewModel.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Coin/Indicators/Settings/ChartIndicatorSettingsViewModel.swift @@ -52,7 +52,7 @@ extension ChartIndicatorSettingsViewModel { var title: String { let indicator = dataSource.chartIndicator switch indicator.abstractType { - case .ma: return "chart_indicators.settings.ma.title".localized + " \(indicator.index + 1)" + case .ma: return indicator.id + " \(indicator.index + 1)" case .rsi: return "chart_indicators.settings.rsi.title".localized case .macd: return "chart_indicators.settings.macd.title".localized default: return "Custom" diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index efee92beb3..6821dd9545 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -693,12 +693,34 @@ Go to Settings - > Unstoppable and allow access to the camera."; // Coin Page -> Analytics +"coin_analytics.indicators.summary" = "Summary"; +"coin_analytics.indicators.title" = "Technical Indicators"; +"coin_analytics.indicators.no_data" = "No Data"; +"coin_analytics.indicators.strong_buy" = "Strong Buy"; +"coin_analytics.indicators.buy" = "Buy"; +"coin_analytics.indicators.neutral" = "Neutral"; +"coin_analytics.indicators.sell" = "Sell"; +"coin_analytics.indicators.strong_sell" = "Strong Sell"; +"coin_analytics.period" = "Period"; +"coin_analytics.period.select_title" = "Select Period"; +"coin_analytics.period.1h" = "1 hour"; +"coin_analytics.period.4h" = "4 hours"; +"coin_analytics.period.1d" = "1 day"; +"coin_analytics.period.1w" = "1 week"; + +"coin_analytics.details" = "Details"; + "coin_analytics.not_available" = "This project has no analytics data"; "coin_analytics.locked.not_subscribed" = "This page available only for Unstoppable Wallet premium users"; "coin_analytics.locked.learn_more" = "Learn More"; "coin_analytics.locked.not_activated" = "Activate your subscription for getting access to analytics"; "coin_analytics.locked.activate" = "Activate"; +"coin_analytics.technical_indicators" = "Technical Indicators"; +"coin_analytics.technical_indicators.info1" = "Summary: This is a general overview of an asset's technicals, considering a variety of technical indicators and timeframes. It provides a consensus viewpoint (Buy, Sell, or Neutral) based on these indicators."; +"coin_analytics.technical_indicators.info2" = "Moving Averages (MA): These are commonly used technical indicators that smooth out price data to create a trend-following indicator. They show the average price over a specific time period. There are several types of MAs:\n\nSimple Moving Average (SMA): This calculates the average of a selected range of prices, usually closing prices, by the number of periods in that range.\n\nExponential Moving Average (EMA): This gives more weight to recent prices, thereby responding more quickly to recent price changes."; +"coin_analytics.technical_indicators.info3" = "Oscillators: These are technical indicators that fluctuate over time within a band (above and below a center line, or between set levels). They are designed to help identify overbought and oversold conditions in a market. Here are some common oscillators:\n\nRelative Strength Index (RSI): This measures the speed and change of price movements. It is usually used to identify overbought or oversold conditions.\n\nMoving Average Convergence Divergence (MACD): This is used to identify potential buy and sell signals. It triggers technical signals when it crosses above (to buy) or below (to sell) its signal line."; + "coin_analytics.cex_volume" = "CEX Volume"; "coin_analytics.cex_volume_rank" = "CEX Volume Rank"; "coin_analytics.cex_volume_rank.description" = "Tokens ranked by trading volume for the token on centralized exchanges."; @@ -826,7 +848,6 @@ Go to Settings - > Unstoppable and allow access to the camera."; "chart_indicators.settings.period.error" = "Period can’t be more than %d"; -"chart_indicators.settings.ma.title" = "Moving Average"; "chart_indicators.settings.ma.description" = "The EMA, SMA, and WMA are moving averages used in technical analysis:\n\nEMA emphasizes recent prices for quicker reactions.\nSMA averages price data for a general trend view.\nWMA balances sensitivity and noise reduction by linearly weighting recent data"; "chart_indicators.settings.ma.type_title" = "Type"; "chart_indicators.settings.ma.period_title" = "Period";