Skip to content

Commit

Permalink
invert spot price (#107)
Browse files Browse the repository at this point in the history
* refactor!: invert spot price in quotes

* remove things

* add telemetry

* lint
  • Loading branch information
p0mvn authored Mar 16, 2024
1 parent 3cb3ceb commit 4e63f77
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 36 deletions.
6 changes: 6 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ const docTemplate = `{
"name": "humanDenoms",
"in": "query",
"required": true
},
{
"type": "boolean",
"description": "Boolean flag indicating whether to apply exponents to the spot price. False by default.",
"name": "applyExponents",
"in": "query"
}
],
"responses": {
Expand Down
6 changes: 6 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@
"name": "humanDenoms",
"in": "query",
"required": true
},
{
"type": "boolean",
"description": "Boolean flag indicating whether to apply exponents to the spot price. False by default.",
"name": "applyExponents",
"in": "query"
}
],
"responses": {
Expand Down
5 changes: 5 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ paths:
name: humanDenoms
required: true
type: boolean
- description: Boolean flag indicating whether to apply exponents to the spot
price. False by default.
in: query
name: applyExponents
type: boolean
produces:
- application/json
responses:
Expand Down
1 change: 1 addition & 0 deletions domain/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type Route interface {
// Note that it mutates the route.
// Computes the spot price of the route.
// Returns the spot price before swap and effective spot price.
// The token in is the base token and the token out is the quote token.
PrepareResultPools(ctx context.Context, tokenIn sdk.Coin) ([]sqsdomain.RoutablePool, osmomath.Dec, osmomath.Dec, error)

String() string
Expand Down
33 changes: 31 additions & 2 deletions router/delivery/http/router_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ type RouterHandler struct {

const routerResource = "/router"

var (
oneDec = osmomath.OneDec()
)

func formatRouterResource(resource string) string {
return routerResource + resource
}
Expand Down Expand Up @@ -55,6 +59,7 @@ func NewRouterHandler(e *echo.Echo, us mvc.RouterUsecase, tu mvc.TokensUsecase,
// @Param tokenOutDenom query string true "String representing the denom of the token out."
// @Param singleRoute query bool false "Boolean flag indicating whether to return single routes (no splits). False (splits enabled) by default."
// @Param humanDenoms query bool true "Boolean flag indicating whether the given denoms are human readable or not. Human denoms get converted to chain internally"
// @Param applyExponents query bool false "Boolean flag indicating whether to apply exponents to the spot price. False by default."
// @Success 200 {object} domain.Quote "The computed best route quote"
// @Router /router/quote [get]
func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) {
Expand All @@ -69,6 +74,15 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) {
}
}

shouldApplyExponentsStr := c.QueryParam("applyExponents")
shouldApplyExponents := false
if shouldApplyExponentsStr != "" {
shouldApplyExponents, err = strconv.ParseBool(shouldApplyExponentsStr)
if err != nil {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}
}

tokenOutDenom, tokenIn, err := getValidRoutingParameters(c)
if err != nil {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
Expand All @@ -93,7 +107,10 @@ func (a *RouterHandler) GetOptimalQuote(c echo.Context) (err error) {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}

scalingFactor := a.getSpotPriceScalingFactor(ctx, tokenInDenom, tokenOutDenom)
scalingFactor := oneDec
if shouldApplyExponents {
scalingFactor = a.getSpotPriceScalingFactor(ctx, tokenInDenom, tokenOutDenom)
}

_, _, err = quote.PrepareResult(ctx, scalingFactor)
if err != nil {
Expand All @@ -118,6 +135,15 @@ func (a *RouterHandler) GetDirectCustomQuote(c echo.Context) error {
return c.JSON(http.StatusBadRequest, domain.ResponseError{Message: "poolID is required"})
}

shouldApplyExponentsStr := c.QueryParam("applyExponents")
shouldApplyExponents := false
if shouldApplyExponentsStr != "" {
shouldApplyExponents, err = strconv.ParseBool(shouldApplyExponentsStr)
if err != nil {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}
}

poolID, err := strconv.ParseUint(poolIDStr, 10, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, domain.ResponseError{Message: err.Error()})
Expand All @@ -129,7 +155,10 @@ func (a *RouterHandler) GetDirectCustomQuote(c echo.Context) error {
return c.JSON(domain.GetStatusCode(err), domain.ResponseError{Message: err.Error()})
}

scalingFactor := a.getSpotPriceScalingFactor(ctx, tokenIn.Denom, tokenOutDenom)
scalingFactor := oneDec
if shouldApplyExponents {
scalingFactor = a.getSpotPriceScalingFactor(ctx, tokenIn.Denom, tokenOutDenom)
}

_, _, err = quote.PrepareResult(ctx, scalingFactor)
if err != nil {
Expand Down
28 changes: 14 additions & 14 deletions router/usecase/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import (
)

type quoteImpl struct {
AmountIn sdk.Coin "json:\"amount_in\""
AmountOut osmomath.Int "json:\"amount_out\""
Route []domain.SplitRoute "json:\"route\""
EffectiveFee osmomath.Dec "json:\"effective_fee\""
PriceImpact osmomath.Dec "json:\"price_impact\""
InOutSpotPrice osmomath.Dec "json:\"in_out_spot_price\""
AmountIn sdk.Coin "json:\"amount_in\""
AmountOut osmomath.Int "json:\"amount_out\""
Route []domain.SplitRoute "json:\"route\""
EffectiveFee osmomath.Dec "json:\"effective_fee\""
PriceImpact osmomath.Dec "json:\"price_impact\""
InBaseOutQuoteSpotPrice osmomath.Dec "json:\"in_base_out_quote_spot_price\""
}

var (
Expand All @@ -40,8 +40,8 @@ func (q *quoteImpl) PrepareResult(ctx context.Context, scalingFactor osmomath.De
totalAmountIn := q.AmountIn.Amount.ToLegacyDec()
totalFeeAcrossRoutes := osmomath.ZeroDec()

totalSpotPriceInOverOut := osmomath.ZeroDec()
totalEffectiveSpotPriceInOverOut := osmomath.ZeroDec()
totalSpotPriceInBaseOutQuote := osmomath.ZeroDec()
totalEffectiveSpotPriceInBaseOutQuote := osmomath.ZeroDec()

resultRoutes := make([]domain.SplitRoute, 0, len(q.Route))

Expand All @@ -65,13 +65,13 @@ func (q *quoteImpl) PrepareResult(ctx context.Context, scalingFactor osmomath.De
// Update the spread factor pro-rated by the amount in
totalFeeAcrossRoutes.AddMut(routeTotalFee.MulMut(routeAmountInFraction))

newPools, routeSpotPriceInOverOut, effectiveSpotPriceInOverOut, err := curRoute.PrepareResultPools(ctx, q.AmountIn)
newPools, routeSpotPriceInBaseOutQuote, effectiveSpotPriceInBaseOutQuote, err := curRoute.PrepareResultPools(ctx, q.AmountIn)
if err != nil {
return nil, osmomath.Dec{}, err
}

totalSpotPriceInOverOut = totalSpotPriceInOverOut.AddMut(routeSpotPriceInOverOut.MulMut(routeAmountInFraction))
totalEffectiveSpotPriceInOverOut = totalEffectiveSpotPriceInOverOut.AddMut(effectiveSpotPriceInOverOut.MulMut(routeAmountInFraction))
totalSpotPriceInBaseOutQuote = totalSpotPriceInBaseOutQuote.AddMut(routeSpotPriceInBaseOutQuote.MulMut(routeAmountInFraction))
totalEffectiveSpotPriceInBaseOutQuote = totalEffectiveSpotPriceInBaseOutQuote.AddMut(effectiveSpotPriceInBaseOutQuote.MulMut(routeAmountInFraction))

resultRoutes = append(resultRoutes, &RouteWithOutAmount{
RouteImpl: route.RouteImpl{
Expand All @@ -84,13 +84,13 @@ func (q *quoteImpl) PrepareResult(ctx context.Context, scalingFactor osmomath.De
}

// Calculate price impact
if !totalSpotPriceInOverOut.IsZero() {
q.PriceImpact = totalEffectiveSpotPriceInOverOut.Quo(totalSpotPriceInOverOut).SubMut(one)
if !totalSpotPriceInBaseOutQuote.IsZero() {
q.PriceImpact = totalEffectiveSpotPriceInBaseOutQuote.Quo(totalSpotPriceInBaseOutQuote).SubMut(one)
}

q.EffectiveFee = totalFeeAcrossRoutes
q.Route = resultRoutes
q.InOutSpotPrice = totalSpotPriceInOverOut.Mul(scalingFactor)
q.InBaseOutQuoteSpotPrice = totalSpotPriceInBaseOutQuote

return q.Route, q.EffectiveFee, nil
}
Expand Down
8 changes: 3 additions & 5 deletions router/usecase/quote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,18 +262,17 @@ func (s *RouterTestSuite) TestPrepareResult_PriceImpact() {
s.Require().NoError(err)

// Compute spot price before swap
spotPriceInOverOut, err := poolOne.SpotPrice(sdk.Context{}, ETH, USDC)
spotPriceInBaseOutQuote, err := poolOne.SpotPrice(sdk.Context{}, USDC, ETH)
s.Require().NoError(err)

coinIn := sdk.NewCoin(ETH, totalInAmount)

// Compute expected effective price
tokenInAfterFee, _ := poolmanager.CalcTakerFeeExactIn(coinIn, DefaultTakerFee)
// .Sub(spreadFactor.TruncateDec())
expectedEffectivePrice := tokenInAfterFee.Amount.ToLegacyDec().Quo(totalOutAmount.ToLegacyDec())
expectedEffectivePrice := totalOutAmount.ToLegacyDec().Quo(tokenInAfterFee.Amount.ToLegacyDec())

// Compute expected price impact
expectedPriceImpact := expectedEffectivePrice.Quo(spotPriceInOverOut.Dec()).Sub(osmomath.OneDec())
expectedPriceImpact := expectedEffectivePrice.Quo(spotPriceInBaseOutQuote.Dec()).Sub(osmomath.OneDec())

// Setup quote
testQuote := &usecase.QuoteImpl{
Expand Down Expand Up @@ -307,7 +306,6 @@ func (s *RouterTestSuite) TestPrepareResult_PriceImpact() {

// Validate price impact.
s.Require().Equal(expectedPriceImpact.String(), testQuote.GetPriceImpact().String())

}

// validateRoutes validates that the given routes are equal.
Expand Down
32 changes: 25 additions & 7 deletions router/usecase/route/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/osmosis-labs/sqs/sqsdomain"
"github.com/prometheus/client_golang/prometheus"

"github.com/osmosis-labs/sqs/domain"
"github.com/osmosis-labs/sqs/router/usecase/pools"
Expand All @@ -26,6 +27,16 @@ type RouteImpl struct {
HasGeneralizedCosmWasmPool bool "json:\"has-cw-pool\""
}

var (
spotPriceErrorResultCounter = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "sqs_routes_spot_price_error_total",
Help: "Spot price error when preparing result pools",
},
[]string{"token_in", "cur_token_out_denom", "route_token_out_denom"},
)
)

// PrepareResultPools implements domain.Route.
// Strips away unnecessary fields from each pool in the route,
// leaving only the data needed by client
Expand All @@ -38,19 +49,26 @@ type RouteImpl struct {
// - Taker Fee
// Note that it mutates the route.
// Returns spot price before swap and the effective spot price
// with token in as base and token out as quote.
func (r RouteImpl) PrepareResultPools(ctx context.Context, tokenIn sdk.Coin) ([]sqsdomain.RoutablePool, osmomath.Dec, osmomath.Dec, error) {
var (
routeSpotPriceInOverOut = osmomath.OneDec()
effectiveSpotPriceInOverOut = osmomath.OneDec()
routeSpotPriceInBaseOutQuote = osmomath.OneDec()
effectiveSpotPriceInBaseOutQuote = osmomath.OneDec()
)

newPools := make([]sqsdomain.RoutablePool, 0, len(r.Pools))

for _, pool := range r.Pools {
// Compute spot price before swap.
spotPriceInOverOut, err := pool.CalcSpotPrice(ctx, pool.GetTokenOutDenom(), tokenIn.Denom)
spotPriceInBaseOutQuote, err := pool.CalcSpotPrice(ctx, tokenIn.Denom, pool.GetTokenOutDenom())
if err != nil {
spotPriceInOverOut = osmomath.ZeroBigDec()
// We don't want to fail the entire quote if one pool fails to calculate spot price.
// This might cause miestimaions downsream but we a
spotPriceInBaseOutQuote = osmomath.ZeroBigDec()

// Increment the counter for the error
routeTokenOutDenom := r.Pools[len(r.Pools)-1].GetTokenOutDenom()
spotPriceErrorResultCounter.WithLabelValues(tokenIn.Denom, pool.GetTokenOutDenom(), routeTokenOutDenom).Inc()
}

// Charge taker fee
Expand All @@ -62,10 +80,10 @@ func (r RouteImpl) PrepareResultPools(ctx context.Context, tokenIn sdk.Coin) ([]
}

// Update effective spot price
effectiveSpotPriceInOverOut.MulMut(tokenIn.Amount.ToLegacyDec().QuoMut(tokenOut.Amount.ToLegacyDec()))
effectiveSpotPriceInBaseOutQuote.MulMut(tokenOut.Amount.ToLegacyDec().QuoMut(tokenIn.Amount.ToLegacyDec()))

// Note, in the future we may want to increase the precision of the spot price
routeSpotPriceInOverOut.MulMut(spotPriceInOverOut.Dec())
routeSpotPriceInBaseOutQuote.MulMut(spotPriceInBaseOutQuote.Dec())

newPool := pools.NewRoutableResultPool(
pool.GetId(),
Expand All @@ -79,7 +97,7 @@ func (r RouteImpl) PrepareResultPools(ctx context.Context, tokenIn sdk.Coin) ([]

tokenIn = tokenOut
}
return newPools, routeSpotPriceInOverOut, effectiveSpotPriceInOverOut, nil
return newPools, routeSpotPriceInBaseOutQuote, effectiveSpotPriceInBaseOutQuote, nil
}

// GetPools implements Route.
Expand Down
16 changes: 8 additions & 8 deletions router/usecase/route/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,13 @@ func (s *RouterTestSuite) TestPrepareResultPools() {
defaultTokenIn := sdk.NewCoin(DenomTwo, DefaultAmt0)

// Estimate balancer pool spot price
balancerPoolSpotPriceInOverOut, err := balancerPool.SpotPrice(sdk.Context{}, DenomTwo, DenomOne)
balancerPoolSpotPriceInOverOut, err := balancerPool.SpotPrice(sdk.Context{}, DenomOne, DenomTwo)
s.Require().NoError(err)

// Estimate balancer pool effective spot price
expectedAmountOutBalancer, err := balancerPool.CalcOutAmtGivenIn(sdk.Context{}, sdk.NewCoins(defaultTokenIn), DenomOne, DefaultSpreadFactor)
s.Require().NoError(err)
expectedEffectivePriceBalancerInOverOut := defaultTokenIn.Amount.ToLegacyDec().Quo(expectedAmountOutBalancer.Amount.ToLegacyDec())
expectedEffectivePriceBalancerInOverOut := expectedAmountOutBalancer.Amount.ToLegacyDec().Quo(defaultTokenIn.Amount.ToLegacyDec())

// Prepare trasnmuter pool
transmuter := s.PrepareCustomTransmuterPoolCustomProject(s.TestAccs[0], []string{DenomOne, DenomThree}, "sqs", "scripts")
Expand All @@ -121,7 +121,7 @@ func (s *RouterTestSuite) TestPrepareResultPools() {

expectedPools []sqsdomain.RoutablePool

expectedSpotPriceInOverOut osmomath.Dec
expectedSpotPriceInBaseOutQuote osmomath.Dec

expectedEffectiveSpotPriceInOverOut osmomath.Dec
}{
Expand All @@ -132,7 +132,7 @@ func (s *RouterTestSuite) TestPrepareResultPools() {

expectedPools: []sqsdomain.RoutablePool{},

expectedSpotPriceInOverOut: osmomath.OneDec(),
expectedSpotPriceInBaseOutQuote: osmomath.OneDec(),
expectedEffectiveSpotPriceInOverOut: osmomath.OneDec(),
},
"single balancer pool in route": {
Expand All @@ -156,7 +156,7 @@ func (s *RouterTestSuite) TestPrepareResultPools() {
},

// Balancer is the only pool in the route so its spot price is expected.
expectedSpotPriceInOverOut: balancerPoolSpotPriceInOverOut.Dec(),
expectedSpotPriceInBaseOutQuote: balancerPoolSpotPriceInOverOut.Dec(),
expectedEffectiveSpotPriceInOverOut: expectedEffectivePriceBalancerInOverOut,
},
"balancer and transmuter in route": {
Expand Down Expand Up @@ -189,7 +189,7 @@ func (s *RouterTestSuite) TestPrepareResultPools() {

// Transmuter has spot price of one. Therefore, the spot price of the route
// should be the same as the spot price of the balancer pool.
expectedSpotPriceInOverOut: balancerPoolSpotPriceInOverOut.Dec(),
expectedSpotPriceInBaseOutQuote: balancerPoolSpotPriceInOverOut.Dec(),
expectedEffectiveSpotPriceInOverOut: expectedEffectivePriceBalancerInOverOut,
},
}
Expand All @@ -199,10 +199,10 @@ func (s *RouterTestSuite) TestPrepareResultPools() {
s.Run(name, func() {

// Note: token in is chosen arbitrarily since it is irrelevant for this test
actualPools, spotPriceBeforeInOverOut, _, err := tc.route.PrepareResultPools(context.TODO(), tc.tokenIn)
actualPools, spotPriceBeforeInBaseOutQuote, _, err := tc.route.PrepareResultPools(context.TODO(), tc.tokenIn)
s.Require().NoError(err)

s.Require().Equal(tc.expectedSpotPriceInOverOut, spotPriceBeforeInOverOut)
s.Require().Equal(tc.expectedSpotPriceInBaseOutQuote, spotPriceBeforeInBaseOutQuote)

s.ValidateRoutePools(tc.expectedPools, actualPools)
})
Expand Down

0 comments on commit 4e63f77

Please sign in to comment.