Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unopened isolated positions pt1 #191

Merged
merged 16 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
Comment on lines -83 to -85
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

per corey's guidance, removing max button


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()
}
}

Comment on lines -228 to -235
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was moved to validate fxn

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
Loading