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

fix: estimated execution price #5308

Draft
wants to merge 4 commits into
base: fix/distance-to-market
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion apps/cowswap-frontend/src/common/hooks/useSafeMemo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function useSafeDeps(deps: unknown[]): unknown[] {
if (dep instanceof Token) return dep.address.toLowerCase()
if (dep instanceof CurrencyAmount) return dep.toExact() + dep.currency.symbol + dep.currency.chainId
if (dep instanceof Percent) return dep.toFixed(6)
if (dep instanceof Price) return dep.toFixed(12) + dep.baseCurrency.symbol + dep.quoteCurrency.symbol
if (dep instanceof Price) return dep.toSignificant(10) + dep.baseCurrency.symbol + dep.quoteCurrency.symbol

return dep
})
Expand Down
127 changes: 74 additions & 53 deletions apps/cowswap-frontend/src/legacy/state/orders/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { LatestAppDataDocVersion } from '@cowprotocol/app-data'
import { ONE_HUNDRED_PERCENT, PENDING_ORDERS_BUFFER, ZERO_FRACTION } from '@cowprotocol/common-const'
import { bpsToPercent, buildPriceFromCurrencyAmounts, isSellOrder } from '@cowprotocol/common-utils'
import { bpsToPercent, buildPriceFromCurrencyAmounts, getWrappedToken, isSellOrder } from '@cowprotocol/common-utils'
import { EnrichedOrder, OrderKind, OrderStatus } from '@cowprotocol/cow-sdk'
import { UiOrderType } from '@cowprotocol/types'
import { Currency, CurrencyAmount, Percent, Price, Token } from '@uniswap/sdk-core'
Expand All @@ -12,7 +12,6 @@ import { decodeAppData } from 'modules/appData/utils/decodeAppData'
import { getIsComposableCowParentOrder } from 'utils/orderUtils/getIsComposableCowParentOrder'
import { getOrderSurplus } from 'utils/orderUtils/getOrderSurplus'
import { getUiOrderType } from 'utils/orderUtils/getUiOrderType'
import type { ParsedOrder } from 'utils/orderUtils/parseOrder'

