Skip to content

Commit 9ee9092

Browse files
author
Fernando Fernandes
committed
Add support for FTX + GET /positions
1 parent 7d675ee commit 9ee9092

21 files changed

+545
-144
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Data+HexString.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
10+
public extension Data {
11+
12+
private static let hexAlphabet = Array("0123456789abcdef".unicodeScalars)
13+
14+
/// Converts `self` to HEX String.
15+
///
16+
/// https://stackoverflow.com/a/47476781/584548
17+
func hexStringEncoded() -> String {
18+
String(reduce(into: "".unicodeScalars) { result, value in
19+
result.append(Self.hexAlphabet[Int(value / 0x10)])
20+
result.append(Self.hexAlphabet[Int(value % 0x10)])
21+
})
22+
}
23+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// SwiftTraderError+Details.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 30.01.22.
6+
//
7+
8+
import Foundation
9+
10+
/// Holds logic to return meaningful error messages containing detailed system errors whenever possible.
11+
public extension SwiftTraderError {
12+
13+
static func error(for operation: SwiftTraderOperation, statusCode: Int, localizedErrorMessage: String, data: Data) -> SwiftTraderError {
14+
15+
switch operation {
16+
17+
case .ftxPositions:
18+
return .ftxStatusCodeNotOK(statusCode: statusCode, localizedErrorMessage: localizedErrorMessage)
19+
20+
case .kucoinFuturesAccountOverview,
21+
.kucoinFuturesCancelStopOrders,
22+
.kucoinFuturesOrderList,
23+
.kucoinFuturesStopOrderList,
24+
.kucoinFuturesPlaceStopLimitOrder,
25+
.kucoinFuturesPositionList:
26+
27+
guard let kucoinError = try? JSONDecoder().decode(KucoinSystemError.self, from: data) else {
28+
return .kucoinStatusCodeNotOK(statusCode: statusCode, localizedErrorMessage: localizedErrorMessage)
29+
}
30+
31+
return .kucoinStatusCodeNotOK(
32+
statusCode: statusCode,
33+
localizedErrorMessage: localizedErrorMessage,
34+
kucoinErrorCode: kucoinError.code,
35+
kucoinErrorMessage: kucoinError.message
36+
)
37+
}
38+
}
39+
}

Sources/SwiftTrader/Model/Error/SwiftTraderError+Kucoin.swift

Lines changed: 0 additions & 24 deletions
This file was deleted.

Sources/SwiftTrader/Model/Error/SwiftTraderError.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ public enum SwiftTraderError: Error {
1818
/// The returned model is unexpected.
1919
case unexpectedResponse(modelString: String)
2020

21+
// MARK: - FTX Related
22+
23+
/// No `FTXAuth` instance was given; it will be impossible to authenticate with FTX.
24+
case ftxMissingAuthentication
25+
26+
/// And error ocurred while executing the function `SwiftTrader.ftxPositions`.
27+
case ftxPositions(error: Error)
28+
29+
/// The response status code is something other than `200`.
30+
case ftxStatusCodeNotOK(statusCode: Int, localizedErrorMessage: String)
31+
2132
// MARK: - Kucoin Related
2233

2334
/// No `KucoinAuth` instance was given; it will be impossible to authenticate with Kucoin.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//
2+
// FTXAuth.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
10+
/// Holds data required to authenticate requests against FTX APIs.
11+
public struct FTXAuth {
12+
13+
// MARK: - Properties
14+
15+
public let apiKey: String
16+
public let apiSecret: String
17+
18+
// MARK: - Lifecycle
19+
20+
public init(apiKey: String, apiSecret: String) {
21+
self.apiKey = apiKey
22+
self.apiSecret = apiSecret
23+
}
24+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
//
2+
// FTXPositionList.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
10+
/// FTX "Get Positions" REST API response.
11+
///
12+
/// https://docs.ftx.com/#get-positions
13+
public struct FTXPositionList: Codable {
14+
public let success: Bool
15+
public let result: [Position]
16+
}
17+
18+
// Represents an open position within FTX.
19+
public struct Position: Codable {
20+
21+
// MARK: - Properties
22+
23+
public let cost: Double
24+
public let cumulativeBuySize: Double?
25+
public let cumulativeSellSize: Double?
26+
public let entryPrice: Double
27+
public let estimatedLiquidationPrice: Double
28+
public let future: String
29+
public let initialMarginRequirement: Double
30+
public let longOrderSize: Double
31+
public let maintenanceMarginRequirement: Double
32+
public let netSize: Double
33+
public let openSize: Double
34+
public let realizedPnl: Double
35+
public let recentAverageOpenPrice: Double?
36+
public let recentBreakEvenPrice: Double?
37+
public let recentPnl: Double?
38+
public let shortOrderSize: Double
39+
public let side: String
40+
public let size: Double
41+
public let unrealizedPnl: Double
42+
public let collateralUsed: Double
43+
44+
enum CodingKeys: String, CodingKey {
45+
case cost
46+
case cumulativeBuySize
47+
case cumulativeSellSize
48+
case entryPrice
49+
case estimatedLiquidationPrice
50+
case future
51+
case initialMarginRequirement
52+
case longOrderSize
53+
case maintenanceMarginRequirement
54+
case netSize
55+
case openSize
56+
case realizedPnl
57+
case recentAverageOpenPrice
58+
case recentBreakEvenPrice
59+
case recentPnl
60+
case shortOrderSize
61+
case side
62+
case size
63+
case unrealizedPnl
64+
case collateralUsed
65+
}
66+
67+
// MARK: - Lifecycle
68+
69+
public init(from decoder: Decoder) throws {
70+
let container = try decoder.container(keyedBy: CodingKeys.self)
71+
self.cost = try container.decode(Double.self, forKey: .cost)
72+
self.cumulativeBuySize = try container.decodeIfPresent(Double.self, forKey: .cumulativeBuySize)
73+
self.cumulativeSellSize = try container.decodeIfPresent(Double.self, forKey: .cumulativeSellSize)
74+
self.entryPrice = try container.decode(Double.self, forKey: .entryPrice)
75+
self.estimatedLiquidationPrice = try container.decode(Double.self, forKey: .estimatedLiquidationPrice)
76+
self.future = try container.decode(String.self, forKey: .future)
77+
self.initialMarginRequirement = try container.decode(Double.self, forKey: .initialMarginRequirement)
78+
self.longOrderSize = try container.decode(Double.self, forKey: .longOrderSize)
79+
self.maintenanceMarginRequirement = try container.decode(Double.self, forKey: .maintenanceMarginRequirement)
80+
self.netSize = try container.decode(Double.self, forKey: .netSize)
81+
self.openSize = try container.decode(Double.self, forKey: .openSize)
82+
self.realizedPnl = try container.decode(Double.self, forKey: .realizedPnl)
83+
self.recentAverageOpenPrice = try container.decodeIfPresent(Double.self, forKey: .recentAverageOpenPrice)
84+
self.recentBreakEvenPrice = try container.decodeIfPresent(Double.self, forKey: .recentBreakEvenPrice)
85+
self.recentPnl = try container.decodeIfPresent(Double.self, forKey: .recentPnl)
86+
self.shortOrderSize = try container.decode(Double.self, forKey: .shortOrderSize)
87+
self.side = try container.decode(String.self, forKey: .side)
88+
self.size = try container.decode(Double.self, forKey: .size)
89+
self.unrealizedPnl = try container.decode(Double.self, forKey: .unrealizedPnl)
90+
self.collateralUsed = try container.decode(Double.self, forKey: .collateralUsed)
91+
}
92+
}

Sources/SwiftTrader/Model/Kucoin/KucoinAuth.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import Foundation
99

10-
/// Holds data required to authenticate the requests against Kucoin APIs.
10+
/// Holds data required to authenticate requests against Kucoin APIs.
1111
public struct KucoinAuth {
1212

1313
// MARK: - Properties

Sources/SwiftTrader/Model/SwiftTraderExchange.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ import Foundation
1010
/// The list of supported exchanges.
1111
public enum SwiftTraderExchange: String, Codable {
1212
case Binance
13+
case FTX
1314
case Kucoin
1415
}

Sources/SwiftTrader/Model/SwiftTraderOperation.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import Foundation
99

1010
/// The currently running `SwiftTrader` operation.
1111
public enum SwiftTraderOperation {
12+
case ftxPositions
1213
case kucoinFuturesAccountOverview
1314
case kucoinFuturesCancelStopOrders
1415
case kucoinFuturesOrderList
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//
2+
// FTXAPI+Header.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
#if canImport(FoundationNetworking)
10+
import FoundationNetworking
11+
#endif
12+
13+
/// Holds logic to set values for default HTTP header fields common to all FTX APIs requests.
14+
extension FTXAPI {
15+
16+
/// Sets values for default header fields in the given `URLRequest`
17+
///
18+
/// Default header fields include `FTX-KEY`, `FTX-TS`, `FTX-SIGN`, etc.
19+
///
20+
/// - Parameters:
21+
/// - request: `URLRequest` where the header field values are to be set.
22+
/// - ftxAuth: `FTXAuth` that holds FTX authentication data.
23+
static func setRequestHeaderFields(request: inout URLRequest, ftxAuth: FTXAuth) throws {
24+
setAPIKey(request: &request, ftxAuth: ftxAuth)
25+
try setAPISignature(request: &request, ftxAuth: ftxAuth)
26+
setAPITimestamp(request: &request, ftxAuth: ftxAuth)
27+
}
28+
}
29+
30+
// MARK: - Private
31+
32+
private extension FTXAPI {
33+
34+
/// "FTX-KEY"
35+
static func setAPIKey(request: inout URLRequest, ftxAuth: FTXAuth) {
36+
request.setValue(ftxAuth.apiKey, forHTTPHeaderField: FTXAPI.HeaderField.apiKey)
37+
}
38+
39+
/// "FTX-SIGN"
40+
static func setAPISignature(request: inout URLRequest, ftxAuth: FTXAuth) throws {
41+
try NetworkRequestSignee.createHMACSignature(
42+
for: &request,
43+
secret: ftxAuth.apiSecret,
44+
isHexString: true,
45+
httpHeaderField: FTXAPI.HeaderField.apiSign
46+
)
47+
}
48+
49+
/// "FTX-TS"
50+
static func setAPITimestamp(request: inout URLRequest, ftxAuth: FTXAuth) {
51+
request.setValue("\(Date().timestampMilliseconds)", forHTTPHeaderField: FTXAPI.HeaderField.apiTimestamp)
52+
}
53+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// FTXAPI.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
10+
/// Holds constants related to FTX REST APIs.
11+
public struct FTXAPI {
12+
13+
public struct BaseURL {
14+
static let production = "https://ftx.com"
15+
}
16+
17+
public struct HeaderField {
18+
static let apiKey = "FTX-KEY"
19+
static let apiSign = "FTX-SIGN"
20+
static let apiTimestamp = "FTX-TS"
21+
}
22+
23+
public struct Path {
24+
static let positions = "/api/positions"
25+
}
26+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//
2+
// FTXPositionListRequest.swift
3+
//
4+
//
5+
// Created by Fernando Fernandes on 07.03.22.
6+
//
7+
8+
import Foundation
9+
#if canImport(FoundationNetworking)
10+
import FoundationNetworking
11+
#endif
12+
import Logging
13+
14+
/// A **request** for the list of positions.
15+
///
16+
/// https://docs.ftx.com/#get-positions
17+
public struct FTXPositionListRequest: NetworkRequest {
18+
19+
// MARK: - Properties
20+
21+
public typealias DecodableModel = FTXPositionList
22+
23+
public var logger: Logger {
24+
NetworkRequestLogger().default
25+
}
26+
27+
public var session: URLSession
28+
29+
public var request: URLRequest {
30+
get throws {
31+
let positionListResource = FTXPositionListResource()
32+
var urlRequest = URLRequest(url: try positionListResource.url)
33+
urlRequest.httpMethod = HTTPMethod.GET.rawValue
34+
try FTXAPI.setRequestHeaderFields(request: &urlRequest, ftxAuth: ftxAuth)
35+
return urlRequest
36+
}
37+
}
38+
39+
public var settings: NetworkRequestSettings
40+
41+
// MARK: Private
42+
43+
private let ftxAuth: FTXAuth
44+
45+
// MARK: - Lifecycle
46+
47+
/// Creates a new `FTXPositionListRequest` instance.
48+
///
49+
/// - Parameters:
50+
/// - session: `URLSession`, default is `.shared`.
51+
/// - ftxAuth: FTX authentication data.
52+
/// - settings: `NetworkRequestSettings`.
53+
public init(session: URLSession = .shared,
54+
ftxAuth: FTXAuth,
55+
settings: NetworkRequestSettings) {
56+
self.session = session
57+
self.ftxAuth = ftxAuth
58+
self.settings = settings
59+
}
60+
}
61+
62+
// MARK: - Network Request Protocol
63+
64+
public extension FTXPositionListRequest {
65+
66+
func decode(_ data: Data) throws -> DecodableModel {
67+
try JSONDecoder().decode(FTXPositionList.self, from: data)
68+
}
69+
}

0 commit comments

Comments
 (0)