diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index 8404311df..b54cdb533 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -47,7 +47,6 @@ 640C3FD82A617D4900555161 /* CandlePriceLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640C3FD72A617D4900555161 /* CandlePriceLevel.swift */; }; 640C3FDC2A618B2000555161 /* MarketEventSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640C3FDB2A618B2000555161 /* MarketEventSymbols.swift */; }; 640F8A532BA9C8D600C7BE22 /* SymbolsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F8A522BA9C8D600C7BE22 /* SymbolsDataProvider.swift */; }; - 640F8A542BA9C8D600C7BE22 /* SymbolsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F8A522BA9C8D600C7BE22 /* SymbolsDataProvider.swift */; }; 64104FC32A210F2400D1FC41 /* EventCode+Native.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64104FC22A210F2400D1FC41 /* EventCode+Native.swift */; }; 64104FC52A26059B00D1FC41 /* ConcurrentSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64104FC42A26059B00D1FC41 /* ConcurrentSet.swift */; }; 64104FC72A2613BC00D1FC41 /* ConcurrentArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64104FC62A2613BC00D1FC41 /* ConcurrentArray.swift */; }; @@ -77,6 +76,17 @@ 641C64B22B346A2E0023CFAD /* DXObservableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641C64B12B346A2E0023CFAD /* DXObservableSubscription.swift */; }; 641C64B42B347C430023CFAD /* DXObservableSubscriptionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641C64B32B347C430023CFAD /* DXObservableSubscriptionTest.swift */; }; 641E45F92B1DE51700649363 /* EventsListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 641E45F82B1DE51700649363 /* EventsListener.swift */; }; + 6421FFD52BE8D3C100AC4657 /* MarketDepthModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFD42BE8D3C100AC4657 /* MarketDepthModel.swift */; }; + 6421FFD92BEA18EC00AC4657 /* OrderSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFD82BEA18EC00AC4657 /* OrderSet.swift */; }; + 6421FFDB2BEA1FBD00AC4657 /* MarketDepthModel+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFDA2BEA1FBD00AC4657 /* MarketDepthModel+Ext.swift */; }; + 6421FFDD2BEA377100AC4657 /* TxMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFDC2BEA377100AC4657 /* TxMode.swift */; }; + 6421FFDF2BEA389100AC4657 /* IndexedTxModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFDE2BEA389100AC4657 /* IndexedTxModel.swift */; }; + 6421FFE32BEA5E4500AC4657 /* TxModelListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFE22BEA5E4500AC4657 /* TxModelListener.swift */; }; + 6421FFE52BEA755700AC4657 /* Dictionary+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFE42BEA755700AC4657 /* Dictionary+Ext.swift */; }; + 6421FFE72BEA820900AC4657 /* MarketDepthListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFE62BEA820900AC4657 /* MarketDepthListener.swift */; }; + 6421FFEA2BEB718A00AC4657 /* SymbolsDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640F8A522BA9C8D600C7BE22 /* SymbolsDataProvider.swift */; }; + 6421FFEC2BEBB53400AC4657 /* MarketDepthHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6421FFEB2BEBB53400AC4657 /* MarketDepthHeaderView.xib */; }; + 6421FFEE2BEBB5AD00AC4657 /* MarketDepthHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFED2BEBB5AD00AC4657 /* MarketDepthHeaderView.swift */; }; 6423E4652B445B92006B208D /* DXFeedTimeSeriesSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6423E4642B445B92006B208D /* DXFeedTimeSeriesSubscription.swift */; }; 6423E4672B44613D006B208D /* NativeTimeSeriesSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6423E4662B44613D006B208D /* NativeTimeSeriesSubscription.swift */; }; 6423E4692B457000006B208D /* DXTimeSeriesSubscriptionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6423E4682B457000006B208D /* DXTimeSeriesSubscriptionTest.swift */; }; @@ -100,6 +110,16 @@ 642BE4D22A2F5D230052340A /* TimeAndSale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BE4D12A2F5D230052340A /* TimeAndSale.swift */; }; 642BE4D42A2F5D730052340A /* TimeAndSale+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BE4D32A2F5D730052340A /* TimeAndSale+Ext.swift */; }; 642BE4D62A2F5E7C0052340A /* TimeAndSaleMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BE4D52A2F5E7C0052340A /* TimeAndSaleMapper.swift */; }; + 642C9A212BFDE71C0074864A /* Array+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A202BFDE71C0074864A /* Array+Ext.swift */; }; + 642C9A222BFDE71C0074864A /* Array+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A202BFDE71C0074864A /* Array+Ext.swift */; }; + 642C9A232BFDE71C0074864A /* Array+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A202BFDE71C0074864A /* Array+Ext.swift */; }; + 642C9A252BFDE9F00074864A /* CandleChartModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A242BFDE9F00074864A /* CandleChartModel.swift */; }; + 642C9A262BFDE9F00074864A /* CandleChartModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A242BFDE9F00074864A /* CandleChartModel.swift */; }; + 642C9A272BFDE9F00074864A /* CandleChartModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A242BFDE9F00074864A /* CandleChartModel.swift */; }; + 642C9A292BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A282BFDEAF20074864A /* CandlePickerType+Ext.swift */; }; + 642C9A2A2BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A282BFDEAF20074864A /* CandlePickerType+Ext.swift */; }; + 642C9A2B2BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A282BFDEAF20074864A /* CandlePickerType+Ext.swift */; }; + 642C9A2D2BFE2FFE0074864A /* DXMarketDepthTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642C9A2C2BFE2FFE0074864A /* DXMarketDepthTest.swift */; }; 642DC9282AAA21C000974F5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642DC9272AAA21C000974F5C /* AppDelegate.swift */; }; 642DC92A2AAA21C000974F5C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642DC9292AAA21C000974F5C /* SceneDelegate.swift */; }; 642DC92C2AAA21C000974F5C /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642DC92B2AAA21C000974F5C /* ViewController.swift */; }; @@ -115,6 +135,10 @@ 643A329B2BD0137000F6F790 /* Optional+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643A329A2BD0137000F6F790 /* Optional+Ext.swift */; }; 643A329F2BD2A04300F6F790 /* OnDemandService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643A329E2BD2A04300F6F790 /* OnDemandService.swift */; }; 643A32A22BD2AEFB00F6F790 /* NativeOnDemandService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643A32A12BD2AEFB00F6F790 /* NativeOnDemandService.swift */; }; + 643F41F52BDFE1B000A2176D /* DXFeedCandleChartApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F41F42BDFE1B000A2176D /* DXFeedCandleChartApp.swift */; }; + 643F41F92BDFE1B200A2176D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 643F41F82BDFE1B200A2176D /* Assets.xcassets */; }; + 643F41FC2BDFE1B200A2176D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 643F41FB2BDFE1B200A2176D /* Preview Assets.xcassets */; }; + 643F42032BE3742D00A2176D /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 646979712A3B5AF60003A9BA /* Colors.xcassets */; }; 64437A8F2A9DEE6F005929B2 /* InstrumentProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64437A8E2A9DEE6F005929B2 /* InstrumentProfile.swift */; }; 64437A922A9DF1DE005929B2 /* NativeInstrumentProfileReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64437A912A9DF1DE005929B2 /* NativeInstrumentProfileReader.swift */; }; 6447A5DB2A8E559000739CCF /* ILastingEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6447A5DA2A8E559000739CCF /* ILastingEvent.swift */; }; @@ -199,18 +223,32 @@ 6469F8C82A3B25C900846831 /* MarketEvent+Access.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8C72A3B25C900846831 /* MarketEvent+Access.swift */; }; 6469F8CF2A3B300A00846831 /* MetricCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8CB2A3B2F1200846831 /* MetricCell.swift */; }; 6469F8D02A3B301C00846831 /* TimeInterval+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8CD2A3B2F9900846831 /* TimeInterval+Ext.swift */; }; - 6469F8D32A3B401700846831 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; 6469F8D42A3B401700846831 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; 6469F8D52A3B401700846831 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; 6469F8D62A3B408900846831 /* Endpoint+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B6277C2A3762D000196D07 /* Endpoint+Ext.swift */; }; 6469F8D72A3B408900846831 /* Endpoint+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B6277C2A3762D000196D07 /* Endpoint+Ext.swift */; }; 6469F8D82A3B4AA400846831 /* MetricCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8CB2A3B2F1200846831 /* MetricCell.swift */; }; 6469F8D92A3B4AA400846831 /* MetricCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8CB2A3B2F1200846831 /* MetricCell.swift */; }; + 646A86152C00C0E200566C0F /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; + 646A86172C00CEBF00566C0F /* MarketDepthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFD62BE8D78A00AC4657 /* MarketDepthViewController.swift */; }; + 646A86182C00CEC500566C0F /* OrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFE82BEB717F00AC4657 /* OrderCell.swift */; }; + 646A86192C00D32C00566C0F /* MainMarketDepth.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64A586FA2C00BC2700EE5306 /* MainMarketDepth.storyboard */; }; 646D19E12A3C970700C82315 /* DXFTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646D19E02A3C970700C82315 /* DXFTimer.swift */; }; 646D19E22A3C994000C82315 /* DXFTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646D19E02A3C970700C82315 /* DXFTimer.swift */; }; 646D19E32A3C994100C82315 /* DXFTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 646D19E02A3C970700C82315 /* DXFTimer.swift */; }; 647426AD2ABC85F20012F793 /* Arguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647426AC2ABC85F20012F793 /* Arguments.swift */; }; 647426AF2ABC93900012F793 /* EventCode+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647426AE2ABC93900012F793 /* EventCode+String.swift */; }; + 647A513D2BE4DDB600B8B8C9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 647A51372BE4DDB600B8B8C9 /* Assets.xcassets */; }; + 647A513E2BE4DDB600B8B8C9 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 647A51392BE4DDB600B8B8C9 /* Preview Assets.xcassets */; }; + 647A513F2BE4DDB600B8B8C9 /* DXFeedCandleChartMacApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647A513B2BE4DDB600B8B8C9 /* DXFeedCandleChartMacApp.swift */; }; + 647A51442BE4DFD600B8B8C9 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 646979712A3B5AF60003A9BA /* Colors.xcassets */; }; + 647A51452BE4E02700B8B8C9 /* CandleChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F42002BDFE25D00A2176D /* CandleChart.swift */; }; + 647A51482BE4EE0600B8B8C9 /* CandleChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F42002BDFE25D00A2176D /* CandleChart.swift */; }; + 647A51492BE4F71700B8B8C9 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6469F8D22A3B401700846831 /* Colors.swift */; }; + 647A514F2BE5113A00B8B8C9 /* Color+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647A514B2BE5102800B8B8C9 /* Color+Ext.swift */; }; + 647A51502BE5129200B8B8C9 /* Color+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647A514B2BE5102800B8B8C9 /* Color+Ext.swift */; }; + 647A51512BE512E100B8B8C9 /* CandleChart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F42002BDFE25D00A2176D /* CandleChart.swift */; }; + 647A51532BE5141600B8B8C9 /* Color+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647A514B2BE5102800B8B8C9 /* Color+Ext.swift */; }; 64820AAF2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64820AAE2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift */; }; 6482F3D12BA492A60079AC3D /* SymbolsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6482F3D02BA492A60079AC3D /* SymbolsViewController.swift */; }; 6482F3D22BA492A60079AC3D /* SymbolsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6482F3D02BA492A60079AC3D /* SymbolsViewController.swift */; }; @@ -236,6 +274,8 @@ 6486B9792AD04F4000D8D5FA /* OrderBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6486B9782AD04F4000D8D5FA /* OrderBase.swift */; }; 6486B97B2AD0517A00D8D5FA /* OrderAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6486B97A2AD0517A00D8D5FA /* OrderAction.swift */; }; 6486B97D2AD057F200D8D5FA /* OrderSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6486B97C2AD057F200D8D5FA /* OrderSource.swift */; }; + 648BC9502C06169A0065C2F7 /* DXFeedFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */; }; + 648BC9512C06169A0065C2F7 /* DXFeedFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 648BD5692AC450D6004A3A95 /* ConnectTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BD5682AC450D6004A3A95 /* ConnectTool.swift */; }; 648BD56B2AC4576F004A3A95 /* HelpTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BD56A2AC4576F004A3A95 /* HelpTool.swift */; }; 648BD56D2AC56A04004A3A95 /* SubscriptionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BD56C2AC56A04004A3A95 /* SubscriptionUtils.swift */; }; @@ -267,6 +307,14 @@ 64A42F4E2B0B9FA4001C3ACC /* DXTimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A42F4D2B0B9FA4001C3ACC /* DXTimeZone.swift */; }; 64A42F502B0BA668001C3ACC /* DXTimeFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A42F4F2B0BA668001C3ACC /* DXTimeFormat.swift */; }; 64A42F522B0BBB6D001C3ACC /* DXTimePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A42F512B0BBB6D001C3ACC /* DXTimePeriod.swift */; }; + 64A586F52C00BC2700EE5306 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A586F42C00BC2700EE5306 /* AppDelegate.swift */; }; + 64A586F72C00BC2700EE5306 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A586F62C00BC2700EE5306 /* SceneDelegate.swift */; }; + 64A586FC2C00BC2700EE5306 /* MainMarketDepth.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64A586FA2C00BC2700EE5306 /* MainMarketDepth.storyboard */; }; + 64A586FE2C00BC2800EE5306 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 64A586FD2C00BC2800EE5306 /* Assets.xcassets */; }; + 64A587012C00BC2800EE5306 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64A586FF2C00BC2800EE5306 /* LaunchScreen.storyboard */; }; + 64A587062C00BD6500EE5306 /* MarketDepthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFD62BE8D78A00AC4657 /* MarketDepthViewController.swift */; }; + 64A5870B2C00BDDC00EE5306 /* OrderCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6421FFE82BEB717F00AC4657 /* OrderCell.swift */; }; + 64A5870C2C00BE1C00EE5306 /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 646979712A3B5AF60003A9BA /* Colors.xcassets */; }; 64AAF0532A8113E800E8942B /* String+Range.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AAF0522A8113E800E8942B /* String+Range.swift */; }; 64AAF0552A82499A00E8942B /* ConcurrentDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AAF0542A82499A00E8942B /* ConcurrentDict.swift */; }; 64AAF0572A82A3FC00E8942B /* ICandleSymbolProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64AAF0562A82A3FC00E8942B /* ICandleSymbolProperty.swift */; }; @@ -307,8 +355,6 @@ 64B6273B2A375C0F00196D07 /* PaddingLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B6272F2A375C0F00196D07 /* PaddingLabel.swift */; }; 64B6273E2A375C0F00196D07 /* QuoteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B627322A375C0F00196D07 /* QuoteTableViewController.swift */; }; 64B627402A375C0F00196D07 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B627342A375C0F00196D07 /* SceneDelegate.swift */; }; - 64B627442A375CB700196D07 /* DXFeedFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */; }; - 64B627452A375CB700196D07 /* DXFeedFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 803BAC0D29BFA50700FFAB1C /* DXFeedFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 64B627742A3761EC00196D07 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64B627702A3761EC00196D07 /* LaunchScreen.storyboard */; }; 64B627752A3761EC00196D07 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 64B627722A3761EC00196D07 /* Main.storyboard */; }; 64B627772A37620D00196D07 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 64B627762A37620D00196D07 /* Assets.xcassets */; }; @@ -448,6 +494,27 @@ remoteGlobalIDString = 803BAC0C29BFA50700FFAB1C; remoteInfo = DXFeedFramework; }; + 647A51422BE4DFBF00B8B8C9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 803BAC0429BFA50700FFAB1C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 803BAC0C29BFA50700FFAB1C; + remoteInfo = DXFeedFramework; + }; + 647A51462BE4E03A00B8B8C9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 803BAC0429BFA50700FFAB1C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 803BAC0C29BFA50700FFAB1C; + remoteInfo = DXFeedFramework; + }; + 64A587092C00BDAF00EE5306 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 803BAC0429BFA50700FFAB1C /* Project object */; + proxyType = 1; + remoteGlobalIDString = 803BAC0C29BFA50700FFAB1C; + remoteInfo = DXFeedFramework; + }; 64B4365F2AB9DA470003919E /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 803BAC0429BFA50700FFAB1C /* Project object */; @@ -554,13 +621,13 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 64B627482A375CB700196D07 /* Embed Frameworks */ = { + 648BC9522C06169B0065C2F7 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( - 64B627452A375CB700196D07 /* DXFeedFramework.framework in Embed Frameworks */, + 648BC9512C06169A0065C2F7 /* DXFeedFramework.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -645,9 +712,22 @@ 641C64B12B346A2E0023CFAD /* DXObservableSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXObservableSubscription.swift; sourceTree = ""; }; 641C64B32B347C430023CFAD /* DXObservableSubscriptionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXObservableSubscriptionTest.swift; sourceTree = ""; }; 641E45F82B1DE51700649363 /* EventsListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsListener.swift; sourceTree = ""; }; + 6421FFD42BE8D3C100AC4657 /* MarketDepthModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketDepthModel.swift; sourceTree = ""; }; + 6421FFD62BE8D78A00AC4657 /* MarketDepthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketDepthViewController.swift; sourceTree = ""; }; + 6421FFD82BEA18EC00AC4657 /* OrderSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSet.swift; sourceTree = ""; }; + 6421FFDA2BEA1FBD00AC4657 /* MarketDepthModel+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MarketDepthModel+Ext.swift"; sourceTree = ""; }; + 6421FFDC2BEA377100AC4657 /* TxMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxMode.swift; sourceTree = ""; }; + 6421FFDE2BEA389100AC4657 /* IndexedTxModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndexedTxModel.swift; sourceTree = ""; }; + 6421FFE22BEA5E4500AC4657 /* TxModelListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxModelListener.swift; sourceTree = ""; }; + 6421FFE42BEA755700AC4657 /* Dictionary+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+Ext.swift"; sourceTree = ""; }; + 6421FFE62BEA820900AC4657 /* MarketDepthListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketDepthListener.swift; sourceTree = ""; }; + 6421FFE82BEB717F00AC4657 /* OrderCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderCell.swift; sourceTree = ""; }; + 6421FFEB2BEBB53400AC4657 /* MarketDepthHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MarketDepthHeaderView.xib; sourceTree = ""; }; + 6421FFED2BEBB5AD00AC4657 /* MarketDepthHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketDepthHeaderView.swift; sourceTree = ""; }; 6423E4642B445B92006B208D /* DXFeedTimeSeriesSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXFeedTimeSeriesSubscription.swift; sourceTree = ""; }; 6423E4662B44613D006B208D /* NativeTimeSeriesSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTimeSeriesSubscription.swift; sourceTree = ""; }; 6423E4682B457000006B208D /* DXTimeSeriesSubscriptionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXTimeSeriesSubscriptionTest.swift; sourceTree = ""; }; + 64245BBD2BE4DD7B00901522 /* DXFeedCandleChartMac.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DXFeedCandleChartMac.app; sourceTree = BUILT_PRODUCTS_DIR; }; 64278C6B2A602CA20074B5AA /* CandleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandleTests.swift; sourceTree = ""; }; 64278C6D2A602D2B0074B5AA /* Candle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Candle.swift; sourceTree = ""; }; 64278C6F2A602FA00074B5AA /* Candle+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Candle+Ext.swift"; sourceTree = ""; }; @@ -662,6 +742,10 @@ 642BE4D12A2F5D230052340A /* TimeAndSale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAndSale.swift; sourceTree = ""; }; 642BE4D32A2F5D730052340A /* TimeAndSale+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeAndSale+Ext.swift"; sourceTree = ""; }; 642BE4D52A2F5E7C0052340A /* TimeAndSaleMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAndSaleMapper.swift; sourceTree = ""; }; + 642C9A202BFDE71C0074864A /* Array+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Ext.swift"; sourceTree = ""; }; + 642C9A242BFDE9F00074864A /* CandleChartModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandleChartModel.swift; sourceTree = ""; }; + 642C9A282BFDEAF20074864A /* CandlePickerType+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CandlePickerType+Ext.swift"; sourceTree = ""; }; + 642C9A2C2BFE2FFE0074864A /* DXMarketDepthTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXMarketDepthTest.swift; sourceTree = ""; }; 642DC9252AAA21C000974F5C /* DXIpfTableApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DXIpfTableApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 642DC9272AAA21C000974F5C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 642DC9292AAA21C000974F5C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -679,6 +763,11 @@ 643A329C2BD15F2900F6F790 /* LastEventsConsole.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = LastEventsConsole.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 643A329E2BD2A04300F6F790 /* OnDemandService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnDemandService.swift; sourceTree = ""; }; 643A32A12BD2AEFB00F6F790 /* NativeOnDemandService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeOnDemandService.swift; sourceTree = ""; }; + 643F41F22BDFE1B000A2176D /* DXFeedCandleChart.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DXFeedCandleChart.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 643F41F42BDFE1B000A2176D /* DXFeedCandleChartApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXFeedCandleChartApp.swift; sourceTree = ""; }; + 643F41F82BDFE1B200A2176D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 643F41FB2BDFE1B200A2176D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 643F42002BDFE25D00A2176D /* CandleChart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CandleChart.swift; sourceTree = ""; }; 64437A8E2A9DEE6F005929B2 /* InstrumentProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstrumentProfile.swift; sourceTree = ""; }; 64437A912A9DF1DE005929B2 /* NativeInstrumentProfileReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeInstrumentProfileReader.swift; sourceTree = ""; }; 644551C92B973A0D0069E3A2 /* FetchDailyCandles.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = FetchDailyCandles.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; @@ -751,6 +840,11 @@ 646D19E02A3C970700C82315 /* DXFTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXFTimer.swift; sourceTree = ""; }; 647426AC2ABC85F20012F793 /* Arguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arguments.swift; sourceTree = ""; }; 647426AE2ABC93900012F793 /* EventCode+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventCode+String.swift"; sourceTree = ""; }; + 647A51372BE4DDB600B8B8C9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 647A51392BE4DDB600B8B8C9 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 647A513A2BE4DDB600B8B8C9 /* DXFeedCandleChartMac.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = DXFeedCandleChartMac.entitlements; sourceTree = ""; }; + 647A513B2BE4DDB600B8B8C9 /* DXFeedCandleChartMacApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DXFeedCandleChartMacApp.swift; sourceTree = ""; }; + 647A514B2BE5102800B8B8C9 /* Color+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Ext.swift"; sourceTree = ""; }; 64820AAE2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXOtcMarketOrderTest.swift; sourceTree = ""; }; 6482F3D02BA492A60079AC3D /* SymbolsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolsViewController.swift; sourceTree = ""; }; 6482F3D62BA49AB70079AC3D /* AddSymbolsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSymbolsViewController.swift; sourceTree = ""; }; @@ -803,6 +897,13 @@ 64A42F4D2B0B9FA4001C3ACC /* DXTimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXTimeZone.swift; sourceTree = ""; }; 64A42F4F2B0BA668001C3ACC /* DXTimeFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXTimeFormat.swift; sourceTree = ""; }; 64A42F512B0BBB6D001C3ACC /* DXTimePeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXTimePeriod.swift; sourceTree = ""; }; + 64A586F22C00BC2600EE5306 /* DXFeedMarketDepth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DXFeedMarketDepth.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 64A586F42C00BC2700EE5306 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 64A586F62C00BC2700EE5306 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 64A586FB2C00BC2700EE5306 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainMarketDepth.storyboard; sourceTree = ""; }; + 64A586FD2C00BC2800EE5306 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 64A587002C00BC2800EE5306 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 64A587022C00BC2800EE5306 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 64A631CF2BDFAA27002E1002 /* OnDemandSample.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = OnDemandSample.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 64AAF0522A8113E800E8942B /* String+Range.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Range.swift"; sourceTree = ""; }; 64AAF0542A82499A00E8942B /* ConcurrentDict.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrentDict.swift; sourceTree = ""; }; @@ -945,6 +1046,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 64245BBA2BE4DD7B00901522 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 642DC9222AAA21C000974F5C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -953,6 +1061,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 643F41EF2BDFE1B000A2176D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 644BD7572A44726F00A0BF99 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -969,6 +1084,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 64A586EF2C00BC2600EE5306 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 64B436482AB9D3410003919E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -981,7 +1103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 64B627442A375CB700196D07 /* DXFeedFramework.framework in Frameworks */, + 648BC9502C06169A0065C2F7 /* DXFeedFramework.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1212,6 +1334,24 @@ path = OnDemandService; sourceTree = ""; }; + 643F41F32BDFE1B000A2176D /* DXFeedCandleChart */ = { + isa = PBXGroup; + children = ( + 643F41F42BDFE1B000A2176D /* DXFeedCandleChartApp.swift */, + 643F41F82BDFE1B200A2176D /* Assets.xcassets */, + 643F41FA2BDFE1B200A2176D /* Preview Content */, + ); + path = DXFeedCandleChart; + sourceTree = ""; + }; + 643F41FA2BDFE1B200A2176D /* Preview Content */ = { + isa = PBXGroup; + children = ( + 643F41FB2BDFE1B200A2176D /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; 64437A902A9DF1C4005929B2 /* Ipf */ = { isa = PBXGroup; children = ( @@ -1299,6 +1439,7 @@ 64A42F3A2B07A7A3001C3ACC /* SymbolParser.swift */, 640885C92B1F7EE700E6CF88 /* QdsUtils.swift */, 643A329A2BD0137000F6F790 /* Optional+Ext.swift */, + 6421FFE42BEA755700AC4657 /* Dictionary+Ext.swift */, ); path = Utils; sourceTree = ""; @@ -1338,6 +1479,30 @@ path = Utils; sourceTree = ""; }; + 647A51362BE4DDB600B8B8C9 /* DXFeedCandleChartMac */ = { + isa = PBXGroup; + children = ( + 647A51372BE4DDB600B8B8C9 /* Assets.xcassets */, + 647A51382BE4DDB600B8B8C9 /* Preview Content */, + 647A513A2BE4DDB600B8B8C9 /* DXFeedCandleChartMac.entitlements */, + 647A513B2BE4DDB600B8B8C9 /* DXFeedCandleChartMacApp.swift */, + 647A514B2BE5102800B8B8C9 /* Color+Ext.swift */, + 642C9A202BFDE71C0074864A /* Array+Ext.swift */, + 643F42002BDFE25D00A2176D /* CandleChart.swift */, + 642C9A242BFDE9F00074864A /* CandleChartModel.swift */, + 642C9A282BFDEAF20074864A /* CandlePickerType+Ext.swift */, + ); + path = DXFeedCandleChartMac; + sourceTree = ""; + }; + 647A51382BE4DDB600B8B8C9 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 647A51392BE4DDB600B8B8C9 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; 6486B9592AD00E0B00D8D5FA /* Extra */ = { isa = PBXGroup; children = ( @@ -1439,6 +1604,21 @@ path = DateTime; sourceTree = ""; }; + 64A586F32C00BC2700EE5306 /* DXFeedMarketDepth */ = { + isa = PBXGroup; + children = ( + 6421FFE82BEB717F00AC4657 /* OrderCell.swift */, + 6421FFD62BE8D78A00AC4657 /* MarketDepthViewController.swift */, + 64A586F42C00BC2700EE5306 /* AppDelegate.swift */, + 64A586F62C00BC2700EE5306 /* SceneDelegate.swift */, + 64A586FA2C00BC2700EE5306 /* MainMarketDepth.storyboard */, + 64A586FD2C00BC2800EE5306 /* Assets.xcassets */, + 64A586FF2C00BC2800EE5306 /* LaunchScreen.storyboard */, + 64A587022C00BC2800EE5306 /* Info.plist */, + ); + path = DXFeedMarketDepth; + sourceTree = ""; + }; 64ACBCDB2A28974900032C53 /* Osub */ = { isa = PBXGroup; children = ( @@ -1491,6 +1671,7 @@ 64B627762A37620D00196D07 /* Assets.xcassets */, 64B627702A3761EC00196D07 /* LaunchScreen.storyboard */, 64B627722A3761EC00196D07 /* Main.storyboard */, + 6421FFEB2BEBB53400AC4657 /* MarketDepthHeaderView.xib */, 64B627302A375C0F00196D07 /* Info.plist */, 64649B1C2BBAF9F400A33C25 /* PrivacyInfo.xcprivacy */, 64B6272D2A375C0E00196D07 /* AppDelegate.swift */, @@ -1503,6 +1684,7 @@ 6482F3D62BA49AB70079AC3D /* AddSymbolsViewController.swift */, 6482F3D92BA83AA20079AC3D /* SymbolCell.swift */, 640F8A522BA9C8D600C7BE22 /* SymbolsDataProvider.swift */, + 6421FFED2BEBB5AD00AC4657 /* MarketDepthHeaderView.swift */, ); path = QuoteTableApp; sourceTree = ""; @@ -1527,6 +1709,13 @@ isa = PBXGroup; children = ( 64C771FE2A9504ED009868C2 /* SnapshotProcessor.swift */, + 6421FFD42BE8D3C100AC4657 /* MarketDepthModel.swift */, + 6421FFDA2BEA1FBD00AC4657 /* MarketDepthModel+Ext.swift */, + 6421FFD82BEA18EC00AC4657 /* OrderSet.swift */, + 6421FFDC2BEA377100AC4657 /* TxMode.swift */, + 6421FFDE2BEA389100AC4657 /* IndexedTxModel.swift */, + 6421FFE22BEA5E4500AC4657 /* TxModelListener.swift */, + 6421FFE62BEA820900AC4657 /* MarketDepthListener.swift */, ); path = Extra; sourceTree = ""; @@ -1537,6 +1726,9 @@ 641E45FD2B1DF67E00649363 /* Playgrounds */, 6469F8D12A3B400100846831 /* Utils */, 644BD75B2A44726F00A0BF99 /* ARQuoteTableApp */, + 64A586F32C00BC2700EE5306 /* DXFeedMarketDepth */, + 643F41F32BDFE1B000A2176D /* DXFeedCandleChart */, + 647A51362BE4DDB600B8B8C9 /* DXFeedCandleChartMac */, 64D8BB3C2A39BB730071BC88 /* LatencyTestApp */, 64B6275D2A3761A000196D07 /* PertTestApp */, 64B627162A375BBA00196D07 /* QuoteTableApp */, @@ -1630,6 +1822,9 @@ 64B4364B2AB9D3410003919E /* ScheduleSampleApp.app */, 64148B642ABB5C320063110E /* Tools */, 6455C3B82B20A44D00257986 /* QdsTools.app */, + 643F41F22BDFE1B000A2176D /* DXFeedCandleChart.app */, + 64245BBD2BE4DD7B00901522 /* DXFeedCandleChartMac.app */, + 64A586F22C00BC2600EE5306 /* DXFeedMarketDepth.app */, ); name = Products; sourceTree = ""; @@ -1686,6 +1881,7 @@ 64820AAE2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift */, 644B95E62BC542F600E95CB7 /* DXAttachTest.swift */, 64048A5A2BD7E5BF00902590 /* DXOnDemandServiceTest.swift */, + 642C9A2C2BFE2FFE0074864A /* DXMarketDepthTest.swift */, ); path = DXFeedFrameworkTests; sourceTree = ""; @@ -1869,6 +2065,24 @@ productReference = 64148B642ABB5C320063110E /* Tools */; productType = "com.apple.product-type.tool"; }; + 64245BBC2BE4DD7B00901522 /* DXFeedCandleChartMac */ = { + isa = PBXNativeTarget; + buildConfigurationList = 64245BC92BE4DD7C00901522 /* Build configuration list for PBXNativeTarget "DXFeedCandleChartMac" */; + buildPhases = ( + 64245BB92BE4DD7B00901522 /* Sources */, + 64245BBA2BE4DD7B00901522 /* Frameworks */, + 64245BBB2BE4DD7B00901522 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 647A51432BE4DFBF00B8B8C9 /* PBXTargetDependency */, + ); + name = DXFeedCandleChartMac; + productName = DXFeedCandleChartMac; + productReference = 64245BBD2BE4DD7B00901522 /* DXFeedCandleChartMac.app */; + productType = "com.apple.product-type.application"; + }; 642DC9242AAA21C000974F5C /* DXIpfTableApp */ = { isa = PBXNativeTarget; buildConfigurationList = 642DC9382AAA21C300974F5C /* Build configuration list for PBXNativeTarget "DXIpfTableApp" */; @@ -1889,6 +2103,26 @@ productReference = 642DC9252AAA21C000974F5C /* DXIpfTableApp.app */; productType = "com.apple.product-type.application"; }; + 643F41F12BDFE1B000A2176D /* DXFeedCandleChart */ = { + isa = PBXNativeTarget; + buildConfigurationList = 643F41FD2BDFE1B200A2176D /* Build configuration list for PBXNativeTarget "DXFeedCandleChart" */; + buildPhases = ( + 643F41EE2BDFE1B000A2176D /* Sources */, + 643F41EF2BDFE1B000A2176D /* Frameworks */, + 643F41F02BDFE1B000A2176D /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 647A51472BE4E03A00B8B8C9 /* PBXTargetDependency */, + ); + name = DXFeedCandleChart; + packageProductDependencies = ( + ); + productName = DXFeedCandleChart; + productReference = 643F41F22BDFE1B000A2176D /* DXFeedCandleChart.app */; + productType = "com.apple.product-type.application"; + }; 644BD7592A44726F00A0BF99 /* DXARQuoteTableApp */ = { isa = PBXNativeTarget; buildConfigurationList = 644BD76C2A44727000A0BF99 /* Build configuration list for PBXNativeTarget "DXARQuoteTableApp" */; @@ -1928,6 +2162,24 @@ productReference = 6455C3B82B20A44D00257986 /* QdsTools.app */; productType = "com.apple.product-type.application"; }; + 64A586F12C00BC2600EE5306 /* DXFeedMarketDepth */ = { + isa = PBXNativeTarget; + buildConfigurationList = 64A587052C00BC2800EE5306 /* Build configuration list for PBXNativeTarget "DXFeedMarketDepth" */; + buildPhases = ( + 64A586EE2C00BC2600EE5306 /* Sources */, + 64A586EF2C00BC2600EE5306 /* Frameworks */, + 64A586F02C00BC2600EE5306 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 64A5870A2C00BDAF00EE5306 /* PBXTargetDependency */, + ); + name = DXFeedMarketDepth; + productName = DXFeedMarketDepth; + productReference = 64A586F22C00BC2600EE5306 /* DXFeedMarketDepth.app */; + productType = "com.apple.product-type.application"; + }; 64B4364A2AB9D3410003919E /* ScheduleSampleApp */ = { isa = PBXNativeTarget; buildConfigurationList = 64B4365E2AB9D3470003919E /* Build configuration list for PBXNativeTarget "ScheduleSampleApp" */; @@ -1954,7 +2206,7 @@ 64B627112A375BBA00196D07 /* Sources */, 64B627122A375BBA00196D07 /* Frameworks */, 64B627132A375BBA00196D07 /* Resources */, - 64B627482A375CB700196D07 /* Embed Frameworks */, + 648BC9522C06169B0065C2F7 /* Embed Frameworks */, ); buildRules = ( ); @@ -2052,21 +2304,30 @@ attributes = { BuildIndependentTargetsInParallel = 1; CLASSPREFIX = DX; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 1520; LastUpgradeCheck = 1420; TargetAttributes = { 64148B632ABB5C320063110E = { CreatedOnToolsVersion = 14.3; }; + 64245BBC2BE4DD7B00901522 = { + CreatedOnToolsVersion = 15.2; + }; 642DC9242AAA21C000974F5C = { CreatedOnToolsVersion = 15.0; }; + 643F41F12BDFE1B000A2176D = { + CreatedOnToolsVersion = 15.2; + }; 644BD7592A44726F00A0BF99 = { CreatedOnToolsVersion = 14.3; }; 6455C3B72B20A44D00257986 = { CreatedOnToolsVersion = 14.3; }; + 64A586F12C00BC2600EE5306 = { + CreatedOnToolsVersion = 15.2; + }; 64B4364A2AB9D3410003919E = { CreatedOnToolsVersion = 14.3; }; @@ -2101,6 +2362,8 @@ Base, ); mainGroup = 803BAC0329BFA50700FFAB1C; + packageReferences = ( + ); productRefGroup = 803BAC0E29BFA50700FFAB1C /* Products */; projectDirPath = ""; projectRoot = ""; @@ -2116,11 +2379,24 @@ 64B4364A2AB9D3410003919E /* ScheduleSampleApp */, 6455C3B72B20A44D00257986 /* QdsTools */, 64148B632ABB5C320063110E /* Tools */, + 643F41F12BDFE1B000A2176D /* DXFeedCandleChart */, + 64245BBC2BE4DD7B00901522 /* DXFeedCandleChartMac */, + 64A586F12C00BC2600EE5306 /* DXFeedMarketDepth */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 64245BBB2BE4DD7B00901522 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 647A51442BE4DFD600B8B8C9 /* Colors.xcassets in Resources */, + 647A513E2BE4DDB600B8B8C9 /* Preview Assets.xcassets in Resources */, + 647A513D2BE4DDB600B8B8C9 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 642DC9232AAA21C000974F5C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2132,6 +2408,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 643F41F02BDFE1B000A2176D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 643F42032BE3742D00A2176D /* Colors.xcassets in Resources */, + 643F41FC2BDFE1B200A2176D /* Preview Assets.xcassets in Resources */, + 643F41F92BDFE1B200A2176D /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 644BD7582A44726F00A0BF99 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2154,6 +2440,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 64A586F02C00BC2600EE5306 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 64A587012C00BC2800EE5306 /* LaunchScreen.storyboard in Resources */, + 64A5870C2C00BE1C00EE5306 /* Colors.xcassets in Resources */, + 64A586FE2C00BC2800EE5306 /* Assets.xcassets in Resources */, + 64A586FC2C00BC2700EE5306 /* MainMarketDepth.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 64B436492AB9D3410003919E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -2170,10 +2467,12 @@ buildActionMask = 2147483647; files = ( 64B627752A3761EC00196D07 /* Main.storyboard in Resources */, + 6421FFEC2BEBB53400AC4657 /* MarketDepthHeaderView.xib in Resources */, 646979722A3B5AF60003A9BA /* Colors.xcassets in Resources */, 64B627772A37620D00196D07 /* Assets.xcassets in Resources */, 64649B1D2BBAF9F400A33C25 /* PrivacyInfo.xcprivacy in Resources */, 64B627742A3761EC00196D07 /* LaunchScreen.storyboard in Resources */, + 646A86192C00D32C00566C0F /* MainMarketDepth.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2305,6 +2604,19 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 64245BB92BE4DD7B00901522 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 647A513F2BE4DDB600B8B8C9 /* DXFeedCandleChartMacApp.swift in Sources */, + 642C9A232BFDE71C0074864A /* Array+Ext.swift in Sources */, + 642C9A2B2BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */, + 647A51512BE512E100B8B8C9 /* CandleChart.swift in Sources */, + 642C9A272BFDE9F00074864A /* CandleChartModel.swift in Sources */, + 647A51532BE5141600B8B8C9 /* Color+Ext.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 642DC9212AAA21C000974F5C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2317,12 +2629,26 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 643F41EE2BDFE1B000A2176D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 647A51452BE4E02700B8B8C9 /* CandleChart.swift in Sources */, + 642C9A222BFDE71C0074864A /* Array+Ext.swift in Sources */, + 642C9A2A2BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */, + 643F41F52BDFE1B000A2176D /* DXFeedCandleChartApp.swift in Sources */, + 642C9A262BFDE9F00074864A /* CandleChartModel.swift in Sources */, + 647A51502BE5129200B8B8C9 /* Color+Ext.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 644BD7562A44726F00A0BF99 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 6482F3D22BA492A60079AC3D /* SymbolsViewController.swift in Sources */, 6482F3DB2BA83AA20079AC3D /* SymbolCell.swift in Sources */, + 6421FFEA2BEB718A00AC4657 /* SymbolsDataProvider.swift in Sources */, 6482F3D82BA49AB70079AC3D /* AddSymbolsViewController.swift in Sources */, 644BD7732A44746000A0BF99 /* QuoteCell.swift in Sources */, 644BD77A2A44746C00A0BF99 /* Colors.swift in Sources */, @@ -2332,7 +2658,6 @@ 644BD7702A44746000A0BF99 /* PaddingLabel.swift in Sources */, 644BD7712A44746000A0BF99 /* QuoteTableViewController.swift in Sources */, 644BD75D2A44726F00A0BF99 /* AppDelegate.swift in Sources */, - 640F8A542BA9C8D600C7BE22 /* SymbolsDataProvider.swift in Sources */, 644BD77C2A44746C00A0BF99 /* TimeInterval+Ext.swift in Sources */, 644BD77B2A44746C00A0BF99 /* Endpoint+Ext.swift in Sources */, 644BD77D2A44746C00A0BF99 /* MetricCell.swift in Sources */, @@ -2350,6 +2675,18 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 64A586EE2C00BC2600EE5306 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 646A86152C00C0E200566C0F /* Colors.swift in Sources */, + 64A586F52C00BC2700EE5306 /* AppDelegate.swift in Sources */, + 64A587062C00BD6500EE5306 /* MarketDepthViewController.swift in Sources */, + 64A5870B2C00BDDC00EE5306 /* OrderCell.swift in Sources */, + 64A586F72C00BC2700EE5306 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 64B436472AB9D3410003919E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -2366,21 +2703,29 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 642C9A212BFDE71C0074864A /* Array+Ext.swift in Sources */, 642528D12A3C534D00A04E41 /* TimeInterval+Ext.swift in Sources */, 64B6273A2A375C0F00196D07 /* QuoteCell.swift in Sources */, 640F8A532BA9C8D600C7BE22 /* SymbolsDataProvider.swift in Sources */, + 642C9A252BFDE9F00074864A /* CandleChartModel.swift in Sources */, 6482F3D72BA49AB70079AC3D /* AddSymbolsViewController.swift in Sources */, - 6469F8D32A3B401700846831 /* Colors.swift in Sources */, + 647A514F2BE5113A00B8B8C9 /* Color+Ext.swift in Sources */, 6469F8D62A3B408900846831 /* Endpoint+Ext.swift in Sources */, 64B6273B2A375C0F00196D07 /* PaddingLabel.swift in Sources */, 64B627402A375C0F00196D07 /* SceneDelegate.swift in Sources */, + 647A51482BE4EE0600B8B8C9 /* CandleChart.swift in Sources */, 6469F8D82A3B4AA400846831 /* MetricCell.swift in Sources */, + 646A86182C00CEC500566C0F /* OrderCell.swift in Sources */, + 646A86172C00CEBF00566C0F /* MarketDepthViewController.swift in Sources */, + 647A51492BE4F71700B8B8C9 /* Colors.swift in Sources */, 646D19E22A3C994000C82315 /* DXFTimer.swift in Sources */, 64B6273E2A375C0F00196D07 /* QuoteTableViewController.swift in Sources */, 64B627392A375C0F00196D07 /* AppDelegate.swift in Sources */, 64B627352A375C0F00196D07 /* QuoteModel.swift in Sources */, 6482F3DA2BA83AA20079AC3D /* SymbolCell.swift in Sources */, 6482F3D12BA492A60079AC3D /* SymbolsViewController.swift in Sources */, + 6421FFEE2BEBB5AD00AC4657 /* MarketDepthHeaderView.swift in Sources */, + 642C9A292BFDEAF20074864A /* CandlePickerType+Ext.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2445,6 +2790,8 @@ 80FC415929C8EE8B00E6B611 /* DXEndpoint.swift in Sources */, 64C771F62A94ADDA009868C2 /* Direction.swift in Sources */, 64BDDB312AD7E5A600694210 /* AnalyticOrderMapper.swift in Sources */, + 6421FFD52BE8D3C100AC4657 /* MarketDepthModel.swift in Sources */, + 6421FFE72BEA820900AC4657 /* MarketDepthListener.swift in Sources */, 640C3FDC2A618B2000555161 /* MarketEventSymbols.swift in Sources */, 641C64AC2B331B160023CFAD /* ObservableSubscriptionChangeListener.swift in Sources */, 642BE4D42A2F5D730052340A /* TimeAndSale+Ext.swift in Sources */, @@ -2460,6 +2807,7 @@ 64B436462AB985AE0003919E /* NativeBox.swift in Sources */, 6447A5E92A8F9E0B00739CCF /* TimeNanosUtil.swift in Sources */, 640C3FD82A617D4900555161 /* CandlePriceLevel.swift in Sources */, + 6421FFDB2BEA1FBD00AC4657 /* MarketDepthModel+Ext.swift in Sources */, 6498E6C42AB20B860093A065 /* ScheduleSessionType+Ext.swift in Sources */, 64656F592A1B864C006A0B19 /* NativeEndpoint.swift in Sources */, 6447A5DD2A8E56CF00739CCF /* ITimeSeriesEvent.swift in Sources */, @@ -2471,6 +2819,7 @@ 64E3637B2AD83459002E2B0D /* SeriesMapper.swift in Sources */, 642BE4CC2A2E1DB70052340A /* Mapper.swift in Sources */, 64104FC72A2613BC00D1FC41 /* ConcurrentArray.swift in Sources */, + 6421FFDF2BEA389100AC4657 /* IndexedTxModel.swift in Sources */, 6498E6B22AB1D41A0093A065 /* DXSchedule.swift in Sources */, 641C64B02B34679D0023CFAD /* NativeObservableSubscription.swift in Sources */, 64BDDB2C2AD7DB9B00694210 /* AnalyticOrder+Ext.swift in Sources */, @@ -2509,6 +2858,7 @@ 6469F8C82A3B25C900846831 /* MarketEvent+Access.swift in Sources */, 640C3FD22A6178D200555161 /* CandleSession.swift in Sources */, 6498E6BD2AB1E0510093A065 /* ScheduleSession.swift in Sources */, + 6421FFDD2BEA377100AC4657 /* TxMode.swift in Sources */, 64BDDB262AD6F6B500694210 /* IcebergType.swift in Sources */, 8088D76529C0FBCE00F240CB /* ThreadManager.swift in Sources */, 64F73BA62B67CD5B0088EC37 /* GraalException+Ext.swift in Sources */, @@ -2528,6 +2878,7 @@ 648E98AA2AAF625800BFD219 /* IIndexedEvent+Ext.swift in Sources */, 64A42F452B0B933B001C3ACC /* NativeTimeUtil.swift in Sources */, 6423E4672B44613D006B208D /* NativeTimeSeriesSubscription.swift in Sources */, + 6421FFE32BEA5E4500AC4657 /* TxModelListener.swift in Sources */, 6447A5E32A8F611700739CCF /* IObservableSubscription.swift in Sources */, 64A42F4E2B0B9FA4001C3ACC /* DXTimeZone.swift in Sources */, 64104FC92A26298D00D1FC41 /* DXFeedSubscription.swift in Sources */, @@ -2566,6 +2917,7 @@ 649706842AD82B070068FF88 /* Series.swift in Sources */, 6447A5E52A8F736E00739CCF /* TimeUtil.swift in Sources */, 64C771FC2A94D7E9009868C2 /* TradingStatus.swift in Sources */, + 6421FFE52BEA755700AC4657 /* Dictionary+Ext.swift in Sources */, 642BE4D22A2F5D230052340A /* TimeAndSale.swift in Sources */, 6486B9652AD038FC00D8D5FA /* Greeks+Ext.swift in Sources */, 64BA92692A306E6000BE26A0 /* TradeBase.swift in Sources */, @@ -2623,6 +2975,7 @@ 64656F5E2A1B97F2006A0B19 /* NativeFeed.swift in Sources */, 6486B97D2AD057F200D8D5FA /* OrderSource.swift in Sources */, 64656F6F2A1CFC12006A0B19 /* WeakBox.swift in Sources */, + 6421FFD92BEA18EC00AC4657 /* OrderSet.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2637,6 +2990,7 @@ 648C72492B19CA5A00E2FEF3 /* DXExceptionTest.swift in Sources */, 649813C42ADD5CB2003CE3B3 /* TestEndpoointStateListener.swift in Sources */, 641C64B42B347C430023CFAD /* DXObservableSubscriptionTest.swift in Sources */, + 642C9A2D2BFE2FFE0074864A /* DXMarketDepthTest.swift in Sources */, 64ACBCEC2A29FE2300032C53 /* XCTestCase+Utils.swift in Sources */, 6433B1322BCFC01F004EFED7 /* DXLastEventsSubscribedTest.swift in Sources */, 6423E4692B457000006B208D /* DXTimeSeriesSubscriptionTest.swift in Sources */, @@ -2707,6 +3061,21 @@ target = 803BAC0C29BFA50700FFAB1C /* DXFeedFramework */; targetProxy = 6455C3D02B20A61400257986 /* PBXContainerItemProxy */; }; + 647A51432BE4DFBF00B8B8C9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 803BAC0C29BFA50700FFAB1C /* DXFeedFramework */; + targetProxy = 647A51422BE4DFBF00B8B8C9 /* PBXContainerItemProxy */; + }; + 647A51472BE4E03A00B8B8C9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 803BAC0C29BFA50700FFAB1C /* DXFeedFramework */; + targetProxy = 647A51462BE4E03A00B8B8C9 /* PBXContainerItemProxy */; + }; + 64A5870A2C00BDAF00EE5306 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 803BAC0C29BFA50700FFAB1C /* DXFeedFramework */; + targetProxy = 64A587092C00BDAF00EE5306 /* PBXContainerItemProxy */; + }; 64B436602AB9DA470003919E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 803BAC0C29BFA50700FFAB1C /* DXFeedFramework */; @@ -2794,6 +3163,22 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; + 64A586FA2C00BC2700EE5306 /* MainMarketDepth.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 64A586FB2C00BC2700EE5306 /* Base */, + ); + name = MainMarketDepth.storyboard; + sourceTree = ""; + }; + 64A586FF2C00BC2800EE5306 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 64A587002C00BC2800EE5306 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; 64B436532AB9D3410003919E /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( @@ -2880,6 +3265,75 @@ }; name = Release; }; + 64245BCA2BE4DD7C00901522 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Samples/DXFeedCandleChartMac/DXFeedCandleChartMac.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Samples/DXFeedCandleChartMac/Preview Content\""; + DEVELOPMENT_TEAM = 485XM3FRHB; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedCandleChartMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 64245BCB2BE4DD7C00901522 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Samples/DXFeedCandleChartMac/DXFeedCandleChartMac.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Samples/DXFeedCandleChartMac/Preview Content\""; + DEVELOPMENT_TEAM = 485XM3FRHB; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 14.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedCandleChartMac; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; 642DC9362AAA21C300974F5C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -2952,6 +3406,76 @@ }; name = Release; }; + 643F41FE2BDFE1B200A2176D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedCandleChart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 643F41FF2BDFE1B200A2176D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = ""; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedCandleChart; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 644BD76A2A44727000A0BF99 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3084,6 +3608,74 @@ }; name = Release; }; + 64A587032C00BC2800EE5306 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Samples/DXFeedMarketDepth/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = MainMarketDepth; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedMarketDepth; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 64A587042C00BC2800EE5306 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Samples/DXFeedMarketDepth/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = MainMarketDepth; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.DXFeedMarketDepth; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; 64B4365C2AB9D3470003919E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3154,7 +3746,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 485XM3FRHB; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Samples/QuoteTableApp/Info.plist; @@ -3165,12 +3757,12 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.quotesapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -3188,7 +3780,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 3; DEVELOPMENT_TEAM = 485XM3FRHB; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Samples/QuoteTableApp/Info.plist; @@ -3199,12 +3791,12 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 15.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.dxfeed.quotesapp; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -3653,6 +4245,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 64245BC92BE4DD7C00901522 /* Build configuration list for PBXNativeTarget "DXFeedCandleChartMac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 64245BCA2BE4DD7C00901522 /* Debug */, + 64245BCB2BE4DD7C00901522 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 642DC9382AAA21C300974F5C /* Build configuration list for PBXNativeTarget "DXIpfTableApp" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -3662,6 +4263,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 643F41FD2BDFE1B200A2176D /* Build configuration list for PBXNativeTarget "DXFeedCandleChart" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 643F41FE2BDFE1B200A2176D /* Debug */, + 643F41FF2BDFE1B200A2176D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 644BD76C2A44727000A0BF99 /* Build configuration list for PBXNativeTarget "DXARQuoteTableApp" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -3680,6 +4290,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 64A587052C00BC2800EE5306 /* Build configuration list for PBXNativeTarget "DXFeedMarketDepth" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 64A587032C00BC2800EE5306 /* Debug */, + 64A587042C00BC2800EE5306 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 64B4365E2AB9D3470003919E /* Build configuration list for PBXNativeTarget "ScheduleSampleApp" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/DXFeedFramework/Api/Osub/IndexedEventSubscriptionSymbol.swift b/DXFeedFramework/Api/Osub/IndexedEventSubscriptionSymbol.swift index 661e4898d..0a24bf160 100644 --- a/DXFeedFramework/Api/Osub/IndexedEventSubscriptionSymbol.swift +++ b/DXFeedFramework/Api/Osub/IndexedEventSubscriptionSymbol.swift @@ -64,10 +64,16 @@ public class IndexedEventSubscriptionSymbol: Symbol { /// Custom symbol has to return string representation. public var stringValue: String { - return "\(symbol.stringValue)source=\(source.toString())" + return "\(symbol.stringValue){source=\(source.toString())}" } } +extension IndexedEventSubscriptionSymbol: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.symbol.stringValue) + hasher.combine(self.source.toString()) + } +} extension IndexedEventSubscriptionSymbol: Equatable { public static func == (lhs: IndexedEventSubscriptionSymbol, rhs: IndexedEventSubscriptionSymbol) -> Bool { diff --git a/DXFeedFramework/Events/Market/Extra/OrderBase.swift b/DXFeedFramework/Events/Market/Extra/OrderBase.swift index 95c047425..b4f8c677d 100644 --- a/DXFeedFramework/Events/Market/Extra/OrderBase.swift +++ b/DXFeedFramework/Events/Market/Extra/OrderBase.swift @@ -311,3 +311,20 @@ extension OrderBase { } } } + +public extension OrderBase { + /// Returns true if this order has some size (sizeAsDouble is neither 0 nor NaN). + func hasSize() -> Bool { + return size != 0 && !size.isNaN + } +} + +extension OrderBase: Hashable { + public static func == (lhs: OrderBase, rhs: OrderBase) -> Bool { + return lhs.index == rhs.index + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(self.index) + } +} diff --git a/DXFeedFramework/Events/Market/Extra/OrderSource.swift b/DXFeedFramework/Events/Market/Extra/OrderSource.swift index 513bd8d56..bffc8d9e8 100644 --- a/DXFeedFramework/Events/Market/Extra/OrderSource.swift +++ b/DXFeedFramework/Events/Market/Extra/OrderSource.swift @@ -45,7 +45,7 @@ public class OrderSource: IndexedEventSource { /// It is a synthetic source. /// The subscription on composite ``Quote`` event is observed when this source is subscribed to. public static let compsoiteBid = - try? OrderSource(1, "COMPOSITE_BID", pubOrder | pubAnalyticOrder | pubSpreadOrder | fullOrderBook) + try? OrderSource(1, "COMPOSITE_BID", 0) /// Ask side of a composite ``Quote``. /// It is a synthetic source. /// The subscription on composite ``Quote`` event is observed when this source is subscribed to. @@ -95,12 +95,18 @@ public class OrderSource: IndexedEventSource { /// Direct-Edge EDGX Exchange. public static let DEX = try? OrderSource("DEX", pubOrder) + /// Direct-Edge EDGX Exchange. Record for price level book. + public static let dex = try? OrderSource("dex", pubOrder) + /// Bats BYX Exchange. public static let BYX = try? OrderSource("BYX", pubOrder) /// Bats BZX Exchange. public static let BZX = try? OrderSource("BZX", pubOrder) + /// Bats BZX Exchange. Record for price level book. + public static let bzx = try? OrderSource("bzx", pubOrder) + /// Bats Europe BXE Exchange. public static let BATE = try? OrderSource("BATE", pubOrder) @@ -168,6 +174,8 @@ public class OrderSource: IndexedEventSource { /// Pink sheets are listings for stocks that trade over-the-counter (OTC). public static let pink = try? OrderSource("pink", pubOrder | pubOtcMarketsOrder) + private static var publishableViews = [[OrderSource]](repeating: [OrderSource](), count: flagsSize) + /// Don't use it. Just for initialization all static variable. /// static let - is always lazy initialized fileprivate static let allValues = [OrderSource.defaultOrderSource, @@ -186,8 +194,10 @@ public class OrderSource: IndexedEventSource { OrderSource.ISE, OrderSource.DEA, OrderSource.DEX, + OrderSource.dex, OrderSource.BYX, OrderSource.BZX, + OrderSource.bzx, OrderSource.BATE, OrderSource.CHIX, OrderSource.CEUX, @@ -208,6 +218,7 @@ public class OrderSource: IndexedEventSource { OrderSource.iex, OrderSource.MEMX, OrderSource.memx, + OrderSource.OCEA, OrderSource.pink] override init(_ identifier: Int, _ name: String) { @@ -247,6 +258,10 @@ public class OrderSource: IndexedEventSource { if !OrderSource.sourcesByName.tryInsert(key: name, value: self) { throw ArgumentException.exception("duplicate name \(name)") } + + for index in 0.. Bool { @@ -304,17 +319,29 @@ public class OrderSource: IndexedEventSource { } return name } + /// Gets a value indicating whether this source supports Full Order Book. public func isFullOrderBook() -> Bool { return (pubFlags & OrderSource.fullOrderBook) != 0 } + /// Returns a list of publishable order sources for a given event type. + /// + /// - Parameters: + /// - eventype : Possible values ``Order``, ``AnalyticOrder``, ``SpreadOrder``, ``OtcMarketsOrder`` + /// - Returns: a list of publishable order sources. + /// - Throws: ``ArgumentException/exception(_:)`` + public static func publishable(eventType: AnyClass) throws -> [OrderSource] { + let index = Int32(try OrderSource.getEventTypeMask(eventType)).leadingZeroBitCount + return publishableViews[31 - index] + } + /// Gets a value indicating whether the given event type can be directly published with this source. /// /// Subscription on such sources can be observed directly via ``DXPublisher`` /// Subscription on such sources is observed via instances of ``GenericIndexedEventSubscriptionSymbol`` /// - Parameters: - /// - eventype : Possible values ``Order``, ``AnalyticOrder``, ``SpreadOrder`` + /// - eventype : Possible values ``Order``, ``AnalyticOrder``, ``SpreadOrder``, ``OtcMarketsOrder`` /// - Returns: true- events can be directly published with this source /// - Throws: ``ArgumentException/exception(_:)`` public func isPublishable(eventType: AnyClass.Type) throws -> Bool { @@ -345,7 +372,7 @@ public class OrderSource: IndexedEventSource { /// Gets type mask by specified event type. /// /// - Parameters: - /// - eventype : Possible values ``Order``, ``AnalyticOrder``, ``SpreadOrder`` + /// - eventype : Possible values ``Order``, ``AnalyticOrder``, ``SpreadOrder``, ``OtcMarketsOrder`` /// - Returns: The mask for event class. /// - Throws: ``ArgumentException/exception(_:)`` public static func getEventTypeMask(_ eventType: AnyClass) throws -> Int { diff --git a/DXFeedFramework/Events/Market/Extra/Scope.swift b/DXFeedFramework/Events/Market/Extra/Scope.swift index c9bcbf165..33a684551 100644 --- a/DXFeedFramework/Events/Market/Extra/Scope.swift +++ b/DXFeedFramework/Events/Market/Extra/Scope.swift @@ -20,6 +20,11 @@ public enum Scope: Int, CaseIterable { /// Represents individual order on the market. case order + + var code: Int { + return self.rawValue + } + } /// Class extension for ``ScopeExt`` enum. diff --git a/DXFeedFramework/Extra/IndexedTxModel.swift b/DXFeedFramework/Extra/IndexedTxModel.swift new file mode 100644 index 000000000..3b29e496d --- /dev/null +++ b/DXFeedFramework/Extra/IndexedTxModel.swift @@ -0,0 +1,173 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +class IndexedTxModel { + class Changes { + let isSnapshot: Bool + let source: IndexedEventSource + let events: [Order] + + init(isSnapshot: Bool, + source: IndexedEventSource, + events: [Order]) { + self.isSnapshot = isSnapshot + self.source = source + self.events = events + } + } + var mode: TxMode + fileprivate var sourceTxDict = [Int: SourceTx]() + weak var listener: TxModelListener? + + init(mode: TxMode) { + self.mode = mode + } + + func setListener(_ listener: TxModelListener) { + self.listener = listener + } +} + +extension IndexedTxModel: TxModelListener { + func modelChanged(changes: Changes) { + self.listener?.modelChanged(changes: changes) + } +} + +extension IndexedTxModel: DXEventListener { + func receiveEvents(_ events: [MarketEvent]) { + var sourceTx: SourceTx? + events.forEach { marketEvent in + guard let order = marketEvent as? Order else { + return + } + let sourceId = order.eventSource.identifier + if sourceTx == nil || sourceId != sourceTx?.source.identifier { + sourceTx = geTxProcessorForEvent(order) + } + sourceTx?.processEvent(order) + } + onBatchReceived() + } + + private func geTxProcessorForEvent(_ event: Order) -> SourceTx { + let sourceId = event.eventSource.identifier + guard let sourceTx = sourceTxDict[sourceId] else { + let sourceTx = SourceTx(source: event.eventSource, mode: mode, listener: self) + sourceTxDict[sourceId] = sourceTx + return sourceTx + } + return sourceTx + } + + private func onBatchReceived() { + if mode.isBatchProcessing() { + notifyListenerForAllSources() + } + } + + private func notifyListenerForAllSources() { + sourceTxDict.values.forEach { sourceTx in + sourceTx.notifyListener() + } + } +} + +extension IndexedTxModel: Hashable { + public static func == (lhs: IndexedTxModel, rhs: IndexedTxModel) -> Bool { + return lhs === rhs + } + + public func hash(into hasher: inout Hasher) { + hasher.combine("\(self):\(stringReference(self))") + } +} + +private class SourceTx { + private var isPartialSnapshot = false + private var isCompleteSnapshot = false + + private weak var snapshotDelegate: SnapshotDelegate? + private var pendingEvents = [Order]() + private var processedEvents = [Order]() + + let source: IndexedEventSource + let mode: TxMode + + weak var listener: TxModelListener? + + init(source: IndexedEventSource, + mode: TxMode, + listener: TxModelListener) { + self.source = source + self.mode = mode + self.listener = listener + } + + func processEvent(_ event: Order) { + if event.snapshotBegin() { + isPartialSnapshot = true + isCompleteSnapshot = false + pendingEvents.removeAll() + } + if isPartialSnapshot && event.endOrSnap() { + isPartialSnapshot = false + isCompleteSnapshot = true + } + if event.pending() || isPartialSnapshot { + pendingEvents.append(event) + return + } + let isSnapshot = isCompleteSnapshot + if isCompleteSnapshot { + isCompleteSnapshot = false + processedEvents.removeAll() + } + if !pendingEvents.isEmpty { + processedEvents.append(contentsOf: pendingEvents) + pendingEvents.removeAll() +// if isSnapshot { +// pendingEvents.trimToSize() +// } + } + processedEvents.append(event) + onTransactionReceived(isSnapshot) + } + private func onTransactionReceived(_ isSnapshot: Bool) { + if mode.isBatchProcessing() { + if isSnapshot { + notifyListener(true) + } + } else { + notifyListener(isSnapshot) + } + } + + public func notifyListener() { + notifyListener(false) + } + + public func notifyListener(_ isSnapshot: Bool) { + if processedEvents.isEmpty { + return + } + defer { + processedEvents.removeAll() + // if (isSnapshot) + // pendingEvents.trimToSize(); + } + if listener == nil { + return + } + + listener?.modelChanged(changes: IndexedTxModel.Changes(isSnapshot: isSnapshot, + source: source, + events: processedEvents)) + } +} diff --git a/DXFeedFramework/Extra/MarketDepthListener.swift b/DXFeedFramework/Extra/MarketDepthListener.swift new file mode 100644 index 000000000..0df98de1c --- /dev/null +++ b/DXFeedFramework/Extra/MarketDepthListener.swift @@ -0,0 +1,26 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +public class OrderBook { + public let buyOrders: [Order] + public let sellOrders: [Order] + public let name = "ASDas" + init(buyOrders: [Order], sellOrders: [Order]) { + self.buyOrders = buyOrders + self.sellOrders = sellOrders + } + + public convenience init() { + self.init(buyOrders: [Order](), sellOrders: [Order]()) + } +} + +public protocol MarketDepthListener: AnyObject { + func modelChanged(changes: OrderBook) +} diff --git a/DXFeedFramework/Extra/MarketDepthModel+Ext.swift b/DXFeedFramework/Extra/MarketDepthModel+Ext.swift new file mode 100644 index 000000000..2e3aadb53 --- /dev/null +++ b/DXFeedFramework/Extra/MarketDepthModel+Ext.swift @@ -0,0 +1,66 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +extension MarketDepthModel { + static let orderComparator: (Order, Order) -> ComparisonResult = { order1, order2 in + let ind1 = order1.scope == Scope.order + let ind2 = order2.scope == Scope.order + if ind1 && ind2 { + // Both orders are individual orders + var compare = order1.timeSequence.compare(order2.timeSequence) // asc + if compare != .orderedSame { + return compare + } + compare = order1.index.compare(order2.index) // asc + return compare + } else if ind1 { + // First order is individual, second is not + return .orderedDescending + } else if ind2 { + // Second order is individual, first is not + return .orderedAscending + } else { + // Both orders are non-individual orders + var compare = order2.size.compare(order1.size) // desc + if compare != .orderedSame { + return compare + } + compare = order1.timeSequence.compare(order2.timeSequence) // asc + if compare != .orderedSame { + return compare + } + compare = ComparisonResult(rawValue: order1.scope.code - order2.scope.code)! // asc + if compare != .orderedSame { + return compare + } + compare = ComparisonResult(rawValue: order1.getExchangeCode() - order2.getExchangeCode())! // asc + if compare != .orderedSame { + return compare + } + if let o1marketMaker = order1.marketMaker, let o2marketMaker = order2.marketMaker { + let compare = o1marketMaker.compare(o2marketMaker) + if compare != .orderedSame { + return compare + } + } + compare = order1.index.compare(order2.index) // asc + return compare + } + } + + static let buyComparator: (Order, Order) -> ComparisonResult = { order1, order2 in + order1.price < order2.price ? .orderedDescending : + (order1.price > order2.price ? .orderedAscending : orderComparator(order1, order2)) + } + + static let sellComparator: (Order, Order) -> ComparisonResult = { order1, order2 in + order1.price < order2.price ? .orderedAscending : + (order1.price > order2.price ? .orderedDescending : orderComparator(order1, order2)) + } +} diff --git a/DXFeedFramework/Extra/MarketDepthModel.swift b/DXFeedFramework/Extra/MarketDepthModel.swift new file mode 100644 index 000000000..6e665e603 --- /dev/null +++ b/DXFeedFramework/Extra/MarketDepthModel.swift @@ -0,0 +1,150 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +public class MarketDepthModel { + let symbol: String + let sources: [OrderSource] + let feed: DXFeed + + var depthLimit: Int = 0 + + let subscription: DXFeedSubscription + + let buyOrders = OrderSet(comparator: buyComparator) + let sellOrders = OrderSet(comparator: sellComparator) + var ordersByIndex = [Long: Order]() + + let aggregationPeriodMillis: Long + + let txModel: IndexedTxModel + weak var listener: MarketDepthListener? + var task: DispatchWorkItem? + + public init(symbol: String, + sources: [OrderSource], + aggregationPeriodMillis: Long, + mode: TxMode, + feed: DXFeed, + listener: MarketDepthListener) throws { + txModel = IndexedTxModel(mode: mode) + self.aggregationPeriodMillis = aggregationPeriodMillis + self.listener = listener + self.symbol = symbol + self.sources = sources + self.feed = feed + self.subscription = try feed.createSubscription(Order.self) + + txModel.setListener(self) + try self.subscription.add(listener: txModel) + if sources.count == 0 { + try self.subscription.setSymbols([symbol]) + } else { + let symbols = sources.map { source in + IndexedEventSubscriptionSymbol(symbol: symbol, source: source) + } + try self.subscription.setSymbols(symbols) + } + } + + public func setListener(_ listener: MarketDepthListener?) { + self.listener = listener + } + + public func setDepthLimit(_ depthLimit: Int) { + if self.depthLimit == depthLimit { + return + } + buyOrders.depthLimit = depthLimit + sellOrders.depthLimit = depthLimit + // cancel scheduled listeners + notifyListeners() + } +} + +extension MarketDepthModel: TxModelListener { + func modelChanged(changes: IndexedTxModel.Changes) { + if update(changes) { + if changes.isSnapshot || aggregationPeriodMillis == 0 { + tryCancelTask() + notifyListeners() + } else { + scheduleTaskIfNeeded() + } + } + } + + func tryCancelTask() { + task?.cancel() + task = nil + } + + func scheduleTaskIfNeeded() { + if aggregationPeriodMillis > 0 { + if task == nil { + task?.cancel() + let task = DispatchWorkItem { + self.notifyListeners() + } + DispatchQueue.global(qos: .background) + .asyncAfter(deadline: .now() + (Double(aggregationPeriodMillis) / 1000), + execute: task) + self.task = task + } + } + } + + func notifyListeners() { + if listener == nil { + return + } + listener?.modelChanged(changes: OrderBook(buyOrders: buyOrders.toList(), sellOrders: sellOrders.toList())) + task = nil + } + + func update(_ changes: IndexedTxModel.Changes) -> Bool { + if changes.isSnapshot { + clearBySource(source: changes.source) + } + changes.events.forEach { order in + let removed = ordersByIndex.removeValue(forKey: order.index) + if let removed = removed { + getOrderSetForOrder(removed).remove(removed) + } + if shallAdd(order) { + ordersByIndex[order.index] = order + getOrderSetForOrder(order).add(order) + } + } + return isChanged() + } + + func isChanged() -> Bool { + return buyOrders.isChanged || sellOrders.isChanged + } + + func clearBySource(source: IndexedEventSource) { + ordersByIndex.removeIf(condition: { element in + element.value.eventSource == source + }) + buyOrders.clearBySource(source) + sellOrders.clearBySource(source) + } + + func getOrderSetForOrder(_ order: Order) -> OrderSet { + if order.orderSide == .buy { + return buyOrders + } else { + return sellOrders + } + } + + func shallAdd(_ order: Order) -> Bool { + return order.hasSize() && (!order.isRemove()) + } +} diff --git a/DXFeedFramework/Extra/OrderSet.swift b/DXFeedFramework/Extra/OrderSet.swift new file mode 100644 index 000000000..2018a12cb --- /dev/null +++ b/DXFeedFramework/Extra/OrderSet.swift @@ -0,0 +1,119 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +class OrderSet { + private var lock = pthread_rwlock_t() + private var snapshot = NSMutableArray() + private let comparator: (Order, Order) -> ComparisonResult + /// add using comparator in orders + private var orders = NSMutableOrderedSet() + var depthLimit: Int = 0 { + willSet { + if newValue != depthLimit { + isChanged = true + } + } + } + + private(set) var isChanged: Bool = false + + deinit { + pthread_rwlock_destroy(&lock) + } + + init(comparator: @escaping (Order, Order) -> ComparisonResult) { + pthread_rwlock_init(&lock, nil) + self.comparator = comparator + } + + func clearBySource(_ source: IndexedEventSource) { + pthread_rwlock_wrlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + let predicate = NSPredicate { order, _ in + (order as? Order)?.eventSource == source + } + isChanged = orders.removeIf(using: predicate) + } + + func remove(_ order: Order) { + pthread_rwlock_wrlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + if orders.contains(order) { + orders.remove(order) + markAsChangedIfNeeded(order) + } + } + + func markAsChangedIfNeeded(_ order: Order) { + if isChanged { + return + } + if isDepthLimitUnbounded() || isSizeWithinDepthLimit() || isOrderWithinDepthLimit(order) { + isChanged = true + } + } + + func add(_ order: Order) { + pthread_rwlock_wrlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + if !orders.contains(order) { + orders.add(order) + markAsChangedIfNeeded(order) + } + } + + func isDepthLimitUnbounded() -> Bool { + return depthLimit <= 0 || depthLimit == .max + } + + func isSizeWithinDepthLimit() -> Bool { + return orders.count <= depthLimit + } + + func isOrderWithinDepthLimit(_ order: Order) -> Bool { + if snapshot.count == 0 { + return true + } + if let last = snapshot.lastObject as? Order { + let compareResult = comparator(last, order) + return compareResult == .orderedSame || compareResult == .orderedDescending + } + return false + } + + func toList() -> [Order] { + if isChanged { + updateSnapshot() + } + if let list = snapshot as? [Order] { + return list + } else { + fatalError("failed casting to array") + } + } + + func updateSnapshot() { + pthread_rwlock_rdlock(&lock) + defer { pthread_rwlock_unlock(&lock) } + + isChanged = false + snapshot.removeAllObjects() + let limit = isDepthLimitUnbounded() ? .max : depthLimit + let sorted = orders.sortedArray { obj1, obj2 in + if let order1 = obj1 as? Order, + let order2 = obj2 as? Order { + return self.comparator(order1, order2) + } + return .orderedSame + } + for (index, element) in sorted.enumerated() where index < limit { + snapshot.add(element) + } + } +} diff --git a/DXFeedFramework/Extra/TxMode.swift b/DXFeedFramework/Extra/TxMode.swift new file mode 100644 index 000000000..79cbda4d0 --- /dev/null +++ b/DXFeedFramework/Extra/TxMode.swift @@ -0,0 +1,17 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +public enum TxMode { + case single + case multiple + + func isBatchProcessing() -> Bool { + self == .multiple + } +} diff --git a/DXFeedFramework/Extra/TxModelListener.swift b/DXFeedFramework/Extra/TxModelListener.swift new file mode 100644 index 000000000..7f9c88c5d --- /dev/null +++ b/DXFeedFramework/Extra/TxModelListener.swift @@ -0,0 +1,12 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +protocol TxModelListener: AnyObject { + func modelChanged(changes: IndexedTxModel.Changes) +} diff --git a/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift b/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift index 6d61e1c25..c740f023f 100644 --- a/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift +++ b/DXFeedFramework/Native/Events/Markets/IndexedEventSource+Ext.swift @@ -21,4 +21,23 @@ extension IndexedEventSource { } return nativeSource } + + static func fromNative(native: UnsafeMutablePointer) -> IndexedEventSource { + switch native.pointee.type { + case ORDER_SOURCE: + let identifier = native.pointee.id + let sourceName = String(pointee: native.pointee.name) + if let source = try? OrderSource.valueOf(identifier: Int(identifier)) { + return source + } else if let source = try? OrderSource.valueOf(name: sourceName) { + return source + } else { + fatalError("Incorrect value of source \(native.pointee.id) \(sourceName)") + } + case INDEXED_EVENT_SOURCE: + return IndexedEventSource(Int(native.pointee.id), String(pointee: native.pointee.name)) + default: + fatalError("Incorrect value of source \(native.pointee.type)") + } + } } diff --git a/DXFeedFramework/Native/SymbolMappers/SymbolMapper.swift b/DXFeedFramework/Native/SymbolMappers/SymbolMapper.swift index 5f8cb4535..e914c999c 100644 --- a/DXFeedFramework/Native/SymbolMappers/SymbolMapper.swift +++ b/DXFeedFramework/Native/SymbolMappers/SymbolMapper.swift @@ -35,7 +35,14 @@ class SymbolMapper { pointer.pointee.from_time = symbol.fromTime let casted = pointer.withMemoryRebound(to: dxfg_symbol_t.self, capacity: 1) { $0 } return casted - + case let symbol as IndexedEventSubscriptionSymbol: + let pointer = UnsafeMutablePointer.allocate(capacity: 1) + pointer.pointee.supper = dxfg_symbol_t(type: INDEXED_EVENT_SUBSCRIPTION) + pointer.pointee.symbol = newNative(symbol.symbol) + let nativeSource = symbol.source.toNative() + pointer.pointee.source = nativeSource + let casted = pointer.withMemoryRebound(to: dxfg_symbol_t.self, capacity: 1) { $0 } + return casted default: let pointer = UnsafeMutablePointer.allocate(capacity: 1) pointer.pointee.supper = dxfg_symbol_t(type: STRING) @@ -62,7 +69,14 @@ class SymbolMapper { } case WILDCARD: break - case INDEXED_EVENT_SUBSCRIPTION: fatalError("Add case for INDEXED_EVENT_SUBSCRIPTION") + case INDEXED_EVENT_SUBSCRIPTION: + symbol.withMemoryRebound(to: dxfg_indexed_event_subscription_symbol_t.self, capacity: 1) { + clearNative(symbol: $0.pointee.symbol) + $0.pointee.source.deinitialize(count: 1) + $0.pointee.source.deallocate() + $0.deinitialize(count: 1) + $0.deallocate() + } case TIME_SERIES_SUBSCRIPTION: symbol.withMemoryRebound(to: dxfg_time_series_subscription_symbol_t.self, capacity: 1) { clearNative(symbol: $0.pointee.symbol) @@ -102,7 +116,17 @@ class SymbolMapper { return try? CandleSymbol.valueOf(result) case WILDCARD: return WildcardSymbol.all - case INDEXED_EVENT_SUBSCRIPTION: fatalError("Add case for INDEXED_EVENT_SUBSCRIPTION") + case INDEXED_EVENT_SUBSCRIPTION: + let result: IndexedEventSubscriptionSymbol? = native.withMemoryRebound( + to: dxfg_indexed_event_subscription_symbol_t.self, capacity: 1) { pointer in + if let symbol = SymbolMapper.newSymbol(native: pointer.pointee.symbol) as? Symbol { + let source = IndexedEventSource.fromNative(native: pointer.pointee.source) + return IndexedEventSubscriptionSymbol(symbol: symbol, source: source) + } else { + return nil + } + } + return result case TIME_SERIES_SUBSCRIPTION: let result: TimeSeriesSubscriptionSymbol? = native.withMemoryRebound( to: dxfg_time_series_subscription_symbol_t.self, capacity: 1) { pointer in diff --git a/DXFeedFramework/Native/Utils/NativeTimeUtil.swift b/DXFeedFramework/Native/Utils/NativeTimeUtil.swift index 651b2826c..2907647fa 100644 --- a/DXFeedFramework/Native/Utils/NativeTimeUtil.swift +++ b/DXFeedFramework/Native/Utils/NativeTimeUtil.swift @@ -23,7 +23,7 @@ class NativeTimeUtil { let result = try ErrorCheck.nativeCall(thread, dxfg_TimeFormat_format(thread, timeFormat.native, - value)) + value)) return try String(nullable: result).value() } diff --git a/DXFeedFramework/Utils/BinaryInteger+Ext.swift b/DXFeedFramework/Utils/BinaryInteger+Ext.swift index 60416290c..f62edab4c 100644 --- a/DXFeedFramework/Utils/BinaryInteger+Ext.swift +++ b/DXFeedFramework/Utils/BinaryInteger+Ext.swift @@ -28,6 +28,28 @@ extension BinaryInteger { } func toHexString() -> String { - return "0x\(String(format: "%01X", Int(self)))" + return "0x\(String(self, radix: 16))" + } + + func compare(_ rhs: Self) -> ComparisonResult { + if self < rhs { + return .orderedAscending + } else if self > rhs { + return .orderedDescending + } else { + return .orderedSame + } + } +} + +extension BinaryFloatingPoint { + func compare(_ rhs: Self) -> ComparisonResult { + if self < rhs { + return .orderedAscending + } else if self > rhs { + return .orderedDescending + } else { + return .orderedSame + } } } diff --git a/DXFeedFramework/Utils/Dictionary+Ext.swift b/DXFeedFramework/Utils/Dictionary+Ext.swift new file mode 100644 index 000000000..355ebbeb3 --- /dev/null +++ b/DXFeedFramework/Utils/Dictionary+Ext.swift @@ -0,0 +1,46 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +public extension Dictionary { + @inlinable mutating func removeIf(condition: (Self.Element) -> Bool) { + for (key, _) in filter(condition) { removeValue(forKey: key) } + } +} + +public extension Set { + mutating func removeIf(condition: (Element) throws -> Bool) rethrows -> Bool { + for value in try self.filter(condition) { + remove(value) + return true + } + return false + } +} + +public extension NSMutableOrderedSet { + func removeIf(using: NSPredicate) -> Bool { + var changed = false + for (value) in filtered(using: using) { + remove(value) + changed = true + } + return changed + } +} + +public extension NSMutableSet { + func removeIf(using: NSPredicate) -> Bool { + var changed = false + for (value) in filtered(using: using) { + remove(value) + changed = true + } + return changed + } +} diff --git a/DXFeedFrameworkTests/DXConnectionTest.swift b/DXFeedFrameworkTests/DXConnectionTest.swift index 4b6bd1763..c73cb9a91 100644 --- a/DXFeedFrameworkTests/DXConnectionTest.swift +++ b/DXFeedFrameworkTests/DXConnectionTest.swift @@ -48,16 +48,20 @@ final class DXConnectionTest: XCTestCase { let endpoint = try DXEndpoint.builder() .build() - let subscription = try endpoint.getFeed()?.createSubscription(Quote.self) + let subscription = try endpoint.getFeed()?.createSubscription(Candle.self) let receivedEventsExpectation = expectation(description: "Received events") let eventListener = DXConnectionListener(expectation: receivedEventsExpectation) try subscription?.add(listener: eventListener) - try subscription?.addSymbols("AAPL") + let startDate = Calendar.current.date(byAdding: .month, value: -1, to: Date())! + + let symbol = TimeSeriesSubscriptionSymbol(symbol: "ETH/USD:GDAX{=d}", date: startDate) + + try subscription?.addSymbols(symbol) try endpoint.connect("dxlink:wss://demo.dxfeed.com/dxlink-ws") defer { try? endpoint.closeAndAwaitTermination() } - wait(for: [receivedEventsExpectation], timeout: 4) + wait(for: [receivedEventsExpectation], timeout: 20) } func testDXLinkConnectionTheoPrice() throws { @@ -84,7 +88,7 @@ final class DXConnectionTest: XCTestCase { func testDXLinkConnectionGreeks() throws { throw XCTSkip("Just for reflection testing") - + // For token-based authorization, use the following address format: // "dxlink:wss://demo.dxfeed.com/dxlink-ws[login=dxlink:token]" let endpoint = try DXEndpoint.builder() diff --git a/DXFeedFrameworkTests/DXExceptPublisherTests.xctestplan b/DXFeedFrameworkTests/DXExceptPublisherTests.xctestplan index a47b61a5f..88cbce3a8 100644 --- a/DXFeedFrameworkTests/DXExceptPublisherTests.xctestplan +++ b/DXFeedFrameworkTests/DXExceptPublisherTests.xctestplan @@ -24,6 +24,7 @@ "DXConnectionStateTests", "DXConnectionTest", "DXExceptionTest", + "DXMarketDepthTest", "DXObservableSubscriptionTest", "DXSnapshotProcessorTest", "EndpointTest\/testGetInstance()", diff --git a/DXFeedFrameworkTests/DXMarketDepthTest.swift b/DXFeedFrameworkTests/DXMarketDepthTest.swift new file mode 100644 index 000000000..de6bc094f --- /dev/null +++ b/DXFeedFrameworkTests/DXMarketDepthTest.swift @@ -0,0 +1,451 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import XCTest +@testable import DXFeedFramework + +// swiftlint:disable type_body_length +// swiftlint:disable function_body_length +final class DXMarketDepthTest: XCTestCase, MarketDepthListener { + + func modelChanged(changes: DXFeedFramework.OrderBook) { + if orderBook.buyOrders != changes.buyOrders { + changesBuy += 1 + } + if orderBook.sellOrders != changes.sellOrders { + changesSell += 1 + } + + orderBook = changes + expectation1?.fulfill() + } + + let symbol = "INDEX-TEST" + let source = OrderSource.defaultOrderSource! + let bookSize = 100 + + var endpoint: DXEndpoint! + var feed: DXFeed! + var publisher: DXPublisher! + var expectation1: XCTestExpectation? + var orderBook = OrderBook() + var model: MarketDepthModel! + var changesBuy = 0 + var changesSell = 0 + + override func setUp() async throws { + endpoint = try DXEndpoint.create(.localHub) + feed = endpoint.getFeed() + publisher = endpoint.getPublisher() + model = try MarketDepthModel(symbol: symbol, + sources: [source], + aggregationPeriodMillis: 0, + mode: .multiple, + feed: feed, + listener: self) + + } + + func testRemoveBySizeAndByFlags() throws { + let order1 = try createOrder(index: 2, side: .buy, price: 3, size: 1, eventFlags: 0) + let order2 = try createOrder(index: 1, side: .buy, price: 2, size: 1, eventFlags: 0) + let order3 = try createOrder(index: 0, side: .buy, price: 1, size: 1, eventFlags: 0) + try publisher.publish(events: [order1, order2, order3]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 3) + XCTAssertEqual(orderBook.sellOrders.count, 0) + + XCTAssert(same(order1: order1, order2: orderBook.buyOrders[0])) + XCTAssert(same(order1: order2, order2: orderBook.buyOrders[1])) + XCTAssert(same(order1: order3, order2: orderBook.buyOrders[2])) + + try publisher.publish(events: [try createOrder(index: 2, + side: .buy, + price: 2, + size: Double.nan, eventFlags: 0)]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 2) + XCTAssertEqual(orderBook.sellOrders.count, 0) + XCTAssert(same(order1: order2, order2: orderBook.buyOrders[0])) + + try publisher.publish(events: [try createOrder(index: 1, + side: .buy, + price: 2, + size: Double.nan, eventFlags: Order.removeEvent)]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 1) + XCTAssertEqual(orderBook.sellOrders.count, 0) + XCTAssert(same(order1: order3, order2: orderBook.buyOrders[0])) + + try publisher.publish(events: [try createOrder(index: 0, + side: .buy, + price: 1, + size: 1, + eventFlags: Order.removeEvent | Order.snapshotEnd)]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 0) + XCTAssertEqual(orderBook.sellOrders.count, 0) + + XCTAssertEqual(model.buyOrders.toList().count, 0) + } + + func testOrderChangeSide() throws { + let order1 = try createOrder(index: 0, side: .buy, price: 1, size: 1, eventFlags: 0) + try publisher.publish(events: [order1]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 1) + XCTAssertEqual(orderBook.sellOrders.count, 0) + XCTAssert(same(order1: order1, order2: orderBook.buyOrders[0])) + + let order2 = try createOrder(index: 0, side: .sell, price: 1, size: 1, eventFlags: 0) + try publisher.publish(events: [order2]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 0) + XCTAssertEqual(orderBook.sellOrders.count, 1) + XCTAssert(same(order1: order2, order2: orderBook.sellOrders[0])) + } + + func testOrderPriorityAfterUpdate() throws { + + let bOrder1 = try createOrder(index: 0, side: .buy, price: 100, size: 1, eventFlags: 0) + let bOrder2 = try createOrder(index: 1, side: .buy, price: 150, size: 1, eventFlags: 0) + + let sOrder1 = try createOrder(index: 3, side: .sell, price: 150, size: 1, eventFlags: 0) + let sOrder2 = try createOrder(index: 2, side: .sell, price: 100, size: 1, eventFlags: 0) + + try publisher.publish(events: [bOrder1]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssert(same(order1: bOrder1, order2: orderBook.buyOrders[0])) + + try publisher.publish(events: [bOrder2]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssert(same(order1: bOrder2, order2: orderBook.buyOrders[0])) + XCTAssert(same(order1: bOrder1, order2: orderBook.buyOrders[1])) + + try publisher.publish(events: [sOrder1]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssert(same(order1: sOrder1, order2: orderBook.sellOrders[0])) + + try publisher.publish(events: [sOrder2]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssert(same(order1: sOrder2, order2: orderBook.sellOrders[0])) + XCTAssert(same(order1: sOrder1, order2: orderBook.sellOrders[1])) + } + + func testMultipleUpdatesWithMixedSides() throws { + let buyLowPrice = try createOrder(index: 0, side: .buy, price: 100, size: 1, eventFlags: 0) + let buyHighPrice = try createOrder(index: 1, side: .buy, price: 200, size: 1, eventFlags: 0) + let sellLowPrice = try createOrder(index: 2, side: .sell, price: 150, size: 1, eventFlags: 0) + let sellHighPrice = try createOrder(index: 3, side: .sell, price: 250, size: 1, eventFlags: 0) + + try publisher.publish(events: [buyLowPrice, sellHighPrice, buyHighPrice, sellLowPrice]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + + XCTAssert(same(order1: buyHighPrice, order2: orderBook.buyOrders[0])) + XCTAssert(same(order1: sellLowPrice, order2: orderBook.sellOrders[0])) + } + + func testDuplicateOrderIndexUpdatesExistingOrder() throws { + let originalIndexOrder = try createOrder(index: 0, side: .buy, price: 100, size: 1, eventFlags: 0) + let duplicateIndexOrder = try createOrder(index: 0, side: .buy, price: 150, size: 1, eventFlags: 0) + + try publisher.publish(events: [originalIndexOrder, duplicateIndexOrder]) + expectation1 = expectation(description: "Events received") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 1) + XCTAssert(same(order1: duplicateIndexOrder, order2: orderBook.buyOrders[0])) + } + + func testEnforceEntryLimit() throws { + model.setDepthLimit(3) + + try publisher.publish(events: [try createOrder(index: 0, side: .buy, price: 5, size: 1, eventFlags: 0), + try createOrder(index: 1, side: .buy, price: 4, size: 1, eventFlags: 0), + try createOrder(index: 2, side: .buy, price: 3, size: 1, eventFlags: 0)]) + + expectation1 = expectation(description: "Events received0") + expectation1?.assertForOverFulfill = false + wait(for: [expectation1!], timeout: 1.0) + + try publisher.publish(events: [try createOrder(index: 3, + side: .buy, + price: 2, + size: 1, + eventFlags: 0)]) // outside limit + expectation1 = expectation(description: "Events received1") + expectation1?.isInverted = true + wait(for: [expectation1!], timeout: 0.1) + + try publisher.publish(events: [try createOrder(index: 4, + side: .buy, + price: 1, + size: 1, + eventFlags: 0)]) // outside limit + expectation1 = expectation(description: "Events received2") + expectation1?.isInverted = true + wait(for: [expectation1!], timeout: 0.1) + + try publisher.publish(events: [try createOrder(index: 4, + side: .buy, + price: 1, + size: 2, + eventFlags: 0)]) // modify outside limit + expectation1 = expectation(description: "Events received3") + expectation1?.isInverted = true + wait(for: [expectation1!], timeout: 0.1) + + try publisher.publish(events: [try createOrder(index: 3, + side: .buy, + price: 2, + size: .nan, + eventFlags: 0)]) // remove outside limit + expectation1 = expectation(description: "Events received4") + expectation1?.isInverted = true + wait(for: [expectation1!], timeout: 0.1) + + try publisher.publish(events: [try createOrder(index: 2, + side: .buy, + price: 3, + size: 2, + eventFlags: 0)]) // update in limit + expectation1 = expectation(description: "Events received5") + expectation1?.assertForOverFulfill = false + wait(for: [expectation1!], timeout: 1.0) + + try publisher.publish(events: [try createOrder(index: 1, + side: .buy, + price: 3, + size: .nan, + eventFlags: 0)]) // remove in limit + expectation1 = expectation(description: "Events received6") + wait(for: [expectation1!], timeout: 1.0) + + model.setDepthLimit(0) + expectation1 = nil + + try publisher.publish(events: [try createOrder(index: 4, + side: .buy, + price: 1, + size: 3, + eventFlags: 0)]) + expectation1 = expectation(description: "Events received7") + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 3) + + try publisher.publish(events: [try createOrder(index: 8, + side: .sell, + price: 1, + size: 3, + eventFlags: 0)]) + expectation1 = expectation(description: "Events received8") + expectation1?.assertForOverFulfill = false + wait(for: [expectation1!], timeout: 1.0) + XCTAssertEqual(orderBook.buyOrders.count, 3) + XCTAssertEqual(orderBook.sellOrders.count, 1) + + model.setDepthLimit(1) + expectation1 = nil + + try publisher.publish(events: [try createOrder(index: 0, side: .buy, price: 2, size: 1, eventFlags: 0), + try createOrder(index: 1, side: .buy, price: 2, size: 1, eventFlags: 0)]) + expectation1 = expectation(description: "Events received9") + expectation1?.assertForOverFulfill = false + wait(for: [expectation1!], timeout: 1.0) + } + + func testStressBuySellOrders() throws { + var book = [Order?](repeatElement(nil, + count: bookSize)) + var expectedBuy = 0 + var expectedSell = 0 + for _ in 0..<10000 { + let index = Int.random(in: 0.. 0 ? 1 : 0, changesBuy) + XCTAssertEqual(expectedSell > 0 ? 1 : 0, changesSell) + } + + func testStressSources() throws { + let sources = Array(try OrderSource.publishable(eventType: Order.self).prefix(12)) + model = try MarketDepthModel(symbol: symbol, + sources: sources, + aggregationPeriodMillis: 0, + mode: .multiple, + feed: feed, + listener: self) + + var expectedBuy = 0 + var expectedSell = 0 + + var books = [Int: [Order?]]() + sources.forEach { source in + books[source.identifier] = [Order?](repeatElement(nil, + count: bookSize)) + } + for _ in 0..<10000 { + let index = Int.random(in: 0.. 0 ? sources.count : 0, changesBuy) + XCTAssertEqual(expectedSell > 0 ? sources.count : 0, changesSell) + + } + + func oneIfBuy(_ order: Order?) -> Int { + guard let order = order else { + return 0 + } + return (order.orderSide == .buy && order.size != 0) ? 1 : 0 + } + + func oneIfSell(_ order: Order?) -> Int { + guard let order = order else { + return 0 + } + return (order.orderSide == .sell && order.size != 0) ? 1 : 0 + } + + func same(order1: Order, order2: Order?) -> Bool { + guard let order2 = order2 else { + return false + } + return order1.index == order2.index && + order1.orderSide == order2.orderSide && + order1.price == order2.price && + order1.size == order2.size && + order1.eventFlags == order2.eventFlags + } + + func createOrder(index: Int64, + side: Side, + price: Double, + size: Double, + eventFlags: Int32) throws -> Order { + let order1 = Order(symbol) + try order1.setIndex(index) + order1.orderSide = side + order1.price = price + order1.size = size + order1.eventFlags = eventFlags + return order1 + } +} + +extension DXMarketDepthTest { + @objc override func value(forKey key: String) -> Any? { + switch key { + case "buyOrdersSize": + return orderBook.buyOrders.count + case "sellOrdersSize": + return orderBook.sellOrders.count + default: + fatalError("\(self) doesn't support \(key)") + } + } +} +// swiftlint:enable type_body_length +// swiftlint:enable function_body_length diff --git a/DXFeedFrameworkTests/DXOnDemandServiceTest.swift b/DXFeedFrameworkTests/DXOnDemandServiceTest.swift index ffa9edf09..c15533f4e 100644 --- a/DXFeedFrameworkTests/DXOnDemandServiceTest.swift +++ b/DXFeedFrameworkTests/DXOnDemandServiceTest.swift @@ -6,7 +6,7 @@ // import XCTest -import DXFeedFramework +@testable import DXFeedFramework final class DXOnDemandServiceTest: XCTestCase { func testCreateService() throws { diff --git a/DXFeedFrameworkTests/DXSnapshotProcessorTest.swift b/DXFeedFrameworkTests/DXSnapshotProcessorTest.swift index dd75a6a9b..c7b16a8ee 100644 --- a/DXFeedFrameworkTests/DXSnapshotProcessorTest.swift +++ b/DXFeedFrameworkTests/DXSnapshotProcessorTest.swift @@ -38,7 +38,8 @@ final class DXSnapshotProcessorTest: XCTestCase, SnapshotDelegate { let snapshotProcessor = SnapshotProcessor() snapshotProcessor.add(self) try subscription?.add(listener: snapshotProcessor) - let symbol = TimeSeriesSubscriptionSymbol(symbol: "AAPL{=1d}", date: Date.init(millisecondsSince1970: 0)) + let date = Calendar.current.date(byAdding: .year, value: -1, to: Date())! + let symbol = TimeSeriesSubscriptionSymbol(symbol: "AAPL{=1d}", date: date) try subscription?.addSymbols(symbol) wait(for: [receivedEventExp], timeout: 4.0) } diff --git a/DXFeedFrameworkTests/EndpointTest.swift b/DXFeedFrameworkTests/EndpointTest.swift index 8d715ab6a..a8951b5b6 100644 --- a/DXFeedFrameworkTests/EndpointTest.swift +++ b/DXFeedFrameworkTests/EndpointTest.swift @@ -101,4 +101,15 @@ final class EndpointTest: XCTestCase { endpoint.add(listener: stateListener!) wait(for: [connectedExpectation], timeout: 1) } + + func testRoleConvert() throws { + let roles: [DXEndpoint.Role] = [.feed, .onDemandFeed, .streamFeed, .publisher, .streamPublisher, .localHub] + let nativeCodes = roles.map { role in + role.toNatie() + } + XCTAssertEqual(nativeCodes.map { nativeRole in + DXEndpoint.Role.fromNative(nativeRole) + }, roles) + } + } diff --git a/DXFeedFrameworkTests/IsolateTest.swift b/DXFeedFrameworkTests/IsolateTest.swift index 454fe56ce..259e72856 100644 --- a/DXFeedFrameworkTests/IsolateTest.swift +++ b/DXFeedFrameworkTests/IsolateTest.swift @@ -8,15 +8,6 @@ import XCTest @testable import DXFeedFramework final class IsolateTest: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - func testCleanup() throws { // just use it to avoid warnings try XCTSkipIf(true, "Just for manual running") diff --git a/DXFeedFrameworkTests/PublisherTest.swift b/DXFeedFrameworkTests/PublisherTest.swift index 420898f34..b42b2c044 100644 --- a/DXFeedFrameworkTests/PublisherTest.swift +++ b/DXFeedFrameworkTests/PublisherTest.swift @@ -7,6 +7,7 @@ import XCTest @testable import DXFeedFramework +// swiftlint:disable function_body_length final class PublisherTest: XCTestCase { override func setUpWithError() throws { @@ -31,10 +32,14 @@ final class PublisherTest: XCTestCase { .build() try endpoint?.connect(":7400") - let testQuote = Quote("AAPL") - testQuote.bidSize = 100 - testQuote.askPrice = 666 - try? testQuote.setSequence(10) + let order = Order("AAPL") + order.index = 0 + + order.orderSide = .buy + order.eventSource = OrderSource.ntv! + order.eventFlags = 0 + order.size = 100 + order.price = 666 let feedEndpoint = try DXEndpoint .builder() .withRole(.feed) @@ -49,7 +54,7 @@ final class PublisherTest: XCTestCase { DispatchQueue.global(qos: .background).asyncAfter(deadline: .now() + 0.3) { print("\(pthread_mach_thread_np(pthread_self()))") print(Thread.current.threadName) - try? publisher?.publish(events: [testQuote]) + try? publisher?.publish(events: [order]) } } } @@ -57,20 +62,25 @@ final class PublisherTest: XCTestCase { } feedEndpoint.add(listener: stateListener!) - let subscription = try feedEndpoint.getFeed()?.createSubscription(Quote.self) + let subscription = try feedEndpoint.getFeed()?.createSubscription(Order.self) try feedEndpoint.connect("localhost:7400") let receivedEventExp = expectation(description: "Received events \(EventCode.quote)") receivedEventExp.assertForOverFulfill = false let listener = AnonymousClass { anonymCl in - anonymCl.callback = { _ in + anonymCl.callback = { events in + events.forEach { event in + print(event.toString()) + } receivedEventExp.fulfill() } return anonymCl } try subscription?.add(listener: listener) - try subscription?.addSymbols(["AAPL"]) + let symbol = IndexedEventSubscriptionSymbol(symbol: "AAPL", source: OrderSource.ntv!) + + try subscription?.addSymbols([symbol]) wait(for: [connectedExpectation], timeout: 1) wait(for: [receivedEventExp], timeout: 20) } catch { @@ -91,3 +101,4 @@ final class PublisherTest: XCTestCase { wait(seconds: 2) } } +// swiftlint:enable function_body_length diff --git a/DXFeedFrameworkTests/XCTestCase+Utils.swift b/DXFeedFrameworkTests/XCTestCase+Utils.swift index 196607a44..661e6f6e9 100644 --- a/DXFeedFrameworkTests/XCTestCase+Utils.swift +++ b/DXFeedFrameworkTests/XCTestCase+Utils.swift @@ -11,4 +11,10 @@ extension XCTestCase { _ = XCTWaiter.wait(for: [expectation(description: "\(seconds) seconds waiting")], timeout: TimeInterval(seconds)) } + + func wait(millis: Float) { + let seconds = millis / 1000 + _ = XCTWaiter.wait(for: [expectation(description: "\(millis) millis waiting")], + timeout: TimeInterval(seconds)) + } } diff --git a/Samples/DXFeedCandleChart/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/DXFeedCandleChart/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Samples/DXFeedCandleChart/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..b5b77380c --- /dev/null +++ b/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "dxfeed_black-sym.svg.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png b/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png new file mode 100644 index 000000000..08b2b8448 Binary files /dev/null and b/Samples/DXFeedCandleChart/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png differ diff --git a/Samples/DXFeedCandleChart/Assets.xcassets/Contents.json b/Samples/DXFeedCandleChart/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Samples/DXFeedCandleChart/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChart/DXFeedCandleChartApp.swift b/Samples/DXFeedCandleChart/DXFeedCandleChartApp.swift new file mode 100644 index 000000000..d86ce6a33 --- /dev/null +++ b/Samples/DXFeedCandleChart/DXFeedCandleChartApp.swift @@ -0,0 +1,20 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import SwiftUI + +@main +struct DXFeedCandleChartApp: App { + var body: some Scene { + WindowGroup { + CandleChart(symbol: "AAPL", + type: .minute, + endpoint: nil, + ipfAddress: "https://demo:demo@tools.dxfeed.com/ipf?SYMBOL=") + } + } +} diff --git a/Samples/DXFeedCandleChart/Preview Content/Preview Assets.xcassets/Contents.json b/Samples/DXFeedCandleChart/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Samples/DXFeedCandleChart/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChartMac/Array+Ext.swift b/Samples/DXFeedCandleChartMac/Array+Ext.swift new file mode 100644 index 000000000..522455bd7 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Array+Ext.swift @@ -0,0 +1,18 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation + +extension Array { + mutating func safeReplace(_ newElement: Element, at index: Int) { + if index >= 0 && index < self.count { + self[index] = newElement + } else { + print("error during replace") + } + } +} diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/DXFeedCandleChartMac/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/1024-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/1024-mac.png new file mode 100644 index 000000000..593966c55 Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/1024-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/128-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/128-mac.png new file mode 100644 index 000000000..6ac8b09b5 Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/128-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/16-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/16-mac.png new file mode 100644 index 000000000..9db1b1023 Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/16-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/256-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/256-mac.png new file mode 100644 index 000000000..52e0a034d Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/256-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/32-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/32-mac.png new file mode 100644 index 000000000..74e428ec1 Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/32-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/512-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/512-mac.png new file mode 100644 index 000000000..0a4ea5960 Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/512-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/64-mac.png b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/64-mac.png new file mode 100644 index 000000000..2346fe28d Binary files /dev/null and b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/64-mac.png differ diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..eba1335b9 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1 @@ +{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/Samples/DXFeedCandleChartMac/Assets.xcassets/Contents.json b/Samples/DXFeedCandleChartMac/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedCandleChartMac/CandleChart.swift b/Samples/DXFeedCandleChartMac/CandleChart.swift new file mode 100644 index 000000000..97bb891c1 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/CandleChart.swift @@ -0,0 +1,387 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import SwiftUI +import Charts +import DXFeedFramework + +extension View { + func execute(_ apply: (AnyView) -> (AnyView)) -> AnyView { + return apply(AnyView(self)) + } +} + +enum CandlePickerType: CaseIterable, Identifiable { + case minute, hour, day, week, month, year + var id: Self { self } +} + +struct CandleModel: Identifiable { + let currency: String + let timestamp: Date + var id: Date { timestamp } + + let open: Decimal + let close: Decimal + let high: Decimal + let low: Decimal + + let isPoint: Bool + let stringtimeStamp: String + let index: Long + + init(currency: String, timestamp: Date, open: Decimal, close: Decimal, high: Decimal, low: Decimal, index: Long) { + self.currency = currency + self.timestamp = timestamp + self.open = open + self.close = close + self.isPoint = open == close && high == low + self.high = high + self.low = low + self.stringtimeStamp = "\(timestamp.timeIntervalSince1970)" + self.index = index + } + + init(candle: Candle, currency: String) { + self.init(currency: currency, + timestamp: Date(millisecondsSince1970: candle.time), + open: Decimal(candle.open), + close: Decimal(candle.close), + high: Decimal(candle.high), + low: Decimal(candle.low), + index: candle.index) + } + + var isClosingHigher: Bool { + self.open <= self.close + } + +} + +struct CandleChart: View { + @ObservedObject var list: CandleChartModel + @State private var selectedPrice: CandleModel? + @State private var type: CandlePickerType = .week + + let dateFormatter: DateFormatter + let shortDateFormatter: DateFormatter + let hourDateFormatter: DateFormatter + + init(symbol: String, + type: CandlePickerType = .week, + endpoint: DXEndpoint?, + ipfAddress: String) { + + dateFormatter = DateFormatter() + dateFormatter.dateFormat = "dd.MM.yy" + + shortDateFormatter = DateFormatter() + shortDateFormatter.dateFormat = "MM.yyyy" + hourDateFormatter = DateFormatter() + hourDateFormatter.dateStyle = .none + hourDateFormatter.timeStyle = .short + + self.list = CandleChartModel(symbol: symbol, + endpoint: endpoint, + ipfAddress: ipfAddress) + _type = State(initialValue: type) + } + + var body: some View { + GeometryReader { reader in + List { + Section { + VStack(alignment: .leading) { + chart.onAppear { + // just workaround for swiftuicharts + scroll to + self.list.fakeLoading() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.selectedPrice = nil + self.list.updateDate(type: self.type) + } + } + Text("NOTICE: A maximum of \(CandleChartModel.maxCout) candles is displayed.") + .font(Font.system(size: 10)) + }.frame(height: max(reader.size.height - 150, 300)) + } + .listRowBackground(Color.sectionBackground) + Section("Chart parameters") { + Picker("Candle type", selection: $type) { + ForEach(CandlePickerType.allCases, id: \.self) { category in + Text(String(describing: category).capitalized).tag(category) + } + }.onChange(of: type) { value in + selectedPrice = nil + list.updateDate(type: value) + } + .foregroundStyle(Color.labelText) + } + .listRowSeparator(.hidden) + .listRowBackground(Color.sectionBackground) + } + // fix for datepicker selected color + .preferredColorScheme(.dark) + .background(Color.viewBackground) + .scrollContentBackground(.hidden) + } + } + + private var chart: some View { + Chart($list.candles) { binding in + let price = binding.wrappedValue + + CandlePlot( + timestamp: .value("Date", price.stringtimeStamp), + open: .value("Open", price.open), + high: .value("High", price.high), + low: .value("Low", price.low), + close: .value("Close", price.close), + isPoint: price.isPoint + ) + .foregroundStyle( price.isClosingHigher ? .green : .red) + } + .chartXScale(domain: .automatic(dataType: String.self) { dates in + dates = list.xValues + }) + .chartYScale(domain: list.yScale()) + .chartYAxis { AxisMarks(preset: .extended) } + .chartXAxis { + if !list.loadingInProgress { + let xAxisValues = self.list.xAxisLabels + AxisMarks(preset: .aligned, values: xAxisValues) { value in + if let strDate = value.as(String.self) { + let date = Date(timeIntervalSince1970: TimeInterval(strDate) ?? 0) + AxisValueLabel(horizontalSpacing: -14, verticalSpacing: 10) { + switch type { + case .year: + Text(shortDateFormatter.string(from: date)) + case .minute: + VStack { + Text(dateFormatter.string(from: date)) + Text(hourDateFormatter.string(from: date)) + } + case .hour: + VStack { + Text(dateFormatter.string(from: date)) + Text(hourDateFormatter.string(from: date)) + } + default: + Text(dateFormatter.string(from: date)) + } + } + } + AxisGridLine(centered: true, stroke: StrokeStyle(lineWidth: 0.5)) + AxisTick(centered: true, length: 0, stroke: .none) + } + } + } + .chartOverlay { proxy in + GeometryReader { geo in + Rectangle().fill(.clear).contentShape(Rectangle()) + .gesture( + SpatialTapGesture() + .onEnded { value in + let element = findElement(location: value.location, proxy: proxy, geometry: geo) + if selectedPrice?.timestamp == element?.timestamp { + // If tapping the same element, clean the selection. + selectedPrice = nil + } else { + selectedPrice = element + } + } + .exclusively( + before: DragGesture() + .onChanged { value in + selectedPrice = findElement(location: value.location, + proxy: proxy, + geometry: geo) + } + ) + ) + } + } + .chartOverlay { proxy in + ZStack(alignment: .topLeading) { + GeometryReader { geo in + if let selectedPrice { + let dateInterval = Calendar.current.dateInterval(of: .minute, for: selectedPrice.timestamp)! + let startPositionX1 = proxy.position(forX: "\(dateInterval.start.timeIntervalSince1970)") ?? 0 + + let rect = plotFrameRect(proxy: proxy, in: geo) + let lineX = startPositionX1 + rect.origin.x + let lineHeight = rect.maxY + let boxWidth: CGFloat = min(geo.size.width, 400) + let boxOffset = max(0, min(geo.size.width - boxWidth, lineX - boxWidth / 2)) + + Rectangle() + .fill(.gray.opacity(0.5)) + .frame(width: 2, height: lineHeight) + .position(x: lineX, y: lineHeight / 2) + + CandleInfoView(for: selectedPrice, currency: list.currency) + .frame(width: boxWidth, alignment: .leading) + .background { + RoundedRectangle(cornerRadius: 13) + .fill(Color.infoBackground) + .foregroundStyle(.thickMaterial) + .padding(.horizontal, -8) + .padding(.vertical, -4) + + } + .offset(x: boxOffset) + .gesture( + TapGesture() + .onEnded { _ in + self.selectedPrice = nil + } + ) + } + } + } + } + .execute { view in +#if os(iOS) + if #available(iOS 17.0, *) { + return AnyView(view + .chartScrollableAxes(.horizontal) + .chartXVisibleDomain(length: list.visibleDomains()) + .chartScrollPosition(x: $list.xScrollPosition) + .onChange(of: list.xScrollPosition) { + selectedPrice = nil + } + ) + } +#endif + return view + } + } + + private func plotFrameRect(proxy: ChartProxy, in geo: GeometryProxy) -> CGRect { + var rect = CGRect.zero + if #available(iOS 17, *) { + if let plotFrame = proxy.plotFrame { + rect = geo[plotFrame] + } + } else { + rect = geo[proxy.plotAreaFrame] + } + return rect + } + + private func findElement(location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) -> CandleModel? { + let rect = plotFrameRect(proxy: proxy, in: geometry) + let relativeXPosition = location.x - rect.origin.x + if let date = proxy.value(atX: relativeXPosition) as String?, let timeInterval = TimeInterval(date) { + // Find the closest date element. + let date = Date(timeIntervalSince1970: timeInterval) + var minDistance: TimeInterval = .infinity + var index: Int? + for dataIndex in list.candles.indices { + let nthSalesDataDistance = list.candles[dataIndex].timestamp.distance(to: date) + if abs(nthSalesDataDistance) < minDistance { + minDistance = abs(nthSalesDataDistance) + index = dataIndex + } + } + if let index { + return list.candles[index] + } + } + return nil + } +} + +struct CandlePlot: ChartContent { + let timestamp: PlottableValue + let open: PlottableValue + let high: PlottableValue + let low: PlottableValue + let close: PlottableValue + let isPoint: Bool + + var body: some ChartContent { + Plot { + if isPoint { + PointMark(x: timestamp, + y: open) + .symbolSize(CandlePlot.openCloseWidth) + } else { + BarMark( + x: timestamp, + yStart: open, + yEnd: close, + width: MarkDimension(floatLiteral: CandlePlot.openCloseWidth) + ) + BarMark( + x: timestamp, + yStart: high, + yEnd: low, + width: MarkDimension(floatLiteral: CandlePlot.highLowWidth) + ) + } + } + } + + static let openCloseWidth: CGFloat = { + // iOS16 doesn't support scroll on chart and content will show on the same screen + if #available(iOS 17, *) { + return 6 + } else { + return 4 + } + }() + + static let highLowWidth: CGFloat = { + // iOS16 doesn't support scroll on chart and content will show on the same screen + if #available(iOS 17, *) { + return 2 + } else { + return 1 + } + }() +} + +// MARK: - Info View with Prices +struct CandleInfoView: View { + let price: CandleModel + let currency: String + + init(for price: CandleModel, currency: String) { + self.price = price + self.currency = currency + } + + var body: some View { + VStack(alignment: .center, spacing: 4) { + Text(price.timestamp.formatted(date: .numeric, time: .shortened)) + HStack(spacing: 10) { + Text("Open: \(price.open.formatted(.currency(code: currency)))") + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .minimumScaleFactor(0.01) + Text("Close: \(price.close.formatted(.currency(code: currency)))") + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .minimumScaleFactor(0.01) + } + HStack(spacing: 10) { + Text("High: \(price.high.formatted(.currency(code: currency)))") + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .minimumScaleFactor(0.01) + Text("Low: \(price.low.formatted(.currency(code: currency)))") + .foregroundColor(.secondary) + .frame(minWidth: 0, maxWidth: .infinity, alignment: .leading) + .minimumScaleFactor(0.01) + } + } + .lineLimit(1) + .font(.headline) + .padding(.vertical) + } + +} diff --git a/Samples/DXFeedCandleChartMac/CandleChartModel.swift b/Samples/DXFeedCandleChartMac/CandleChartModel.swift new file mode 100644 index 000000000..1276930ca --- /dev/null +++ b/Samples/DXFeedCandleChartMac/CandleChartModel.swift @@ -0,0 +1,229 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +import DXFeedFramework +import SwiftUI + +extension Candle { + func max() -> Double { + return Swift.max(Swift.max(self.open, self.close), + Swift.max(self.high, self.low)) + } + + func min() -> Double { + return Swift.min(Swift.min(self.open, self.close), + Swift.min(self.high, self.low)) + } +} + +class CandleChartModel: ObservableObject { + static let maxCout = 150 + let symbol: String + public private(set) var currency = "" + public private(set) var descriptionString = "" + + var endpoint: DXEndpoint! + var feed: DXFeed! + var subscription: DXFeedSubscription? + var snapshotProcessor: SnapshotProcessor! + + @Published var candles: [CandleModel] + @Published var xScrollPosition: String = "" + @Published var xAxisLabels = [String]() + var xValues = [String]() + + private var maxValue: Double = 0 + private var minValue: Double = Double.greatestFiniteMagnitude + var type = CandlePickerType.year + + var loadingInProgress = false + init(symbol: String, + endpoint: DXEndpoint?, + ipfAddress: String) { + self.symbol = symbol + self.candles = [CandleModel]() + try? createSubscription(endpoint: endpoint) + fetchInfo(ipfAddress: ipfAddress) + } + + func createSubscription(endpoint: DXEndpoint?) throws { + if let endpoint = endpoint { + self.endpoint = endpoint + } else { + self.endpoint = try DXEndpoint.create().connect("demo.dxfeed.com:7300") + } + feed = self.endpoint.getFeed() + subscription = try feed?.createSubscription([Candle.self]) + snapshotProcessor = SnapshotProcessor() + snapshotProcessor.add(self) + try subscription?.add(listener: snapshotProcessor) + } + + func fetchInfo(ipfAddress: String) { + DispatchQueue.global(qos: .background).async { + let reader = DXInstrumentProfileReader() + let address = ipfAddress + self.symbol + let result = try? reader.readFromFile(address: address) + guard let result = result else { + return + } + result.forEach { profile in + self.currency = profile.currency + self.descriptionString = profile.descriptionStr + } + } + } + + func updateDate(type: CandlePickerType) { + print("start load \(Date()) \(type)") + loadingInProgress = true + self.type = type + let date = type.calcualteStartDate() + candles = [CandleModel]() + let candleSymbol = CandleSymbol.valueOf(symbol, type.toDxFeedValue()) + let symbol = TimeSeriesSubscriptionSymbol(symbol: candleSymbol, date: date) + try? subscription?.setSymbols([symbol]) + } + + func fakeLoading() { + loadingInProgress = true + self.xScrollPosition = "\(Calendar.current.date(byAdding: .day, value: -1, to: Date())!.timeIntervalSince1970)" + loadingInProgress = false + } + + private static func visiblePointsOnScreen(type: CandlePickerType, valuesCount: Int) -> Int { + switch type { + case .month: + return min(valuesCount, 30) + case .week: + return min(valuesCount, 30) + case .year: + return min(valuesCount, 30) + case .minute: + return min(valuesCount, 30) + case .day: + return min(valuesCount, 30) + case .hour: + return min(valuesCount, 30) + } + } + + func visibleDomains() -> Int { + let valuesCount = self.candles.count + if valuesCount == 0 { + return 1 + } + let pointsOnScreen = CandleChartModel.visiblePointsOnScreen(type: type, valuesCount: valuesCount) + return pointsOnScreen + } + + func yScale() -> ClosedRange { + if candles.count == 0 { + return 0...0 + } + if type == .minute { + return minValue...maxValue + } else { + return (minValue*0.95)...maxValue*1.05 + } + } + + private static func calculateXaxisValues(with type: CandlePickerType, values: [CandleModel]) -> [String] { + var visiblePages: Double = 1 + let valuesCount = values.count + if #available(iOS 17.0, *) { + let pointsOnScreen = CandleChartModel.visiblePointsOnScreen(type: type, valuesCount: valuesCount) + visiblePages = Double(valuesCount)/Double(pointsOnScreen) + } + // initial case, to avoid showing lines on empty screen + if values.count == 0 { + return [String]() + } + + let maxInterval = Int(visiblePages.isNaN ? 1 : visiblePages) * 4 + let stringValues = stride(from: 0, to: valuesCount, by: valuesCount / maxInterval).map { position in + values[position].stringtimeStamp + } + return stringValues + } +} + +extension CandleChartModel: SnapshotDelegate { + func receiveEvents(_ events: [DXFeedFramework.MarketEvent], isSnapshot: Bool) { + let result = events.map { marketEvent in + marketEvent.candle + } + DispatchQueue.main.async { [self] in + if isSnapshot { + receivedSnapshot(events: result) + } else { + receivedUpdate(events: result) + } + } + } + + private func receivedSnapshot(events: [Candle]) { + self.loadingInProgress = false + var maxValue = Double.zero + var minValue = Double.greatestFiniteMagnitude + let firstNElements = events.prefix(CandleChartModel.maxCout) + let temp = firstNElements.map { candle in + maxValue = max(maxValue, candle.max()) + minValue = min(minValue, candle.min()) + let price = CandleModel(candle: candle, currency: self.currency) + return price + } + self.maxValue = maxValue + self.minValue = minValue + self.xAxisLabels = CandleChartModel.calculateXaxisValues(with: self.type, values: temp) + self.candles = temp + let xValues = Array(temp.map({ stock in + stock.stringtimeStamp + }).reversed()) + self.xValues = xValues + // scroll to last page + let pointsOnScreen = CandleChartModel.visiblePointsOnScreen(type: self.type, valuesCount: temp.count) + self.xScrollPosition = temp[pointsOnScreen - 1].stringtimeStamp + } + + private func receivedUpdate(events: [Candle]) { + events.forEach { candle in + let newPrice = CandleModel(candle: candle, currency: self.currency) + if candle.isRemove() { + // remove + self.candles.removeAll { price in + price.index == newPrice.index + } + } else { + self.maxValue = max(self.maxValue, candle.max()) + self.minValue = min(self.minValue, candle.min()) + + if let index = self.candles.firstIndex(where: { price in + price.timestamp == newPrice.timestamp + }) { + // update + self.candles.safeReplace(newPrice, at: index) + } else { + // insert + self.candles.insert(newPrice, at: 0) + let temp = self.candles + self.xAxisLabels = CandleChartModel.calculateXaxisValues(with: self.type, values: temp) + let currentScroll = self.xScrollPosition + + let xValues = Array(temp.enumerated().map({ index, stock in + if stock.stringtimeStamp == currentScroll { + self.xScrollPosition = temp[index - 1].stringtimeStamp + } + return stock.stringtimeStamp + }).reversed()) + self.xValues = xValues + } + } + } + } +} diff --git a/Samples/DXFeedCandleChartMac/CandlePickerType+Ext.swift b/Samples/DXFeedCandleChartMac/CandlePickerType+Ext.swift new file mode 100644 index 000000000..ae282f655 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/CandlePickerType+Ext.swift @@ -0,0 +1,45 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +import DXFeedFramework + +extension CandlePickerType { + func toDxFeedValue() -> CandlePeriod { + switch self { + case .week: + return CandlePeriod.valueOf(value: 1, type: .week) + case .month: + return CandlePeriod.valueOf(value: 1, type: .month) + case .year: + return CandlePeriod.valueOf(value: 1, type: .year) + case .minute: + return CandlePeriod.valueOf(value: 1, type: .minute) + case .day: + return CandlePeriod.valueOf(value: 1, type: .day) + case .hour: + return CandlePeriod.valueOf(value: 1, type: .hour) + } + } + + func calcualteStartDate() -> Date { + switch self { + case .minute: + return Calendar.current.date(byAdding: .day, value: -7, to: Date())! + case .hour: + return Calendar.current.date(byAdding: .day, value: -7, to: Date())! + case .day: + return Calendar.current.date(byAdding: .year, value: -1, to: Date())! + case .week: + return Calendar.current.date(byAdding: .year, value: -4, to: Date())! + case .month: + return Calendar.current.date(byAdding: .year, value: -10, to: Date())! + case .year: + return Date(timeIntervalSince1970: 0) + } + } +} diff --git a/Samples/DXFeedCandleChartMac/Color+Ext.swift b/Samples/DXFeedCandleChartMac/Color+Ext.swift new file mode 100644 index 000000000..177469f9a --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Color+Ext.swift @@ -0,0 +1,24 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import SwiftUI + +#if os(macOS) +extension Color { + static let viewBackground = Color.tableBackground + static let sectionBackground = Color.cellBackground + static let infoBackground = Color.priceBackground + static let labelText = Color.text +} +#elseif os(iOS) +extension Color { + static let viewBackground = Color(UIColor.tableBackground) + static let sectionBackground = Color(UIColor.cellBackground) + static let infoBackground = Color(UIColor.priceBackground) + static let labelText = Color(UIColor.text) +} +#endif diff --git a/Samples/DXFeedCandleChartMac/DXFeedCandleChartMac.entitlements b/Samples/DXFeedCandleChartMac/DXFeedCandleChartMac.entitlements new file mode 100644 index 000000000..40b639e46 --- /dev/null +++ b/Samples/DXFeedCandleChartMac/DXFeedCandleChartMac.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/Samples/DXFeedCandleChartMac/DXFeedCandleChartMacApp.swift b/Samples/DXFeedCandleChartMac/DXFeedCandleChartMacApp.swift new file mode 100644 index 000000000..5ff9ee40a --- /dev/null +++ b/Samples/DXFeedCandleChartMac/DXFeedCandleChartMacApp.swift @@ -0,0 +1,23 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import SwiftUI + +@main +struct DXFeedCandleChartMacApp: App { + let symbol = "AAPL" + var body: some Scene { + WindowGroup { + CandleChart(symbol: symbol, + type: .week, + endpoint: nil, + ipfAddress: "https://demo:demo@tools.dxfeed.com/ipf?SYMBOL=") + .navigationTitle("CandleChart: \(symbol)") + } + .defaultSize(width: 800, height: 800) + } +} diff --git a/Samples/DXFeedCandleChartMac/Preview Content/Preview Assets.xcassets/Contents.json b/Samples/DXFeedCandleChartMac/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Samples/DXFeedCandleChartMac/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedMarketDepth/AppDelegate.swift b/Samples/DXFeedMarketDepth/AppDelegate.swift new file mode 100644 index 000000000..4215eacb8 --- /dev/null +++ b/Samples/DXFeedMarketDepth/AppDelegate.swift @@ -0,0 +1,21 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, + didDiscardSceneSessions sceneSessions: Set) {} + +} diff --git a/Samples/DXFeedMarketDepth/Assets.xcassets/AccentColor.colorset/Contents.json b/Samples/DXFeedMarketDepth/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Samples/DXFeedMarketDepth/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/Contents.json b/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..b5b77380c --- /dev/null +++ b/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "dxfeed_black-sym.svg.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png b/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png new file mode 100644 index 000000000..08b2b8448 Binary files /dev/null and b/Samples/DXFeedMarketDepth/Assets.xcassets/AppIcon.appiconset/dxfeed_black-sym.svg.png differ diff --git a/Samples/DXFeedMarketDepth/Assets.xcassets/Contents.json b/Samples/DXFeedMarketDepth/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Samples/DXFeedMarketDepth/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Samples/DXFeedMarketDepth/Base.lproj/LaunchScreen.storyboard b/Samples/DXFeedMarketDepth/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..865e9329f --- /dev/null +++ b/Samples/DXFeedMarketDepth/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/DXFeedMarketDepth/Base.lproj/MainMarketDepth.storyboard b/Samples/DXFeedMarketDepth/Base.lproj/MainMarketDepth.storyboard new file mode 100644 index 000000000..92fa1fbca --- /dev/null +++ b/Samples/DXFeedMarketDepth/Base.lproj/MainMarketDepth.storyboard @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/DXFeedMarketDepth/Info.plist b/Samples/DXFeedMarketDepth/Info.plist new file mode 100644 index 000000000..9903fe0b0 --- /dev/null +++ b/Samples/DXFeedMarketDepth/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + MainMarketDepth + + + + + + diff --git a/Samples/DXFeedMarketDepth/MarketDepthViewController.swift b/Samples/DXFeedMarketDepth/MarketDepthViewController.swift new file mode 100644 index 000000000..c5e8b0d9d --- /dev/null +++ b/Samples/DXFeedMarketDepth/MarketDepthViewController.swift @@ -0,0 +1,164 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import UIKit +import DXFeedFramework + +class MarketDepthViewController: UIViewController { + enum SectionIndex: Int { + case buy = 1 + case sell = 0 + } + private static let defaultCellSize: CGFloat = 40 + private var cellSize: CGFloat = defaultCellSize + var numberOfRows = 0 + private var endpoint: DXEndpoint! + private var feed: DXFeed! + var symbol = "ETH/USD:GDAX" + + var model: MarketDepthModel? + var orderBook = OrderBook() + var maxValue: Double = 0 + + @IBOutlet var ordersTableView: UITableView! + @IBOutlet var headerStackView: UIStackView! + @IBOutlet var headerConstraint: NSLayoutConstraint! + + override func viewDidLoad() { + super.viewDidLoad() + + endpoint = try? DXEndpoint.create().connect("demo.dxfeed.com:7300") + feed = endpoint?.getFeed()! + + self.ordersTableView.backgroundColor = .clear +// self.ordersTableView.separatorStyle = .none + self.ordersTableView.register( UINib(nibName: "MarketDepthHeaderView", bundle: nil), + forHeaderFooterViewReuseIdentifier: "MarketDepthHeaderView") + if #available(iOS 15.0, *) { + self.ordersTableView.sectionHeaderTopPadding = 5 + } + self.view.backgroundColor = .tableBackground + } + + private func calculateCells() { + let viewSize = ordersTableView.frame.size.height + numberOfRows = Int((viewSize / MarketDepthViewController.defaultCellSize) / 2) + cellSize = viewSize / CGFloat(numberOfRows) / 2 + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + calculateCells() + model?.setDepthLimit(numberOfRows) + + self.ordersTableView.reloadData() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + calculateCells() + + do { + let sources = [OrderSource.agregateAsk!, OrderSource.agregateBid!] + + model = try MarketDepthModel(symbol: symbol, + sources: sources, + aggregationPeriodMillis: 0, + mode: .multiple, + feed: feed, + listener: self) + model?.setDepthLimit(numberOfRows) + + } catch { + print("MarketDepthModel: \(error)") + } + } +} + +extension MarketDepthViewController: MarketDepthListener { + func modelChanged(changes: DXFeedFramework.OrderBook) { + var maxValue: Double = 0 + changes.buyOrders.forEach { order in + maxValue = max(maxValue, order.size) + } + changes.sellOrders.forEach { order in + maxValue = max(maxValue, order.size) + } + + DispatchQueue.main.async { + self.maxValue = maxValue + self.orderBook = changes + self.headerStackView.isHidden = changes.buyOrders.count == 0 && changes.sellOrders.count == 0 + self.ordersTableView.reloadData() + } + } + +} + +extension MarketDepthViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return cellSize + } +} + +extension MarketDepthViewController: UITableViewDataSource { + func numberOfSections(in tableView: UITableView) -> Int { + return 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let sectionIndex = SectionIndex(rawValue: section) + switch sectionIndex { + case .buy: + return min(orderBook.buyOrders.count, numberOfRows) + case .sell: + return min(orderBook.sellOrders.count, numberOfRows) + default: + return 0 + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let sectionIndex = SectionIndex(rawValue: indexPath.section) + switch sectionIndex { + case .buy: + return buyCell(tableView, cellForRowAt: indexPath) + case .sell: + return sellCell(tableView, cellForRowAt: indexPath) + default: + return UITableViewCell() + } + } + + func buyCell(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell", for: indexPath) + as? OrderCell else { + return UITableViewCell() + } + let order = orderBook.buyOrders[indexPath.row] + cell.update(order: order, + maxSize: maxValue, + isBuy: true) + cell.selectionStyle = UITableViewCell.SelectionStyle.none + return cell + } + + func sellCell(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell", for: indexPath) + as? OrderCell else { + return UITableViewCell() + } + let order = orderBook.sellOrders[indexPath.row] + cell.update(order: order, + maxSize: maxValue, + isBuy: false) + cell.selectionStyle = UITableViewCell.SelectionStyle.none + return cell + } +} diff --git a/Samples/DXFeedMarketDepth/OrderCell.swift b/Samples/DXFeedMarketDepth/OrderCell.swift new file mode 100644 index 000000000..b91df4293 --- /dev/null +++ b/Samples/DXFeedMarketDepth/OrderCell.swift @@ -0,0 +1,97 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import Foundation +import UIKit +import DXFeedFramework + +extension NSLayoutConstraint { + func constraintWithMultiplier(_ multiplier: CGFloat) -> NSLayoutConstraint { + return NSLayoutConstraint(item: self.firstItem!, + attribute: self.firstAttribute, + relatedBy: self.relation, + toItem: self.secondItem, + attribute: self.secondAttribute, + multiplier: multiplier, + constant: self.constant) + } +} + +class OrderCell: UITableViewCell { + lazy var formatter: NumberFormatter = + { + let formatter = NumberFormatter() + formatter.minimumFractionDigits = 0 + formatter.maximumFractionDigits = 4 + return formatter + }() + + @IBOutlet var priceLabel: UILabel! + @IBOutlet var sizeBuyLabel: UILabel! + @IBOutlet var sizeSellLabel: UILabel! + + @IBOutlet var sizeBuyContentView: UIView! + @IBOutlet var sizeSellContentView: UIView! + + @IBOutlet var sizeBuyConstraint: NSLayoutConstraint! + @IBOutlet var sizeSellConstraint: NSLayoutConstraint! + + static let redBarColor = UIColor.red.withAlphaComponent(0.3) + static let greenBarColor = UIColor.green.withAlphaComponent(0.3) + + override func awakeFromNib() { + super.awakeFromNib() + + self.priceLabel.textColor = .text + + sizeBuyLabel.textColor = .text + sizeSellLabel.textColor = .text + + self.backgroundColor = .tableBackground + sizeBuyContentView.superview?.backgroundColor = .tableBackground + sizeSellContentView.superview?.backgroundColor = .tableBackground + } + + func update(order: Order, + maxSize: Double, + isBuy: Bool) { + let price = order.price + let size = order.size + + priceLabel.text = formatter.string(from: NSNumber(value: price)) + + sizeBuyLabel.text = formatter.string(from: NSNumber(value: size)) + sizeBuyLabel.textColor = isBuy ? .green : .red + + sizeSellLabel.text = formatter.string(from: NSNumber(value: size)) + sizeSellLabel.textColor = isBuy ? .green : .red + + sizeBuyContentView.backgroundColor = isBuy ? OrderCell.greenBarColor : OrderCell.redBarColor + sizeSellContentView.backgroundColor = isBuy ? OrderCell.greenBarColor : OrderCell.redBarColor + + var multiplier = min(size/maxSize, 1) + if !multiplier.isFinite || multiplier.isInfinite { + multiplier = 0 + } + update(constraint: &sizeBuyConstraint, multiplier: multiplier, on: sizeBuyContentView) + update(constraint: &sizeSellConstraint, multiplier: multiplier, on: sizeSellContentView) + + sizeBuyLabel.isHidden = !isBuy + sizeSellLabel.isHidden = isBuy + + sizeBuyContentView.isHidden = !isBuy + sizeSellContentView.isHidden = isBuy + } + + private func update(constraint: inout NSLayoutConstraint, multiplier: Double, on view: UIView) { + let newConstraint = constraint.constraintWithMultiplier(multiplier) + view.superview?.removeConstraint(constraint) + view.superview?.addConstraint(newConstraint) + self.layoutIfNeeded() + constraint = newConstraint + } +} diff --git a/Samples/DXFeedMarketDepth/SceneDelegate.swift b/Samples/DXFeedMarketDepth/SceneDelegate.swift new file mode 100644 index 000000000..e3609df79 --- /dev/null +++ b/Samples/DXFeedMarketDepth/SceneDelegate.swift @@ -0,0 +1,32 @@ +// +// +// Copyright (C) 2024 Devexperts LLC. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions) { + } + + func sceneDidDisconnect(_ scene: UIScene) { + } + + func sceneDidBecomeActive(_ scene: UIScene) { + } + + func sceneWillResignActive(_ scene: UIScene) { + } + + func sceneWillEnterForeground(_ scene: UIScene) { + } + + func sceneDidEnterBackground(_ scene: UIScene) { + } +} diff --git a/Samples/Playgrounds/DXFeedconnect.playground/Contents.swift b/Samples/Playgrounds/DXFeedconnect.playground/Contents.swift index c070e556d..df43de627 100644 --- a/Samples/Playgrounds/DXFeedconnect.playground/Contents.swift +++ b/Samples/Playgrounds/DXFeedconnect.playground/Contents.swift @@ -39,7 +39,7 @@ let allTypes = [Candle.self, Series.self, OptionSale.self] -let argSymbols = ["ETH/USD:GDAX"] +let argSymbols = ["AAPL"] let argTime: String? = nil // To avoid release inside internal {} scope diff --git a/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift b/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift index b3573b9ac..b31897c7b 100644 --- a/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift +++ b/Samples/Playgrounds/DxFeedReconnectSample.playground/Contents.swift @@ -36,14 +36,13 @@ class EndpoointStateListener: DXEndpointListener, Hashable { func hash(into hasher: inout Hasher) { hasher.combine("\(self):\(stringReference(self))") } - var callback: (DXEndpointState, DXEndpointState) -> Void = { _,_ in } + var callback: (DXEndpointState, DXEndpointState) -> Void = { _, _ in } init(overrides: (EndpoointStateListener) -> EndpoointStateListener) { _ = overrides(self) } } - // Demonstrates how to connect to an endpoint, subscribe to market data events, // handle reconnections and re-subscribing. let address = "demo.dxfeed.com:7300" // The address of the DxFeed endpoint. diff --git a/Samples/Playgrounds/LastEventsConsole.playground/Contents.swift b/Samples/Playgrounds/LastEventsConsole.playground/Contents.swift index 52d5c42d6..fb2bb9280 100644 --- a/Samples/Playgrounds/LastEventsConsole.playground/Contents.swift +++ b/Samples/Playgrounds/LastEventsConsole.playground/Contents.swift @@ -2,15 +2,13 @@ import PlaygroundSupport import UIKit import DXFeedFramework -/** - * This sample demonstrates a way to subscribe to the big world of symbols with dxFeed API, so that the events are - * updated and cached in memory of this process, and then take snapshots of those events from memory whenever - * they are needed. This example repeatedly reads symbol name from the console and prints a snapshot of its last - * quote, trade, summary, and profile events. - */ - -// Just UI for symbol input +/// This sample demonstrates a way to subscribe to the big world of symbols with dxFeed API, so that the events are +/// updated and cached in memory of this process, and then take snapshots of those events from memory whenever +/// they are needed. This example repeatedly reads symbol name from the console and prints a snapshot of its last +/// quote, trade, summary, and profile events. + class InputViewController: UIViewController { + // Just UI for symbol input var textField = UITextField() var resultLabel = UILabel() @@ -109,16 +107,14 @@ func fetchData(feed: DXFeed, symbol: String) throws -> String { * available for any reason and the wait above had timed out. This sample just prints all results. * "null" is printed when the event is not available. */ - try promises.forEach { pr in - result += (try pr.getResult()?.toString() ?? "Null result for \(pr)") + "\n" + try promises.forEach { promise in + result += (try promise.getResult()?.toString() ?? "Null result for \(promise)") + "\n" } print(result) print("end fetching for \(symbol)") return result } - - let viewController = InputViewController() viewController.feed = feed viewController.view.frame = CGRect(x: 0, y: 0, width: 400, height: 300) diff --git a/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift b/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift index 20ac43c55..7323f2a64 100644 --- a/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift +++ b/Samples/Playgrounds/SimpleAuthSample.playground/Contents.swift @@ -36,14 +36,14 @@ class EndpoointStateListener: DXEndpointListener, Hashable { func hash(into hasher: inout Hasher) { hasher.combine("\(self):\(stringReference(self))") } - var callback: (DXEndpointState, DXEndpointState) -> Void = { _,_ in } + var callback: (DXEndpointState, DXEndpointState) -> Void = { _, _ in } init(overrides: (EndpoointStateListener) -> EndpoointStateListener) { _ = overrides(self) } } -let address = "demo.dxfeed.com:7300"; +let address = "demo.dxfeed.com:7300" func updateTokenAndReconnect() { try? DXEndpoint.getInstance().connect("\(address)[login=entitle:\(generateToken())]") @@ -55,7 +55,6 @@ func generateToken() -> String { return String((0.. - @@ -38,7 +37,7 @@ -