import { Order, updateOrder, UpdateOrderParams as UpdateOrderParamsAction } from './actions'
import { OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE } from './consts'
Expand All @@ -35,7 +34,10 @@ export type OrderTransitionStatus =
* or all buyAmount has been bought, for buy orders
*/
export function isOrderFulfilled(
order: Pick<EnrichedOrder, 'buyAmount' | 'sellAmount' | 'executedBuyAmount' | 'executedSellAmountBeforeFees' | 'kind'>
order: Pick<
EnrichedOrder,
'buyAmount' | 'sellAmount' | 'executedBuyAmount' | 'executedSellAmountBeforeFees' | 'kind'
>,
): boolean {
const { buyAmount, sellAmount, executedBuyAmount, executedSellAmountBeforeFees, kind } = order

Expand Down Expand Up @@ -98,7 +100,7 @@ export function classifyOrder(
| 'kind'
| 'signingScheme'
| 'status'
> | null
> | null,
): OrderTransitionStatus {
if (!order) {
console.debug(`[state::orders::classifyOrder] unknown order`)
Expand Down Expand Up @@ -133,7 +135,7 @@ export function classifyOrder(
export function isOrderUnfillable(
order: Order,
orderPrice: Price<Currency, Currency>,
executionPrice: Price<Currency, Currency>
executionPrice: Price<Currency, Currency>,
): boolean {
// Calculate the percentage of the current price in regards to the order price
const percentageDifference = ONE_HUNDRED_PERCENT.subtract(executionPrice.divide(orderPrice))
Expand All @@ -143,7 +145,7 @@ export function isOrderUnfillable(
orderPrice.toSignificant(10),
executionPrice.toSignificant(10),
`${percentageDifference.toFixed(4)}%`,
percentageDifference.greaterThan(OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE)
percentageDifference.greaterThan(OUT_OF_MARKET_PRICE_DELTA_PERCENTAGE),
)

// Example. Consider the pair X-Y:
Expand Down Expand Up @@ -175,7 +177,7 @@ export function getOrderMarketPrice(order: Order, quotedAmount: string, feeAmoun
order.outputToken,
// For sell orders, the market price has the fee subtracted from the sell amount
JSBI.subtract(JSBI.BigInt(remainingAmount), JSBI.BigInt(feeAmount)),
quotedAmount
quotedAmount,
)
}

Expand Down Expand Up @@ -221,32 +223,75 @@ const EXECUTION_PRICE_FEE_COEFFICIENT = new Percent(5, 100)
* @param fillPrice AKA MarketPrice
* @param fee Estimated fee in inputToken atoms, as string
*/
export function getEstimatedExecutionPrice(
order: undefined, // there's no order when calling this way
fillPrice: Price<Currency, Currency>,
fee: string,
inputAmount: CurrencyAmount<Currency>,
outputAmount: CurrencyAmount<Currency>,
kind: OrderKind,
fullAppData: EnrichedOrder['fullAppData'],
): Price<Currency, Currency> | null
export function getEstimatedExecutionPrice(
order: Order,
fillPrice: Price<Currency, Currency>,
fee: string
fee: string,
): Price<Currency, Currency> | null
export function getEstimatedExecutionPrice(
order: Order | undefined,
fillPrice: Price<Currency, Currency>,
fee: string,
inputAmount?: CurrencyAmount<Currency>,
outputAmount?: CurrencyAmount<Currency>,
kind?: OrderKind,
fullAppData?: EnrichedOrder['fullAppData'],
): Price<Currency, Currency> | null {
// Build CurrencyAmount and Price instances
const feeAmount = CurrencyAmount.fromRawAmount(order.inputToken, fee)
// Take partner fee into account when calculating the limit price
const limitPrice = getOrderLimitPriceWithPartnerFee(order)
let inputToken: Token
let outputToken: Token
let limitPrice: Price<Currency, Currency>
let sellAmount: string

if (order) {
inputToken = order.inputToken
outputToken = order.outputToken
limitPrice = getOrderLimitPriceWithPartnerFee(order)

if (getUiOrderType(order) === UiOrderType.SWAP) {
return limitPrice
}

if (getUiOrderType(order) === UiOrderType.SWAP) {
return limitPrice
}
// Parent TWAP order, ignore
if (getIsComposableCowParentOrder(order)) {
return null
}

// Parent TWAP order, ignore
if (getIsComposableCowParentOrder(order)) {
return null
sellAmount = getRemainderAmountsWithoutSurplus(order).sellAmount
} else {
sellAmount = inputAmount!.quotient.toString()
inputToken = getWrappedToken(inputAmount!.currency)
outputToken = getWrappedToken(outputAmount!.currency)
limitPrice = getOrderLimitPriceWithPartnerFee({
inputToken,
outputToken,
sellAmount,
buyAmount: outputAmount!.quotient.toString(),
kind: kind as OrderKind,
fullAppData,
})
}

const feeAmount = CurrencyAmount.fromRawAmount(inputToken, fee)

// Build CurrencyAmount and Price instances
// const feeAmount = CurrencyAmount.fromRawAmount(order.inputToken, feeAmount)
// Take partner fee into account when calculating the limit price

// Check what's left to sell, discounting the surplus, if any
const { sellAmount } = getRemainderAmountsWithoutSurplus(order)
const remainingSellAmount = CurrencyAmount.fromRawAmount(order.inputToken, sellAmount)
const remainingSellAmount = CurrencyAmount.fromRawAmount(inputToken, sellAmount)

// When fee > amount, return 0 price
if (!remainingSellAmount.greaterThan(ZERO_FRACTION)) {
return new Price(order.inputToken, order.outputToken, '0', '0')
return new Price(inputToken, outputToken, '0', '0')
}

const feeWithMargin = feeAmount.add(feeAmount.multiply(EXECUTION_PRICE_FEE_COEFFICIENT))
Expand All @@ -255,7 +300,7 @@ export function getEstimatedExecutionPrice(

// Just in case when the denominator is <= 0 after subtraction the fee
if (!denominator.greaterThan(ZERO_FRACTION)) {
return new Price(order.inputToken, order.outputToken, '0', '0')
return new Price(inputToken, outputToken, '0', '0')
}

/**
Expand All @@ -267,37 +312,11 @@ export function getEstimatedExecutionPrice(
* Fee with margin: 0.002 + 5% = 0.0021 WETH
* Executes at: 182000 / (100 - 0.0021) = 1820.038 USDC per 1 WETH
*/
const feasibleExecutionPrice = new Price(
order.inputToken,
order.outputToken,
denominator.quotient,
numerator.quotient
)
const feasibleExecutionPrice = new Price(inputToken, outputToken, denominator.quotient, numerator.quotient)

// Pick the MAX between FEP and FP
const estimatedExecutionPrice = fillPrice.greaterThan(feasibleExecutionPrice) ? fillPrice : feasibleExecutionPrice

// TODO: remove debug statement
console.debug(`getEstimatedExecutionPrice`, {
'Amount (A)':
remainingSellAmount.toFixed(remainingSellAmount.currency.decimals) + ' ' + remainingSellAmount.currency.symbol,
'Fee (F)': feeAmount.toFixed(feeAmount.currency.decimals) + ' ' + feeAmount.currency.symbol,
'Limit Price (LP)': `${limitPrice.toFixed(8)} ${limitPrice.quoteCurrency.symbol} per ${
limitPrice.baseCurrency.symbol
} (${limitPrice.numerator.toString()}/${limitPrice.denominator.toString()})`,
'Feasible Execution Price (FEP)': `${feasibleExecutionPrice.toFixed(18)} ${
feasibleExecutionPrice.quoteCurrency.symbol
} per ${feasibleExecutionPrice.baseCurrency.symbol}`,
'Fill Price (FP)': `${fillPrice.toFixed(8)} ${fillPrice.quoteCurrency.symbol} per ${
fillPrice.baseCurrency.symbol
} (${fillPrice.numerator.toString()}/${fillPrice.denominator.toString()})`,
'Est.Execution Price (EEP)': `${estimatedExecutionPrice.toFixed(8)} ${
estimatedExecutionPrice.quoteCurrency.symbol
} per ${estimatedExecutionPrice.baseCurrency.symbol}`,
id: order.id.slice(0, 8),
class: order.class,
})

return estimatedExecutionPrice
}

Expand Down Expand Up @@ -378,22 +397,24 @@ export function partialOrderUpdate({ chainId, order, isSafeWallet }: UpdateOrder
}

export function getOrderVolumeFee(
fullAppData: EnrichedOrder['fullAppData']
fullAppData: EnrichedOrder['fullAppData'],
): LatestAppDataDocVersion['metadata']['partnerFee'] | undefined {
const appData = decodeAppData(fullAppData) as LatestAppDataDocVersion

return appData?.metadata?.partnerFee
}

export function getOrderLimitPriceWithPartnerFee(order: Order | ParsedOrder): Price<Currency, Currency> {
type LimitPriceOrder = Pick<Order, 'inputToken' | 'outputToken' | 'sellAmount' | 'buyAmount' | 'kind' | 'fullAppData'>

export function getOrderLimitPriceWithPartnerFee(order: LimitPriceOrder): Price<Currency, Currency> {
const inputAmount = CurrencyAmount.fromRawAmount(order.inputToken, order.sellAmount.toString())
const outputAmount = CurrencyAmount.fromRawAmount(order.outputToken, order.buyAmount.toString())

const { inputCurrencyAmount, outputCurrencyAmount } = getOrderAmountsWithPartnerFee(
order.fullAppData,
inputAmount,
outputAmount,
isSellOrder(order.kind)
isSellOrder(order.kind),
)

return buildPriceFromCurrencyAmounts(inputCurrencyAmount, outputCurrencyAmount)
Expand All @@ -403,7 +424,7 @@ function getOrderAmountsWithPartnerFee(
fullAppData: EnrichedOrder['fullAppData'],
sellAmount: CurrencyAmount<Token>,
buyAmount: CurrencyAmount<Token>,
isSellOrder: boolean
isSellOrder: boolean,
): { inputCurrencyAmount: CurrencyAmount<Token>; outputCurrencyAmount: CurrencyAmount<Token> } {
const volumeFee = getOrderVolumeFee(fullAppData)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
import { useAtomValue, useSetAtom } from 'jotai'
import { useEffect } from 'react'

import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils'

import { getEstimatedExecutionPrice } from 'legacy/state/orders/utils'

import { useAppData } from 'modules/appData'
import { useLimitOrdersDerivedState } from 'modules/limitOrders/hooks/useLimitOrdersDerivedState'
import { executionPriceAtom } from 'modules/limitOrders/state/executionPriceAtom'
import { limitRateAtom } from 'modules/limitOrders/state/limitRateAtom'

import { calculateExecutionPrice } from 'utils/orderUtils/calculateExecutionPrice'
import { useSafeEffect } from 'common/hooks/useSafeMemo'

export function ExecutionPriceUpdater() {
const { marketRate, feeAmount } = useAtomValue(limitRateAtom)
const { inputCurrencyAmount, outputCurrencyAmount, orderKind } = useLimitOrdersDerivedState()
const setExecutionPrice = useSetAtom(executionPriceAtom)
const { fullAppData } = useAppData() || {}

const inputToken = inputCurrencyAmount?.currency && getWrappedToken(inputCurrencyAmount.currency)
const outputToken = outputCurrencyAmount?.currency && getWrappedToken(outputCurrencyAmount.currency)

const marketPrice =
marketRate && inputToken && outputToken && FractionUtils.toPrice(marketRate, inputToken, outputToken)

const fee = feeAmount?.quotient.toString()

const price = calculateExecutionPrice({
inputCurrencyAmount,
outputCurrencyAmount,
feeAmount,
marketRate,
orderKind,
})
const price =
marketPrice &&
fee &&
getEstimatedExecutionPrice(
undefined,
marketPrice,
fee,
inputCurrencyAmount,
outputCurrencyAmount,
orderKind,
fullAppData,
)

useEffect(() => {
setExecutionPrice(price)
useSafeEffect(() => {
price && price.greaterThan(0) && setExecutionPrice(price)
}, [price, setExecutionPrice])

return null
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { useSetAtom } from 'jotai'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'

import { FractionUtils, getWrappedToken } from '@cowprotocol/common-utils'
import { Fraction, Token } from '@uniswap/sdk-core'
import { CurrencyAmount, Fraction, Token } from '@uniswap/sdk-core'

import { Nullish } from 'types'

import { updateLimitRateAtom } from 'modules/limitOrders/state/limitRateAtom'
import { useDerivedTradeState } from 'modules/trade/hooks/useDerivedTradeState'
import { useTradeQuote } from 'modules/tradeQuote'
import { useUsdPrice } from 'modules/usdAmount/hooks/useUsdPrice'

import { useSafeEffect } from 'common/hooks/useSafeMemo'


export function QuoteObserverUpdater() {
const state = useDerivedTradeState()

Expand All @@ -23,10 +27,21 @@ export function QuoteObserverUpdater() {

const { price, isLoading } = useSpotPrice(inputToken, outputToken)

useEffect(() => {
// Update market rate based on spot prices
useSafeEffect(() => {
updateLimitRateState({ marketRate: price, isLoadingMarketRate: isLoading })
}, [price, isLoading, updateLimitRateState])

const { response } = useTradeQuote()
const { quote } = response || {}
const { feeAmount: feeAmountRaw } = quote || {}
const feeAmount = inputCurrency && feeAmountRaw ? CurrencyAmount.fromRawAmount(inputCurrency, feeAmountRaw) : null

// Update fee amount based on quote response
useSafeEffect(() => {
updateLimitRateState({ feeAmount })
}, [feeAmount, updateLimitRateState])

return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface ExecutionPriceParams {
*/
export function convertAmountToCurrency(
amount: CurrencyAmount<Currency>,
targetCurrency: Currency
targetCurrency: Currency,
): CurrencyAmount<Currency> {
const { numerator, denominator } = amount

Expand All @@ -34,12 +34,12 @@ export function convertAmountToCurrency(
const decimalsDiff = Math.abs(inputDecimals - outputDecimals)
const decimalsDiffAmount = rawToTokenAmount(1, decimalsDiff)

const fixedNumenator =
const fixedNumerator =
inputDecimals < outputDecimals
? JSBI.multiply(numerator, decimalsDiffAmount)
: JSBI.divide(numerator, decimalsDiffAmount)

return CurrencyAmount.fromFractionalAmount(targetCurrency, fixedNumenator, denominator)
return CurrencyAmount.fromFractionalAmount(targetCurrency, fixedNumerator, denominator)
}

export function calculateExecutionPrice(params: ExecutionPriceParams): Price<Currency, Currency> | null {
Expand All @@ -62,7 +62,7 @@ export function calculateExecutionPrice(params: ExecutionPriceParams): Price<Cur
baseAmount: inputCurrencyAmount,
quoteAmount: convertAmountToCurrency(
inputCurrencyAmount.subtract(feeAmount).multiply(marketRateFixed),
outputCurrencyAmount.currency
outputCurrencyAmount.currency,
),
})
const marketPrice = isInverted ? marketPriceRaw.invert() : marketPriceRaw
Expand Down
Loading