From 342a921133b5fd28323fff3f777c5b1fea6cc771 Mon Sep 17 00:00:00 2001 From: AKosylo Date: Tue, 10 Oct 2023 18:50:58 +0200 Subject: [PATCH] OrderBase: WIP --- DXFeedFramework.xcodeproj/project.pbxproj | 4 + .../Events/Market/Extra/OrderBase.swift | 43 +++++- .../Events/Market/Extra/OrderSource.swift | 144 +++++++++++++++++- DXFeedFramework/Utils/ConcurrentDict.swift | 11 ++ DXFeedFrameworkTests/OrderSourceTest.swift | 49 ++++++ 5 files changed, 249 insertions(+), 2 deletions(-) create mode 100644 DXFeedFrameworkTests/OrderSourceTest.swift diff --git a/DXFeedFramework.xcodeproj/project.pbxproj b/DXFeedFramework.xcodeproj/project.pbxproj index 4371c4708..665054d01 100644 --- a/DXFeedFramework.xcodeproj/project.pbxproj +++ b/DXFeedFramework.xcodeproj/project.pbxproj @@ -212,6 +212,7 @@ 648BD56F2AC582AB004A3A95 /* DateTimeParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BD56E2AC582AB004A3A95 /* DateTimeParserTest.swift */; }; 648BD5712AC583AC004A3A95 /* TimeFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BD5702AC583AC004A3A95 /* TimeFormat.swift */; }; 648E98AA2AAF625800BFD219 /* IIndexedEvent+Ext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648E98A92AAF625800BFD219 /* IIndexedEvent+Ext.swift */; }; + 649282ED2AD593F3008F0F04 /* OrderSourceTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649282EC2AD593F3008F0F04 /* OrderSourceTest.swift */; }; 64963B6A2A8E545C001E40F7 /* IEventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64963B692A8E545C001E40F7 /* IEventType.swift */; }; 6498E6B22AB1D41A0093A065 /* DXSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6498E6B12AB1D41A0093A065 /* DXSchedule.swift */; }; 6498E6B52AB1D4480093A065 /* NativeSchedule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6498E6B42AB1D4480093A065 /* NativeSchedule.swift */; }; @@ -648,6 +649,7 @@ 648BD56E2AC582AB004A3A95 /* DateTimeParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateTimeParserTest.swift; sourceTree = ""; }; 648BD5702AC583AC004A3A95 /* TimeFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeFormat.swift; sourceTree = ""; }; 648E98A92AAF625800BFD219 /* IIndexedEvent+Ext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IIndexedEvent+Ext.swift"; sourceTree = ""; }; + 649282EC2AD593F3008F0F04 /* OrderSourceTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderSourceTest.swift; sourceTree = ""; }; 64963B692A8E545C001E40F7 /* IEventType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IEventType.swift; sourceTree = ""; }; 6498E6B12AB1D41A0093A065 /* DXSchedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DXSchedule.swift; sourceTree = ""; }; 6498E6B42AB1D4480093A065 /* NativeSchedule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSchedule.swift; sourceTree = ""; }; @@ -1373,6 +1375,7 @@ 648BD56E2AC582AB004A3A95 /* DateTimeParserTest.swift */, 64098F662ACEB6F70020D741 /* DXConnectionStateTests.swift */, 6486B95C2AD0287E00D8D5FA /* DateTests.swift */, + 649282EC2AD593F3008F0F04 /* OrderSourceTest.swift */, ); path = DXFeedFrameworkTests; sourceTree = ""; @@ -2211,6 +2214,7 @@ 6447A5ED2A8FCC2200739CCF /* UtilsTest.swift in Sources */, 64278C6C2A602CA20074B5AA /* CandleTests.swift in Sources */, 6498E6B72AB1DACE0093A065 /* ScheduleTest.swift in Sources */, + 649282ED2AD593F3008F0F04 /* OrderSourceTest.swift in Sources */, 6426C8932A531AB500236784 /* ThreadsTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DXFeedFramework/Events/Market/Extra/OrderBase.swift b/DXFeedFramework/Events/Market/Extra/OrderBase.swift index c2f643766..3398dc0dc 100644 --- a/DXFeedFramework/Events/Market/Extra/OrderBase.swift +++ b/DXFeedFramework/Events/Market/Extra/OrderBase.swift @@ -8,9 +8,19 @@ import Foundation class OrderBase: MarketEvent, IIndexedEvent, CustomStringConvertible { + var eventSource: IndexedEventSource = .defaultSource + var type: EventCode = .orderBase - var eventSource: IndexedEventSource +// var eventSource: IndexedEventSource { +// get { +// +// +// } +// set { +// +// } +// } var eventFlags: Int32 = 0 @@ -135,7 +145,38 @@ tradeSize: \(tradeSize) extension OrderBase { + /// Gets a value indicating whether this order has some size func hsaSize() -> Bool { return size != 0 && !size.isNaN } + /// Gets exchange code of this order. + public func getExchangeCode() -> Character { + return Character(BitUtil.getBits(flags: Int(flags), mask: OrderBase.exchangeMask, shift: OrderBase.exchangeShift)) + } + /// Sets exchange code of this order. + /// + /// - Throws: ``ArgumentException/exception(_:)`` + public func setExchangeCode(_ code: Character) throws { + try StringUtil.checkChar(char: code, mask: OrderBase.exchangeMask, name: "exchangeCode") + if let value = code.unicodeScalars.first?.value { + flags = Int32(BitUtil.setBits(flags: Int(flags), + mask: OrderBase.exchangeMask, + shift: OrderBase.exchangeShift, + bits: Int(value))) + } + } + + /// Gets or sets side of this order. + public var orderSide: Side { + get { + Side.valueOf(Int(BitUtil.getBits(flags: Int(flags), mask: OrderBase.sideMask, shift: OrderBase.sideShift))) + } + set { + flags = Int32(BitUtil.setBits(flags: Int(flags), + mask: OrderBase.sideMask, + shift: OrderBase.sideShift, + bits: newValue.rawValue)) + } + } + } diff --git a/DXFeedFramework/Events/Market/Extra/OrderSource.swift b/DXFeedFramework/Events/Market/Extra/OrderSource.swift index 3442fbcad..d95fa5d36 100644 --- a/DXFeedFramework/Events/Market/Extra/OrderSource.swift +++ b/DXFeedFramework/Events/Market/Extra/OrderSource.swift @@ -7,6 +7,148 @@ import Foundation -public class OrderSource : IndexedEventSource +/// Identifies source of ``Order``, ``AnalyticOrder``, ``OtcMarketsOrder`` and ``SpreadOrder`` events. +/// +/// There are the following kinds of order sources: +/// +/// Synthetic sources ``COMPOSITE_BID``, ``COMPOSITE_ASK``, +/// ``REGIONAL_BID``, and ``REGIONAL_ASK`` are provided for convenience of a consolidated +/// order book and are automatically generated based on the corresponding ``Quote`` events. +/// Aggregate sources ``AGGREGATE_BID`` and ``AGGREGATE_ASK`` provide +/// futures depth (aggregated by price level) and NASDAQ Level II (top of book for each market maker). +/// These source cannot be directly published to via dxFeed API. +/// ``isPublishable(Class) Publishable`` sources ``DEFAULT``, ``NTV``, and ``ISE`` +/// support full range of dxFeed API features. +/// +/// + +public class OrderSource: IndexedEventSource { + private static let pubOrder = 0x0001 + private static let pubAnalyticOrder = 0x0002 + private static let pubOtcMarketsOrder = 0x0004 + private static let pubSpreadOrder = 0x0008 + private static let fullOrderBook = 0x0010 + private static let flagsSize = 5 + + private let pubFlags: Int + private let isBuiltin: Bool + + static private let sourcesById = ConcurrentDict() + static private let sourcesByName = ConcurrentDict() + private let sourcesByIdCache = NSCache() + + private override init(identifier: Int, name: String) { + self.pubFlags = 0 + self.isBuiltin = false + super.init(identifier: identifier, name: name) + } + + private init(identifier: Int, name: String, pubFlags: Int) throws { + self.pubFlags = pubFlags + self.isBuiltin = true + super.init(identifier: identifier, name: name) + switch identifier { + case _ where identifier < 0: + throw ArgumentException.exception("id is negative") + case _ where identifier > 0 && identifier < 0x20 && !OrderSource.isSpecialSourceId(sourceId: identifier): + throw ArgumentException.exception("id is not marked as special") + case _ where identifier > 0x20: + if try (identifier != OrderSource.composeId(name: name)) + && (name != OrderSource.decodeName(identifier: identifier)) { + throw ArgumentException.exception("id does not match name") + } + default: + print("") + } + // Flag FullOrderBook requires that source must be publishable. + if (pubFlags & OrderSource.fullOrderBook) != 0 && (pubFlags & (OrderSource.pubOrder | OrderSource.pubAnalyticOrder | OrderSource.pubSpreadOrder)) == 0 { + throw ArgumentException.exception("Unpublishable full order book order") + } + + if !OrderSource.sourcesById.tryInsert(key: identifier, value: self) { + throw ArgumentException.exception("duplicate id") + } + if !OrderSource.sourcesByName.tryInsert(key: name, value: self) { + throw ArgumentException.exception("duplicate name") + } + } + + /// Determines whether specified source identifier refers to special order source. + ///Special order sources are used for wrapping non-order events into order events. + internal static func isSpecialSourceId(sourceId: Int) -> Bool { + return sourceId >= 1 && sourceId <= 6 + } + + internal static func composeId(name: String) throws -> Int { + var sourceId = 0 + let count = name.count + if count == 0 || count > 4 { + return 0 + } + for index in 0.. String { + if identifier == 0 { + throw ArgumentException.exception("Source name must contain from 1 to 4 characters. Current \(identifier)") + } + var name = String() + for index in stride(from: 24, through: 0, by: -8) { + if identifier >> index == 0 { // Skip highest contiguous zeros. + continue + } + var char = Character(((identifier >> index) & 0xff)) + try check(char: String(char)) + name.append(char) + } + return name + } + /// Gets a value indicating whether this source supports Full Order Book. + public func isFullOrderBook() -> Bool { + return (pubFlags & OrderSource.fullOrderBook) != 0 + } + + /// Returns order source for the specified source identifier. + /// - Throws: ``ArgumentException/exception(_:)``. Rethrows exception from Java. + public static func valueOf(identifier: Int) throws -> OrderSource { + var source: OrderSource + if let source = sourcesById[identifier] { + return source + } else { + let name = try decodeName(identifier: identifier) + let source = OrderSource(identifier: identifier, name: name) + sourcesById[identifier] = source + return source + } + } + /// Returns order source for the specified source name. + /// + /// The name must be either predefined, or contain at most 4 alphanumeric characters. + /// - Throws: ``ArgumentException/exception(_:)``. Rethrows exception from Java. + public static func valueOf(name: String) throws -> OrderSource { + var source: OrderSource + if let source = sourcesByName[name] { + return source + } else { + let identifier = try composeId(name: name) + let source = OrderSource(identifier: identifier, name: name) + sourcesByName[name] = source + return source + } + + } } diff --git a/DXFeedFramework/Utils/ConcurrentDict.swift b/DXFeedFramework/Utils/ConcurrentDict.swift index 02eab1b40..473506073 100644 --- a/DXFeedFramework/Utils/ConcurrentDict.swift +++ b/DXFeedFramework/Utils/ConcurrentDict.swift @@ -58,6 +58,17 @@ class ConcurrentDict: CustomStringConvertible { writer { $0.removeAll() } } + public func tryInsert(key: Key, value: Value) -> Bool { + var result = true + writer { + if $0[key] != nil { + result = false + } + $0[key] = value + } + return result + } + public func reader(_ block: ([Key: Value]) throws -> U) rethrows -> U { try accessQueue.sync { try block(set) } } diff --git a/DXFeedFrameworkTests/OrderSourceTest.swift b/DXFeedFrameworkTests/OrderSourceTest.swift new file mode 100644 index 000000000..0b8c734be --- /dev/null +++ b/DXFeedFrameworkTests/OrderSourceTest.swift @@ -0,0 +1,49 @@ +// +// OrderSourceTest.swift +// DXFeedFrameworkTests +// +// Created by Aleksey Kosylo on 10.10.23. +// + +import XCTest +@testable import DXFeedFramework + +final class OrderSourceTest: 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 testIsSpecialSource() throws { + XCTAssert(OrderSource.isSpecialSourceId(sourceId: 1)) + XCTAssert(!OrderSource.isSpecialSourceId(sourceId: 10)) + XCTAssert(OrderSource.isSpecialSourceId(sourceId: 2)) + } + + func testCompose() throws { + let res = try OrderSource.composeId(name: "1234") + XCTAssert(res == 825373492) + } + + func testCheckChar() throws { + do { + try OrderSource.check(char: "a") + } catch { + XCTAssert(false, "\(error)") + } + } + + func testDecode() throws { + do { + let res = try OrderSource.decodeName(identifier: 1) + print(res) + } catch { + print("\(error)") + } + } +}