Skip to content

Commit

Permalink
MOB-94 : match signed number formatting to web (#86)
Browse files Browse the repository at this point in the history
* display mkt value / qty

* add back long/short prefix

* match web for signed amounts

* change usage of `signOnly`

* remove hardcoded values

* use token denom from config file

* move like 0.00 logic to signed amount view model

* move like 0.00 logic to signed amount view model

* add formatter tests

* clean up

* address @rui's comment

* clean up

* move convenience inits to dydxViews, clean up
  • Loading branch information
mike-dydx authored Feb 14, 2024
1 parent 3ec9a6a commit ba8147b
Show file tree
Hide file tree
Showing 18 changed files with 235 additions and 137 deletions.
14 changes: 10 additions & 4 deletions PlatformUI/PlatformUI/Components/Labels/SignedAmount.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,26 @@ public class SignedAmountViewModel: PlatformViewModel, Hashable {
case allText
}

public enum DisplayType {
case dollar
case percent
}

@Published public var text: String?
@Published public var sign: PlatformUISign
@Published public var coloringOption: ColoringOption
@Published public var positiveTextStyleKey: String
@Published public var negativeTextStyleKey: String

public init(text: String? = nil, sign: PlatformUISign = .plus, coloringOption: ColoringOption = .signOnly, positiveTextStyleKey: String, negativeTextStyleKey: String) {
public init(text: String? = nil, sign: PlatformUISign = .plus, coloringOption: ColoringOption, positiveTextStyleKey: String, negativeTextStyleKey: String) {
self.text = text
self.sign = sign
self.coloringOption = coloringOption
self.positiveTextStyleKey = positiveTextStyleKey
self.negativeTextStyleKey = negativeTextStyleKey
}


public static func == (lhs: SignedAmountViewModel, rhs: SignedAmountViewModel) -> Bool {
lhs.text == rhs.text &&
lhs.sign == rhs.sign &&
Expand All @@ -41,7 +47,7 @@ public class SignedAmountViewModel: PlatformViewModel, Hashable {
hasher.combine(coloringOption)
}

public static var previewValue = SignedAmountViewModel(text: "2.02", sign: .plus, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus")
public static var previewValue = SignedAmountViewModel(text: "2.02", sign: .plus, coloringOption: .allText, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus")

public override func createView(parentStyle: ThemeStyle = ThemeStyle.defaultStyle, styleKey: String? = nil) -> PlatformView {
PlatformView(viewModel: self, parentStyle: parentStyle, styleKey: styleKey) { [weak self] style in
Expand Down Expand Up @@ -98,10 +104,10 @@ struct SignedAmount_Previews: PreviewProvider {

static var previews: some View {
Group {
SignedAmountViewModel(text: "$2.00", sign: .plus, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus").createView()
SignedAmountViewModel(text: "$2.00", sign: .plus, coloringOption: .allText, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus").createView()
.previewLayout(.sizeThatFits)

SignedAmountViewModel(text: "$2.00", sign: .minus, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus").createView()
SignedAmountViewModel(text: "$2.00", sign: .minus, coloringOption: .allText, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus").createView()
.previewLayout(.sizeThatFits)

SignedAmountViewModel(text: "$2.00", sign: .plus, coloringOption: .allText, positiveTextStyleKey: "signed-plus", negativeTextStyleKey: "signed-minus").createView()
Expand Down
46 changes: 35 additions & 11 deletions dydx/dydxFormatter/dydxFormatter/dydxFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ public final class dydxFormatter: NSObject, SingletonProtocol {

private var percentFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.roundingMode = .up
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 2
formatter.maximumFractionDigits = 2
Expand Down Expand Up @@ -234,17 +233,33 @@ public final class dydxFormatter: NSObject, SingletonProtocol {
return nil
}

public func dollarVolume(number: Double?, digits: Int = 2) -> String? {
/// formats the number as "$" or "-$" of "+$" prefixed
/// - Parameters:
/// - number: the number to format
/// - digits: after-decimal precision
/// - shouldDisplaySignForPositiveNumbers: whether to include "+" in the prefix for positive numbers
/// - Returns: the number formatted as a dollar amount
public func dollarVolume(number: Double?, digits: Int = 2, shouldDisplaySignForPositiveNumbers: Bool = false) -> String? {
if let number = number {
return dollarVolume(number: NSNumber(value: number), digits: digits)
return dollarVolume(number: NSNumber(value: number), digits: digits, shouldDisplaySignForPositiveNumbers: shouldDisplaySignForPositiveNumbers)
}
return nil
}

public func dollarVolume(number: NSNumber?, digits: Int = 2) -> String? {
if let number = number, let formatted = condensed(number: number.abs(), digits: digits) {
if number.doubleValue >= 0.0 {
/// formats the number as "$" or "-$" of "+$" prefixed
/// - Parameters:
/// - number: the number to format
/// - digits: after-decimal precision
/// - shouldDisplaySignForPositiveNumbers: whether to include "+" in the prefix for positive numbers
/// - Returns: the number formatted as a dollar amount
public func dollarVolume(number: NSNumber?, digits: Int = 2, shouldDisplaySignForPositiveNumbers: Bool = false) -> String? {
if let number = number,
let formatted = condensed(number: number.abs(), digits: digits),
let formattedZero = condensed(number: 0.0, digits: digits) {
if formattedZero == formatted {
return "$\(formatted)"
} else if number.doubleValue >= 0.0 {
return "\(shouldDisplaySignForPositiveNumbers ? "+" : "")$\(formatted)"
} else {
return "-$\(formatted)"
}
Expand Down Expand Up @@ -434,21 +449,30 @@ public final class dydxFormatter: NSObject, SingletonProtocol {
}
}

public func percent(number: Double?, digits: Int, minDigits: Int? = nil) -> String? {
public func percent(number: Double?, digits: Int, minDigits: Int? = nil, shouldDisplayPlusSignForPositiveNumbers: Bool = false) -> String? {
if let number = number {
return percent(number: NSNumber(value: number), digits: digits, minDigits: minDigits)
return percent(number: NSNumber(value: number), digits: digits, minDigits: minDigits, shouldDisplayPlusSignForPositiveNumbers: shouldDisplayPlusSignForPositiveNumbers)
}
return nil
}

public func percent(number: NSNumber?, digits: Int, minDigits: Int? = nil) -> String? {
public func percent(number: NSNumber?, digits: Int, minDigits: Int? = nil, shouldDisplayPlusSignForPositiveNumbers: Bool = false) -> String? {
if let number = number {
if number.doubleValue.isFinite {
let percent = NSNumber(value: number.doubleValue * 100.0)
percentFormatter.minimumFractionDigits = minDigits ?? digits
percentFormatter.maximumFractionDigits = digits
if let formatted = percentFormatter.string(from: percent) {
return "\(formatted)%"
if let formatted = percentFormatter.string(from: percent.abs()),
let formattedZero = percentFormatter.string(from: 0) {
if formattedZero == formatted {
return "\(formatted)%"
} else if number.doubleValue >= 0.0 {
return "\(shouldDisplayPlusSignForPositiveNumbers ? "+" : "")\(formatted)%"
} else if number.doubleValue < 0.0 {
return "-\(formatted)%"
} else {
return nil
}
} else {
return nil
}
Expand Down
105 changes: 70 additions & 35 deletions dydx/dydxFormatter/dydxFormatterTests/dydxFormatterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,47 +19,82 @@ final class dydxFormatterTests: XCTestCase {
}

func testDollarFormatting() throws {
var number: Double = 1
var digits = 2
var formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
var expected = "$1.00"
XCTAssertEqual(formatted, expected)
struct TestCase {
let number: Double
let digits: Int
let expected: String
}

let testCases: [TestCase] = [
.init(number: 1, digits: 2, expected: "$1.00"),
.init(number: -0.001, digits: 0, expected: "$0"),
.init(number: -0.001, digits: 3, expected: "-$0.001"),
.init(number: -0.001, digits: 2, expected: "$0.00"),
.init(number: 0.001, digits: 2, expected: "$0.00"),
.init(number: -0.005, digits: 2, expected: "$0.00"),
.init(number: -0.0051, digits: 2, expected: "-$0.01")
]

for testCase in testCases {
let formatted = dydxFormatter.shared.dollar(number: testCase.number, digits: testCase.digits)
XCTAssertEqual(formatted, testCase.expected)
}
}

number = -0.001
digits = 0
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "$0"
XCTAssertEqual(formatted, expected)
func testDollarVolumeFormatting() throws {
struct TestCase {
let number: Double
let digits: Int
let shouldDisplaySignForPositiveNumbers: Bool
let expected: String
}

number = -0.001
digits = 2
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "$0.00"
XCTAssertEqual(formatted, expected)
let testCases: [TestCase] = [
.init(number: 1, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "$1.00"),
.init(number: -0.001, digits: 0, shouldDisplaySignForPositiveNumbers: false, expected: "$0"),
.init(number: -0.001, digits: 3, shouldDisplaySignForPositiveNumbers: false, expected: "-$0.001"),
.init(number: -0.001, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "$0.00"),
.init(number: 0.001, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "$0.00"),
.init(number: -0.005, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "$0.00"),
.init(number: -0.0051, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "-$0.01"),
.init(number: 1, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "+$1.00"),
.init(number: 0, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "$0.00"),
.init(number: -1, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "-$1.00")
]

number = -0.001
digits = 3
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "-$0.001"
XCTAssertEqual(formatted, expected)
for testCase in testCases {
let formatted = dydxFormatter.shared.dollarVolume(number: testCase.number, digits: testCase.digits, shouldDisplaySignForPositiveNumbers: testCase.shouldDisplaySignForPositiveNumbers)
XCTAssertEqual(formatted, testCase.expected)
}
}

number = 0.001
digits = 2
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "$0.00"
XCTAssertEqual(formatted, expected)
func testPercentFormatting() throws {
struct TestCase {
let number: Double
let digits: Int
let shouldDisplaySignForPositiveNumbers: Bool
let expected: String
}

number = -0.005
digits = 2
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "$0.00"
XCTAssertEqual(formatted, expected)
let testCases: [TestCase] = [
.init(number: 0.01, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "1.00%"),
.init(number: -0.00001, digits: 0, shouldDisplaySignForPositiveNumbers: false, expected: "0%"),
.init(number: -0.00001, digits: 3, shouldDisplaySignForPositiveNumbers: false, expected: "-0.001%"),
.init(number: -0.00001, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "0.00%"),
.init(number: 0.00001, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "0.00%"),
.init(number: -0.00005, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "0.00%"),
.init(number: -0.000051, digits: 2, shouldDisplaySignForPositiveNumbers: false, expected: "-0.01%"),
.init(number: 0.01, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "+1.00%"),
.init(number: 0, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "0.00%"),
.init(number: -0.01, digits: 2, shouldDisplaySignForPositiveNumbers: true, expected: "-1.00%")
]

number = -0.0051
digits = 2
formatted = dydxFormatter.shared.dollar(number: number, digits: digits)
expected = "-$0.01"
XCTAssertEqual(formatted, expected)
for testCase in testCases {
let formatted = dydxFormatter.shared.percent(number: testCase.number, digits: testCase.digits, shouldDisplayPlusSignForPositiveNumbers: testCase.shouldDisplaySignForPositiveNumbers)
print(testCase)
XCTAssertEqual(formatted, testCase.expected)
print()
}
}

func testPerformanceExample() throws {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class dydxTransferSubaccountWorker: BaseWorker {
override func start() {
super.start()

AbacusStateManager.shared.state.accountBalance(of: .usdc)
AbacusStateManager.shared.state.accountBalance(of: AbacusStateManager.shared.environment?.usdcTokenInfo?.denom)
.filter { value in
(value ?? 0) > dydxTransferSubaccountWorker.balanceRetainAmount
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,22 +60,11 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter<dydxMarketPositionVie

viewModel?.unrealizedPNLAmount = sharedOrderViewModel.unrealizedPnl
viewModel?.unrealizedPNLPercent = sharedOrderViewModel.unrealizedPnlPercent

let sign: PlatformUISign
if position.realizedPnlPercent?.current?.doubleValue ?? 0 > 0 {
sign = .plus
} else if position.realizedPnlPercent?.current?.doubleValue ?? 0 < 0 {
sign = .minus
} else {
sign = .none
}
let pnl = dydxFormatter.shared.dollarVolume(number: abs(position.realizedPnl?.current?.doubleValue ?? 0), digits: 2)
viewModel?.realizedPNLAmount = SignedAmountViewModel(text: pnl, sign: sign, coloringOption: .signOnly)
viewModel?.realizedPNLAmount = SignedAmountViewModel(amount: position.realizedPnl?.current?.doubleValue, displayType: .dollar, coloringOption: .allText)
viewModel?.liquidationPrice = dydxFormatter.shared.dollar(number: position.liquidationPrice?.current?.doubleValue, digits: configs.displayTickSizeDecimals?.intValue ?? 0)

viewModel?.leverage = sharedOrderViewModel.leverage
viewModel?.leverageIcon = sharedOrderViewModel.leverageIcon
viewModel?.liquidationPrice = dydxFormatter.shared.dollar(number: position.liquidationPrice?.current?.doubleValue, digits: configs.displayTickSizeDecimals?.intValue ?? 0)

viewModel?.size = sharedOrderViewModel.size
viewModel?.side = sharedOrderViewModel.sideText
viewModel?.token = sharedOrderViewModel.token
Expand All @@ -87,15 +76,6 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter<dydxMarketPositionVie
viewModel?.openPrice = dydxFormatter.shared.dollar(number: position.entryPrice?.current?.doubleValue, digits: configs.displayTickSizeDecimals?.intValue ?? 0)
viewModel?.closePrice = dydxFormatter.shared.dollar(number: position.exitPrice?.doubleValue, digits: configs.displayTickSizeDecimals?.intValue ?? 0)

let fundingSign: PlatformUISign
if position.netFunding?.doubleValue ?? 0 > 0 {
fundingSign = .plus
} else if position.netFunding?.doubleValue ?? 0 < 0 {
fundingSign = .minus
} else {
fundingSign = .none
}
let funding = dydxFormatter.shared.dollarVolume(number: abs(position.netFunding?.doubleValue ?? 0), digits: 2)
viewModel?.funding = SignedAmountViewModel(text: funding, sign: fundingSign, coloringOption: .signOnly)
viewModel?.funding = SignedAmountViewModel(amount: position.netFunding?.doubleValue, displayType: .dollar, coloringOption: .allText)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,12 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter<dydxPortfolioPosi
if let leverage = position.leverage?.current?.doubleValue, let maxLeverage = position.maxLeverage?.current?.doubleValue, maxLeverage > 0 {
item.leverageIcon = LeverageRiskModel(level: LeverageRiskModel.Level(marginUsage: leverage / maxLeverage), viewSize: 16, displayOption: .iconOnly)
}

item.indexPrice = dydxFormatter.shared.dollar(number: market.oraclePrice, digits: configs.displayTickSizeDecimals?.intValue ?? 0)
item.entryPrice = dydxFormatter.shared.dollar(number: position.entryPrice?.current, digits: configs.displayTickSizeDecimals?.intValue ?? 0)

let sign: PlatformUISign
if position.unrealizedPnlPercent?.current?.doubleValue ?? 0 > 0 {
sign = .plus
} else if position.unrealizedPnlPercent?.current?.doubleValue ?? 0 < 0 {
sign = .minus
} else {
sign = .none
}
let pnlPercent = dydxFormatter.shared.percent(number: abs(position.unrealizedPnlPercent?.current?.doubleValue ?? 0), digits: 2)
item.unrealizedPnlPercent = SignedAmountViewModel(text: pnlPercent, sign: sign, coloringOption: .allText)

let pnl = dydxFormatter.shared.dollarVolume(number: abs(position.unrealizedPnl?.current?.doubleValue ?? 0), digits: 2)
item.unrealizedPnl = SignedAmountViewModel(text: pnl, sign: sign, coloringOption: .signOnly)
item.unrealizedPnl = SignedAmountViewModel(amount: position.unrealizedPnl?.current?.doubleValue ?? 0, displayType: .dollar, coloringOption: .allText)
item.unrealizedPnlPercent = dydxFormatter.shared.percent(number: position.unrealizedPnlPercent?.current?.doubleValue ?? 0, digits: 2)

if let url = asset.resources?.imageUrl {
item.logoUrl = URL(string: url)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class dydxProfileBalancesViewPresenter: HostedViewPresenter<dydxProfileBa
public override func start() {
super.start()

AbacusStateManager.shared.state.accountBalance(of: .dydx)
AbacusStateManager.shared.state.accountBalance(of: AbacusStateManager.shared.environment?.dydxTokenInfo?.denom)
.sink { [weak self] dydxAmount in
if let dydxAmount = dydxAmount {
self?.viewModel?.walletAmount = dydxFormatter.shared.raw(number: Parser.standard.asNumber(dydxAmount), digits: 4)
Expand All @@ -46,7 +46,7 @@ public class dydxProfileBalancesViewPresenter: HostedViewPresenter<dydxProfileBa
}
.store(in: &subscriptions)

AbacusStateManager.shared.state.stakingBalance(of: .dydx)
AbacusStateManager.shared.state.stakingBalance(of: AbacusStateManager.shared.environment?.dydxTokenInfo?.denom ?? "")
.sink { [weak self] dydxAmount in
if let dydxAmount = dydxAmount {
self?.viewModel?.stakedAmount = dydxFormatter.shared.raw(number: Parser.standard.asNumber(dydxAmount), digits: 4)
Expand Down
Loading

0 comments on commit ba8147b

Please sign in to comment.