diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index b263866fd..53f63ffa5 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 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 */; }; @@ -727,6 +728,7 @@ 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 = ""; }; @@ -1829,6 +1831,7 @@ 64820AAE2BB2E26100BDFD0B /* DXOtcMarketOrderTest.swift */, 644B95E62BC542F600E95CB7 /* DXAttachTest.swift */, 64048A5A2BD7E5BF00902590 /* DXOnDemandServiceTest.swift */, + 642C9A2C2BFE2FFE0074864A /* DXMarketDepthTest.swift */, ); path = DXFeedFrameworkTests; sourceTree = ""; @@ -2891,6 +2894,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 */, diff --git a/DXFeedFramework/Extra/MarketDepthListener.swift b/DXFeedFramework/Extra/MarketDepthListener.swift index c33cba56c..0df98de1c 100644 --- a/DXFeedFramework/Extra/MarketDepthListener.swift +++ b/DXFeedFramework/Extra/MarketDepthListener.swift @@ -10,7 +10,7 @@ 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 diff --git a/DXFeedFramework/Native/Feed/NativePublisher.swift b/DXFeedFramework/Native/Feed/NativePublisher.swift index 284adc2be..cf0f884a4 100644 --- a/DXFeedFramework/Native/Feed/NativePublisher.swift +++ b/DXFeedFramework/Native/Feed/NativePublisher.swift @@ -41,7 +41,7 @@ class NativePublisher { let thread = currentThread() _ = try ErrorCheck.nativeCall(thread, dxfg_DXPublisher_publishEvents(thread, publisher, - listPointer)) + listPointer)) } } 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..4b3fc7ba6 --- /dev/null +++ b/DXFeedFrameworkTests/DXMarketDepthTest.swift @@ -0,0 +1,367 @@ +// +// +// 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 + +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! + 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") + 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 { + let bookSize = 100 + var book = [Order?](repeatElement(nil, + count: bookSize)) + var expectedBuy = 0 + var expectedSell = 0 + for position in 0..<10000 { + var index = Int.random(in: 0.. 0 ? 1 : 0, changesBuy) + XCTAssertEqual(expectedSell > 0 ? 1 : 0, changesSell) + } + + func testStressSources() throws { + let sources = [OrderSource.ntv!, + OrderSource.NTV!, + OrderSource.ICE!, + OrderSource.ISE!, + ] + model = try MarketDepthModel(symbol: symbol, sources: sources, aggregationPeriodMillis: 0, mode: .multiple, feed: feed, listener: self) + + let bookSize = 100 + var expectedBuy = 0 + var expectedSell = 0 + + + var books = [Int: Array]() + sources.forEach { source in + books[source.identifier] = [Order?](repeatElement(nil, + count: bookSize)) + } + for position in 0..<10000 { + var 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)") + } + } +} 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/PublisherTest.swift b/DXFeedFrameworkTests/PublisherTest.swift index 420898f34..e53342da5 100644 --- a/DXFeedFrameworkTests/PublisherTest.swift +++ b/DXFeedFrameworkTests/PublisherTest.swift @@ -31,10 +31,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 +53,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 +61,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 { 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/QuoteTableApp/MarketDepthViewController.swift b/Samples/QuoteTableApp/MarketDepthViewController.swift index 5866b2b95..1b7129a44 100644 --- a/Samples/QuoteTableApp/MarketDepthViewController.swift +++ b/Samples/QuoteTableApp/MarketDepthViewController.swift @@ -83,9 +83,11 @@ extension MarketDepthViewController: MarketDepthListener { func modelChanged(changes: DXFeedFramework.OrderBook) { var maxValue: Double = 0 changes.buyOrders.forEach { order in + print(order.eventSource.name) maxValue = max(maxValue, order.size) } changes.sellOrders.forEach { order in + print(order.eventSource.name) maxValue = max(maxValue, order.size) }