Skip to content

Commit

Permalink
unopened isolated positions pt1 (#191)
Browse files Browse the repository at this point in the history
* hide/show target leverage for margin mode selection

* fix adjust margin % horizontal sizings

* remove unused files

* dismiss on success, update button state on submission

* multi-line receipt line item title

* use localized error

* stubbed unopened isolated positions UI

* wire up abacus data for unopened positions

* put back fixed size, clean up

* hide keyboard when margin direction changes

* remove max action

* add local validation

* update button state after validation

* remove alternate validation

* clean up

* ui tweaks
  • Loading branch information
mike-dydx authored Jun 17, 2024
1 parent 5702268 commit a66f9cc
Show file tree
Hide file tree
Showing 12 changed files with 383 additions and 152 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter<dydxMarketPositionVie
}

private func updatePosition(position: SubaccountPosition, triggerOrders: [SubaccountOrder], marketMap: [String: PerpetualMarket], assetMap: [String: Asset]) {
guard let sharedOrderViewModel = dydxPortfolioPositionsViewPresenter.createViewModelItem(position: position, marketMap: marketMap, assetMap: assetMap) else {
guard let sharedOrderViewModel = dydxPortfolioPositionsViewPresenter.createPositionViewModelItem(position: position, marketMap: marketMap, assetMap: assetMap) else {
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ protocol dydxPortfolioPositionsViewPresenterProtocol: HostedViewPresenterProtoco
}

class dydxPortfolioPositionsViewPresenter: HostedViewPresenter<dydxPortfolioPositionsViewModel>, dydxPortfolioPositionsViewPresenterProtocol {
private var cache = [String: dydxPortfolioPositionItemViewModel]()
private var positionsCache = [String: dydxPortfolioPositionItemViewModel]()
private var pendingPositionsCache = [String: dydxPortfolioPendingPositionsItemViewModel]()

init(viewModel: dydxPortfolioPositionsViewModel?) {
super.init()
Expand All @@ -37,40 +38,94 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter<dydxPortfolioPosi
// TODO: remove once isolated markets is supported and force released
self?.viewModel?.shouldDisplayIsolatedPositionsWarning = onboarded
if onboarded {
self?.viewModel?.placeholderText = DataLocalizer.localize(path: "APP.GENERAL.PLACEHOLDER_NO_POSITIONS")
self?.viewModel?.emptyText = DataLocalizer.localize(path: "APP.GENERAL.PLACEHOLDER_NO_POSITIONS")
} else {
self?.viewModel?.placeholderText = DataLocalizer.localize(path: "APP.GENERAL.PLACEHOLDER_NO_POSITIONS_LOG_IN")
self?.viewModel?.emptyText = DataLocalizer.localize(path: "APP.GENERAL.PLACEHOLDER_NO_POSITIONS_LOG_IN")
}
}
.store(in: &subscriptions)

Publishers
.CombineLatest3(AbacusStateManager.shared.state.selectedSubaccountPositions,
.CombineLatest4(AbacusStateManager.shared.state.selectedSubaccountPositions,
AbacusStateManager.shared.state.selectedSubaccountPendingPositions,
AbacusStateManager.shared.state.marketMap,
AbacusStateManager.shared.state.assetMap)
.sink { [weak self] positions, marketMap, assetMap in
.sink { [weak self] positions, pendingPositions, marketMap, assetMap in
self?.updatePositions(positions: positions, marketMap: marketMap, assetMap: assetMap)
self?.updatePendingPositions(pendingPositions: pendingPositions, marketMap: marketMap, assetMap: assetMap)
}
.store(in: &subscriptions)
}

private func updatePositions(positions: [SubaccountPosition], marketMap: [String: PerpetualMarket], assetMap: [String: Asset]) {
let items: [dydxPortfolioPositionItemViewModel] = positions.compactMap { position -> dydxPortfolioPositionItemViewModel? in
let item = Self.createViewModelItem(position: position, marketMap: marketMap, assetMap: assetMap, cache: cache)
cache[position.assetId] = item
let item = Self.createPositionViewModelItem(position: position,
marketMap: marketMap,
assetMap: assetMap,
positionsCache: positionsCache)
positionsCache[position.assetId] = item
return item
}

self.viewModel?.items = items
self.viewModel?.positionItems = items
}

static func createViewModelItem(position: SubaccountPosition, marketMap: [String: PerpetualMarket], assetMap: [String: Asset], cache: [String: dydxPortfolioPositionItemViewModel]? = nil) -> dydxPortfolioPositionItemViewModel? {
private func updatePendingPositions(pendingPositions: [SubaccountPendingPosition], marketMap: [String: PerpetualMarket], assetMap: [String: Asset]) {
let items: [dydxPortfolioPendingPositionsItemViewModel] = pendingPositions.compactMap { pendingPosition -> dydxPortfolioPendingPositionsItemViewModel? in
let item = Self.createPendingPositionsViewModelItem(pendingPosition: pendingPosition,
marketMap: marketMap,
assetMap: assetMap,
pendingPositionsCache: pendingPositionsCache)
pendingPositionsCache[pendingPosition.assetId] = item
return item
}

self.viewModel?.pendingPositionItems = items
}

static func createPendingPositionsViewModelItem(
pendingPosition: SubaccountPendingPosition,
marketMap: [String: PerpetualMarket],
assetMap: [String: Asset],
pendingPositionsCache: [String: dydxPortfolioPendingPositionsItemViewModel]? = nil
) -> dydxPortfolioPendingPositionsItemViewModel? {

guard let market = marketMap[pendingPosition.marketId],
let configs = market.configs,
let asset = assetMap[pendingPosition.assetId],
let margin = pendingPosition.equity?.current?.doubleValue,
margin != 0,
let marginFormatted = dydxFormatter.shared.dollar(number: margin, digits: 2)
else {
return nil
}

let viewOrdersAction: () -> Void = {
Router.shared?.navigate(to: RoutingRequest(path: "/market",
params: ["market": market.id,
"currentSection": "positions"]),
animated: true,
completion: nil)
}
let cancelOrdersAction: () -> Void = {
Router.shared?.navigate(to: RoutingRequest(path: "/trade/markets", params: ["market": market.id]), animated: true, completion: nil)
}

return dydxPortfolioPendingPositionsItemViewModel(marketLogoUrl: URL(string: asset.resources?.imageUrl ?? ""),
marketName: asset.name!,
margin: marginFormatted,
orderCount: pendingPosition.orderCount,
viewOrdersAction: viewOrdersAction,
cancelOrdersAction: cancelOrdersAction)
}

static func createPositionViewModelItem(position: SubaccountPosition, marketMap: [String: PerpetualMarket], assetMap: [String: Asset], positionsCache: [String: dydxPortfolioPositionItemViewModel]? = nil) -> dydxPortfolioPositionItemViewModel? {
guard let market = marketMap[position.id], let configs = market.configs, let asset = assetMap[position.assetId],
(position.size.current?.doubleValue ?? 0) != 0 else {
return nil
}

let item = cache?[position.assetId] ?? dydxPortfolioPositionItemViewModel()
let item = positionsCache?[position.assetId] ?? dydxPortfolioPositionItemViewModel()

let positionSize = abs(position.size.current?.doubleValue ?? 0)
item.size = dydxFormatter.shared.localFormatted(number: positionSize, digits: configs.displayStepSizeDecimals?.intValue ?? 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,16 +80,13 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
viewModel.amount?.onEdited = { amount in
AbacusStateManager.shared.adjustIsolatedMargin(input: amount, type: .amount)
}
viewModel.amount?.maxAction = {
AbacusStateManager.shared.adjustIsolatedMargin(input: "1", type: .amountpercent)
}

ctaButtonPresenter.viewModel?.ctaAction = { [weak self] in
self?.ctaButtonPresenter.viewModel?.ctaButtonState = .thinking
AbacusStateManager.shared.commitAdjustIsolatedMargin { [weak self] (_, error, _) in
self?.ctaButtonPresenter.viewModel?.ctaButtonState = .disabled()
if let error = error {
self?.viewModel?.submissionError = InlineAlertViewModel(.init(title: nil, body: error.message, level: .error))
self?.viewModel?.inlineAlert = InlineAlertViewModel(.init(title: nil, body: error.localizedMessage, level: .error))
return
} else {
Router.shared?.navigate(to: RoutingRequest(path: "/action/dismiss"), animated: true, completion: nil)
Expand Down Expand Up @@ -120,15 +117,8 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
self?.updateState(market: market, assetMap: assetMap)
self?.updateFields(input: input)
self?.updateForMarginDirection(input: input)
self?.updatePrePostValues(input: input)
self?.updatePrePostValues(input: input, market: market)
self?.updateLiquidationPrice(input: input, market: market)
self?.updateButtonState(input: input)
}
.store(in: &subscriptions)

AbacusStateManager.shared.state.adjustIsolatedMarginInput
.sink { [weak self] _ in
self?.viewModel?.submissionError = nil
}
.store(in: &subscriptions)
}
Expand All @@ -149,10 +139,65 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
}
}

private func updatePrePostValues(input: AdjustIsolatedMarginInput) {
// TODO: move this to abacus
/// locally validates the input
/// - Parameter input: input to validate
/// - Returns: the localization error string key if invalid input
private func validate(input: AdjustIsolatedMarginInput, market: PerpetualMarket) -> String? {
guard let amount = parser.asNumber(input.amount)?.doubleValue else { return nil }
switch input.type {
case IsolatedMarginAdjustmentType.add:
if let crossFreeCollateral = input.summary?.crossFreeCollateral?.doubleValue, amount >= crossFreeCollateral {
return "ERRORS.TRANSFER_MODAL.TRANSFER_MORE_THAN_FREE"
}
if let crossMarginUsageUpdated = input.summary?.crossMarginUsageUpdated?.doubleValue, crossMarginUsageUpdated > 1 {
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"
}
}
default:
break
}

return nil
}

private func clearPostValues() {
for receipt in [viewModel?.amountReceipt, viewModel?.buttonReceipt] {
for item in receipt?.receiptChangeItems ?? [] {
item.value.after = nil
}
}
}

private func updatePrePostValues(input: AdjustIsolatedMarginInput, market: PerpetualMarket) {
var crossReceiptItems = [dydxReceiptChangeItemView]()
var positionReceiptItems = [dydxReceiptChangeItemView]()

if let errorStringKey = validate(input: input, market: market) {
clearPostValues()
viewModel?.inlineAlert = InlineAlertViewModel(InlineAlertViewModel.Config(
title: nil,
body: DataLocalizer.localize(path: errorStringKey),
level: .error))
ctaButtonPresenter.viewModel?.ctaButtonState = .disabled()
return
} else {
ctaButtonPresenter.viewModel?.ctaButtonState = .enabled()
viewModel?.inlineAlert = nil
}

let crossFreeCollateral: AmountTextModel = .init(amount: input.summary?.crossFreeCollateral, unit: .dollar)
let crossFreeCollateralUpdated: AmountTextModel = .init(amount: input.summary?.crossFreeCollateralUpdated, unit: .dollar)
let crossFreeCollateralChange: AmountChangeModel = .init(
Expand Down Expand Up @@ -225,14 +270,6 @@ private class dydxAdjustMarginInputViewPresenter: HostedViewPresenter<dydxAdjust
}
}

private func updateButtonState(input: AdjustIsolatedMarginInput) {
if parser.asNumber(input.amount)?.doubleValue ?? 0 > 0 {
self.ctaButtonPresenter.viewModel?.ctaButtonState = .enabled()
} else {
self.ctaButtonPresenter.viewModel?.ctaButtonState = .disabled()
}
}

private func updateFields(input: AdjustIsolatedMarginInput) {
viewModel?.amount?.value = dydxFormatter.shared.raw(number: parser.asNumber(input.amount), digits: 2)
}
Expand Down
11 changes: 11 additions & 0 deletions dydx/dydxStateManager/dydxStateManager/AbacusState+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ public final class AbacusState {
.eraseToAnyPublisher()
}

public var selectedSubaccountPendingPositions: AnyPublisher<[SubaccountPendingPosition], Never> {
selectedSubaccount
.compactMap { subaccount in
subaccount?.pendingPositions
}
.prepend([])
.removeDuplicates()
.share()
.eraseToAnyPublisher()
}

public var selectedSubaccountOrders: AnyPublisher<[SubaccountOrder], Never> {
selectedSubaccount
.compactMap { subaccount in
Expand Down
2 changes: 1 addition & 1 deletion dydx/dydxStateManager/dydxStateManager/Models+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Abacus
import Utilities

extension ParsingError: Error {
var localizedDescription: String? {
public var localizedMessage: String? {
if let stringKey = stringKey {
return DataLocalizer.localize(path: stringKey)
}
Expand Down
Loading

0 comments on commit a66f9cc

Please sign in to comment.