Skip to content

Commit

Permalink
isolated position testing follow-ups (#202)
Browse files Browse the repository at this point in the history
* update in-line error msg ordering & change margin unit to percentage

* reflect selected market in margin mode selection screen

* remove sign from leverage multiplier

* add variable precision to leverage

* replace usage of `initialMarginFraction` with `effectiveInitialMarginFraction`

* incl 3x as a target leverage

* inverted coloring for cross margin and position leverage directions

* remove assertion failure for now

* fix display logic for isolated/cross selection

* clean up
  • Loading branch information
mike-dydx committed Jun 28, 2024
1 parent 89d02fa commit 8f3d0b5
Show file tree
Hide file tree
Showing 11 changed files with 94 additions and 38 deletions.
29 changes: 18 additions & 11 deletions dydx/dydxFormatter/dydxFormatter/dydxFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -367,12 +367,12 @@ public final class dydxFormatter: NSObject, SingletonProtocol {
}

/*
xxxxxx,yyyyy or xxxxx.yyyyy
will take the number and round it to the closest step size
e.g. if number is 1021 and step size is "100" then output is "1000"
converts number in multiplier, e.g.
1.20 -> 1.2x
removes trailing 0s as well
*/
public func multiplier(number: Double?) -> String? {
if let formattedText = dydxFormatter.shared.raw(number: number, digits: 2) {
public func multiplier(number: Double?, maxPrecision: Int = 2) -> String? {
if let formattedText = dydxFormatter.shared.raw(number: number, minDigits: 0, maxDigits: maxPrecision) {
return "\(formattedText)×"
} else {
return nil
Expand Down Expand Up @@ -421,19 +421,26 @@ public final class dydxFormatter: NSObject, SingletonProtocol {

public func raw(number: Double?, digits: Int, locale: Locale = Locale.current) -> String? {
guard let number = number else { return nil }
return raw(number: NSNumber(value: number), digits: digits, locale: locale)
return raw(number: NSNumber(value: number), minDigits: digits, maxDigits: digits, locale: locale)
}

public func raw(number: Double?, minDigits: Int, maxDigits: Int, locale: Locale = Locale.current) -> String? {
guard let number = number else { return nil }
return raw(number: NSNumber(value: number), minDigits: minDigits, maxDigits: maxDigits, locale: locale)
}

/*
xxxxxx,yyyyy or xxxxx.yyyyy
*/
public func raw(number: NSNumber?, digits: Int, locale: Locale = Locale.current) -> String? {
guard let number = number else { return nil }
return raw(number: number, minDigits: digits, maxDigits: digits, locale: locale)
}

public func raw(number: NSNumber?, minDigits: Int, maxDigits: Int, locale: Locale = Locale.current) -> String? {
if let value = number?.doubleValue {
if value.isFinite {
if let number = number {
rawFormatter.locale = locale
rawFormatter.minimumFractionDigits = max(digits, 0)
rawFormatter.maximumFractionDigits = max(digits, 0)
rawFormatter.minimumFractionDigits = max(minDigits, 0)
rawFormatter.maximumFractionDigits = max(maxDigits, 0)
rawFormatter.roundingMode = .halfUp

let formatted = rawFormatter.string(from: number)
Expand Down
26 changes: 26 additions & 0 deletions dydx/dydxFormatter/dydxFormatterTests/dydxFormatterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,32 @@ final class dydxFormatterTests: XCTestCase {
}
}

func testMultiplierFormatting() throws {
struct TestCase {
let number: Double
let maxPrecision: Int
let expected: String
}

let testCases: [TestCase] = [
.init(number: 0.01, maxPrecision: 2, expected: "0.01×"),
.init(number: -0.01, maxPrecision: 2, expected: "-0.01×"),
.init(number: 0.01, maxPrecision: 3, expected: "0.01×"),
.init(number: -0.01, maxPrecision: 3, expected: "-0.01×"),
.init(number: 0.01, maxPrecision: 1, expected: ""),
.init(number: -0.01, maxPrecision: 1, expected: ""),
.init(number: 10, maxPrecision: 3, expected: "10×"),
.init(number: -10, maxPrecision: 3, expected: "-10×")
]

for testCase in testCases {
let formatted = dydxFormatter.shared.multiplier(number: testCase.number, maxPrecision: testCase.maxPrecision)
print(testCase)
XCTAssertEqual(formatted, testCase.expected)
print()
}
}

func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,16 @@ final class dydxTradeReceiptPresenter: dydxReceiptPresenter {
let title = DataLocalizer.localize(path: "APP.TRADE.POSITION_LEVERAGE")
let unit = AmountTextModel.Unit.multiplier
positionLeverageViewModel.title = title
positionLeverageViewModel.value = createAmountChangeViewModel(title: title, tradeState: position?.leverage, tickSize: 2, unit: unit)
positionLeverageViewModel.value = createAmountChangeViewModel(title: title, tradeState: position?.leverage, tickSize: 2, unit: unit, shouldUseAbsoluteValues: true)
}

private func createAmountChangeViewModel(title: String,
tradeState: TradeStatesWithDoubleValues?,
tickSize: NSNumber?,
unit: AmountTextModel.Unit) -> AmountChangeModel {
let currentValue = tradeState?.current?.doubleValue.asNsNumber
let postValue = tradeState?.postOrder?.doubleValue.asNsNumber
tradeState: TradeStatesWithDoubleValues?,
tickSize: NSNumber?,
unit: AmountTextModel.Unit,
shouldUseAbsoluteValues: Bool = false) -> AmountChangeModel {
let currentValue = shouldUseAbsoluteValues ? tradeState?.current?.doubleValue.asNsNumber.abs() : tradeState?.current?.doubleValue.asNsNumber
let postValue = shouldUseAbsoluteValues ? tradeState?.postOrder?.doubleValue.asNsNumber.abs() : tradeState?.postOrder?.doubleValue.asNsNumber
let currentViewModel = currentValue == nil ? nil : AmountTextModel(amount: currentValue, unit: unit)
let postViewModel = postValue == nil ? nil : AmountTextModel(amount: postValue, unit: unit)
return AmountChangeModel(before: currentViewModel, after: postViewModel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,18 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
return "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE"
}
case IsolatedMarginAdjustmentType.remove:
if let freeCollateral = input.summary?.crossFreeCollateral?.doubleValue, amount >= freeCollateral {
return "ERRORS.TRANSFER_MODAL.TRANSFER_MORE_THAN_FREE"
}
if let positionMarginUpdated = input.summary?.positionMarginUpdated?.doubleValue, positionMarginUpdated < 0 {
return "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE"
}
if let effectiveInitialMarginFraction = market.configs?.effectiveInitialMarginFraction?.doubleValue, effectiveInitialMarginFraction > 0 {
let marketMaxLeverage = 1 / effectiveInitialMarginFraction
if let positionLeverageUpdated = input.summary?.positionLeverageUpdated?.doubleValue, positionLeverageUpdated > marketMaxLeverage {
return "ERRORS.TRADE_BOX_TITLE.INVALID_NEW_POSITION_LEVERAGE"
}
}
if let positionMarginUpdated = input.summary?.positionMarginUpdated?.doubleValue, positionMarginUpdated < 0 {
return "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE"
}
if let freeCollateral = input.summary?.crossFreeCollateral?.doubleValue, amount >= freeCollateral {
return "ERRORS.TRANSFER_MODAL.TRANSFER_MORE_THAN_FREE"
}
default:
break
}
Expand Down Expand Up @@ -208,11 +208,12 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
title: DataLocalizer.localize(path: "APP.GENERAL.CROSS_FREE_COLLATERAL"),
value: crossFreeCollateralChange))

let crossMarginUsage: AmountTextModel = .init(amount: input.summary?.crossMarginUsage, unit: .dollar)
let crossMarginUsageUpdated: AmountTextModel = .init(amount: input.summary?.crossMarginUsageUpdated, unit: .dollar)
let crossMarginUsage: AmountTextModel = .init(amount: input.summary?.crossMarginUsage, unit: .percentage)
let crossMarginUsageUpdated: AmountTextModel = .init(amount: input.summary?.crossMarginUsageUpdated, unit: .percentage)
let crossMarginUsageChange: AmountChangeModel = .init(
before: crossMarginUsage.amount != nil ? crossMarginUsage : nil,
after: crossMarginUsageUpdated.amount != nil ? crossMarginUsageUpdated : nil)
after: crossMarginUsageUpdated.amount != nil ? crossMarginUsageUpdated : nil,
increasingIsPositiveDirection: false)
crossReceiptItems.append(
dydxReceiptChangeItemView(
title: DataLocalizer.localize(path: "APP.GENERAL.CROSS_MARGIN_USAGE"),
Expand All @@ -228,11 +229,13 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
title: DataLocalizer.localize(path: "APP.TRADE.POSITION_MARGIN"),
value: positionMarginChange))

let positionLeverage: AmountTextModel = .init(amount: input.summary?.positionLeverage, unit: .multiplier)
let positionLeverageUpdated: AmountTextModel = .init(amount: input.summary?.positionLeverageUpdated, unit: .multiplier)
let positionLeverage: AmountTextModel = .init(amount: input.summary?.positionLeverage?.abs(), unit: .multiplier)
let positionLeverageUpdated: AmountTextModel = .init(amount: input.summary?.positionLeverageUpdated?.abs(), unit: .multiplier)
let positionLeverageChange: AmountChangeModel = .init(
before: positionLeverage.amount != nil ? positionLeverage : nil,
after: positionLeverageUpdated.amount != nil ? positionLeverageUpdated : nil)
after: positionLeverageUpdated.amount != nil ? positionLeverageUpdated : nil,
increasingIsPositiveDirection: false
)
positionReceiptItems.append(
dydxReceiptChangeItemView(
title: DataLocalizer.localize(path: "APP.TRADE.POSITION_LEVERAGE"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ private class dydxMarginModeViewPresenter: HostedViewPresenter<dydxMarginModeVie
super.init()

viewModel = dydxMarginModeViewModel()
viewModel?.market = "BTC-USD"
viewModel?.items = [crossItemViewModel, isolatedItemViewModel]
}

Expand All @@ -72,14 +71,17 @@ private class dydxMarginModeViewPresenter: HostedViewPresenter<dydxMarginModeVie
}

private func update(tradeInput: Abacus.TradeInput) {
viewModel?.market = tradeInput.marketId
let isSelectionDisabled = tradeInput.options?.marginModeOptions == nil
viewModel?.isDisabled = isSelectionDisabled
switch tradeInput.marginMode {
case .cross:
crossItemViewModel.isSelected = true
isolatedItemViewModel.isSelected = false
isolatedItemViewModel.isDisabled = tradeInput.options?.marginModeOptions == nil
isolatedItemViewModel.isDisabled = isSelectionDisabled
case .isolated:
crossItemViewModel.isSelected = false
crossItemViewModel.isDisabled = tradeInput.options?.marginModeOptions == nil
crossItemViewModel.isDisabled = isSelectionDisabled
isolatedItemViewModel.isSelected = true
default:
assertionFailure("should have margin mode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ private class dydxTargetLeverageViewPresenter: HostedViewPresenter<dydxTargetLev
.compactMap { $0 }
.sink { [weak self] configsAndAssetMap, tradeInput in
guard let viewModel = self?.viewModel, let marketId = tradeInput?.marketId, let market = configsAndAssetMap[marketId] else { return }
if let imf = market.configs?.initialMarginFraction?.doubleValue, imf > 0 {
if let imf = market.configs?.effectiveInitialMarginFraction?.doubleValue, imf > 0 {
let maxLeverage = 1.0 / imf
viewModel.leverageOptions = [1, 2, 5, 10]
viewModel.leverageOptions = [1, 2, 3, 5, 10]
.filter { $0 < maxLeverage }
.map { dydxTargetLeverageViewModel.LeverageTextAndValue(text: dydxFormatter.shared.multiplier(number: Double($0)) ?? "", value: $0) }
viewModel.leverageOptions.append(dydxTargetLeverageViewModel.LeverageTextAndValue(text: DataLocalizer.localize(path: "APP.GENERAL.MAX"), value: maxLeverage))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ class dydxTradeInputMarginViewPresenter: HostedViewPresenter<dydxTradeInputMargi
private func update(withTradeInput tradeInput: Abacus.TradeInput) {
viewModel?.shouldDisplayTargetLeverage = tradeInput.options?.needsTargetLeverage == true
viewModel?.marginMode = DataLocalizer.localize(path: "APP.GENERAL.\(tradeInput.marginMode.rawValue)")
viewModel?.targetLeverage = dydxFormatter.shared.raw(number: tradeInput.targetLeverage, digits: 2)
viewModel?.targetLeverage = dydxFormatter.shared.raw(number: tradeInput.targetLeverage, minDigits: 0, maxDigits: 2)
}
}
13 changes: 12 additions & 1 deletion dydx/dydxViews/dydxViews/Shared/AmountChange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,22 @@ import Utilities

public class AmountChangeModel: BeforeArrowAfterModel<AmountTextModel> {

public convenience init(before: AmountTextModel?, after: AmountTextModel?) {
public convenience init(before: AmountTextModel?, after: AmountTextModel?, increasingIsPositiveDirection: Bool = true) {
self.init()

self.before = before
self.after = after

changeDirection = { [weak self] in
guard let beforeAmount = self?.before?.amount, let afterAmount = self?.after?.amount else {
return .orderedSame
}
if increasingIsPositiveDirection {
return beforeAmount.compare(afterAmount)
} else {
return afterAmount.compare(beforeAmount)
}
}
}

public required init(unit: AmountTextModel.Unit = .dollar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public class dydxMarginModeItemViewModel: PlatformViewModel {
public class dydxMarginModeViewModel: PlatformViewModel {
@Published public var market: String?
@Published public var items: [dydxMarginModeItemViewModel] = []
@Published public var isDisabled: Bool = false

public init() { }

Expand Down Expand Up @@ -125,9 +126,14 @@ public class dydxMarginModeViewModel: PlatformViewModel {
}
}

if self.isDisabled {
InlineAlertViewModel(.init(title: nil, body: DataLocalizer.localize(path: "WARNINGS.TRADE_BOX.UNABLE_TO_CHANGE_MARGIN_MODE", params: ["MARKET": self.market ?? "--"]), level: .warning))
.createView()
}
Spacer()
}
.padding(.horizontal)
.padding(.bottom, max((self.safeAreaInsets?.bottom ?? 0), 16))
.themeColor(background: .layer3)
.makeSheet(sheetStyle: .fitSize)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public class dydxTargetLeverageViewModel: PlatformViewModel {

private func createOptionsGroup(parentStyle: ThemeStyle) -> some View {
let spacing: CGFloat = 8
let maxItemsToDisplay = CGFloat(min(5, leverageOptions.count))
let maxItemsToDisplay = CGFloat(leverageOptions.count)

return SingleAxisGeometryReader { width in
let width = (width + spacing) / maxItemsToDisplay - spacing
Expand Down
2 changes: 1 addition & 1 deletion dydxV4/dydxV4/_Tracking/TrackingViewController+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension TrackingViewController: ScreenIdentifiable, TrackingViewProtocol {
private extension TrackingViewController {
private var path: String {
guard let path = history?.path else {
assertionFailure("no path for \(screenClass)")
// assertionFailure("no path for \(screenClass)")
return ""
}
return path
Expand Down

0 comments on commit 8f3d0b5

Please sign in to comment.