Skip to content

Commit

Permalink
feat: add price impact to sqs (osmosis-labs#7108)
Browse files Browse the repository at this point in the history
* feat: add price impact to sqs

* fix test

* Update TestPrepareResultPools for route

* test quote's PrepareResult
  • Loading branch information
p0mvn authored Dec 13, 2023
1 parent 5475345 commit 532e8e2
Show file tree
Hide file tree
Showing 15 changed files with 328 additions and 31 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ sqs-validate-cl-state:
sqs-quote-compare:
ingest/sqs/scripts/quote.sh "http://localhost:9092"

sqs-quote-compare-stage:
ingest/sqs/scripts/quote.sh "http://165.227.168.61"

# Updates go tests with the latest mainnet state
# Make sure that the node is running locally
sqs-update-mainnet-state:
Expand Down
38 changes: 36 additions & 2 deletions ingest/sqs/domain/mocks/pool_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ type MockRoutablePool struct {
TokenOutDenom string
TakerFee osmomath.Dec
SpreadFactor osmomath.Dec

mockedTokenOut sdk.Coin
}

// CalcSpotPrice implements domain.RoutablePool.
func (mp *MockRoutablePool) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
if mp.PoolType == poolmanagertypes.CosmWasm {
return osmomath.OneBigDec(), nil
}

spotPrice, err := mp.ChainPoolModel.SpotPrice(sdk.Context{}, quoteDenom, baseDenom)
if err != nil {
return osmomath.BigDec{}, err
}

return spotPrice, nil
}

// GetSpreadFactor implements domain.RoutablePool.
Expand Down Expand Up @@ -57,6 +73,15 @@ func (mp *MockRoutablePool) GetSQSPoolModel() domain.SQSPool {

// CalculateTokenOutByTokenIn implements routerusecase.RoutablePool.
func (mp *MockRoutablePool) CalculateTokenOutByTokenIn(tokenIn sdk.Coin) (sdk.Coin, error) {
// We allow the ability to mock out the token out amount.
if !mp.mockedTokenOut.IsNil() {
return mp.mockedTokenOut, nil
}

if mp.PoolType == poolmanagertypes.CosmWasm {
return sdk.NewCoin(mp.TokenOutDenom, tokenIn.Amount), nil
}

// Cast to balancer
balancerPool, ok := mp.ChainPoolModel.(*balancer.Pool)
if !ok {
Expand Down Expand Up @@ -94,8 +119,8 @@ func (mp *MockRoutablePool) GetTokenOutDenom() string {
}

// ChargeTakerFee implements domain.RoutablePool.
func (*MockRoutablePool) ChargeTakerFeeExactIn(tokenIn sdk.Coin) (tokenInAfterFee sdk.Coin) {
return tokenIn.Sub(sdk.NewCoin(tokenIn.Denom, domain.DefaultTakerFee.Mul(tokenIn.Amount.ToLegacyDec()).TruncateInt()))
func (mp *MockRoutablePool) ChargeTakerFeeExactIn(tokenIn sdk.Coin) (tokenInAfterFee sdk.Coin) {
return tokenIn.Sub(sdk.NewCoin(tokenIn.Denom, mp.TakerFee.Mul(tokenIn.Amount.ToLegacyDec()).TruncateInt()))
}

// GetTakerFee implements domain.PoolI.
Expand Down Expand Up @@ -167,9 +192,18 @@ func WithTokenOutDenom(mockPool *MockRoutablePool, tokenOutDenom string) *MockRo
return newPool
}

// Allows mocking out quote token out when CalculateTokenOutByTokenIn is called.
func WithMockedTokenOut(mockPool *MockRoutablePool, tokenOut sdk.Coin) *MockRoutablePool {
newPool := deepCopyPool(mockPool)
newPool.mockedTokenOut = tokenOut
return newPool
}

func WithChainPoolModel(mockPool *MockRoutablePool, chainPool poolmanagertypes.PoolI) *MockRoutablePool {
newPool := deepCopyPool(mockPool)
newPool.ChainPoolModel = chainPool
newPool.PoolType = chainPool.GetType()
newPool.ID = chainPool.GetId()
return newPool
}

Expand Down
11 changes: 8 additions & 3 deletions ingest/sqs/domain/route_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package domain_test
import (
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"

"github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain"
Expand Down Expand Up @@ -75,7 +76,10 @@ var (
func (s *RouterTestSuite) TestPrepareResultPools() {
s.Setup()

balancerPoolID := s.PrepareBalancerPool()
balancerPoolID := s.PrepareBalancerPoolWithCoins(sdk.NewCoins(
sdk.NewCoin(DenomOne, sdk.NewInt(1_000_000_000)),
sdk.NewCoin(DenomTwo, sdk.NewInt(1_000_000_000)),
)...)

balancerPool, err := s.App.PoolManagerKeeper.GetPool(s.Ctx, balancerPoolID)
s.Require().NoError(err)
Expand Down Expand Up @@ -118,9 +122,10 @@ func (s *RouterTestSuite) TestPrepareResultPools() {
tc := tc
s.Run(name, func() {

resultPools := tc.route.PrepareResultPools()
// Note: token in is chosen arbitrarily since it is irrelevant for this test
_, _, err := tc.route.PrepareResultPools(sdk.NewCoin(DenomTwo, DefaultAmt0))
s.Require().NoError(err)

s.ValidateRoutePools(tc.expectedPools, resultPools)
s.ValidateRoutePools(tc.expectedPools, tc.route.GetPools())
})
}
Expand Down
9 changes: 7 additions & 2 deletions ingest/sqs/domain/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type RoutablePool interface {

GetTokenOutDenom() string

CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error)

CalculateTokenOutByTokenIn(tokenIn sdk.Coin) (sdk.Coin, error)
ChargeTakerFeeExactIn(tokenIn sdk.Coin) (tokenInAfterFee sdk.Coin)

Expand Down Expand Up @@ -51,9 +53,11 @@ type Route interface {
// PrepareResultPools strips away unnecessary fields
// from each pool in the route,
// leaving only the data needed by client
// Runs the quote logic one final time to compute the effective spot price.
// Note that it mutates the route.
// Returns the resulting pools.
PrepareResultPools() []RoutablePool
// Computes the spot price of the route.
// Returns the spot price before swap and effective spot price.
PrepareResultPools(tokenIn sdk.Coin) (osmomath.Dec, osmomath.Dec, error)

String() string
}
Expand All @@ -69,6 +73,7 @@ type Quote interface {
GetAmountOut() osmomath.Int
GetRoute() []SplitRoute
GetEffectiveSpreadFactor() osmomath.Dec
GetPriceImpact() osmomath.Dec

// PrepareResult mutates the quote to prepare
// it with the data formatted for output to the client.
Expand Down
8 changes: 6 additions & 2 deletions ingest/sqs/pools/usecase/pools_usecase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,12 @@ func (s *PoolsUsecaseTestSuite) TestGetRoutesFromCandidates() {

// Note: this is only done to be able to use the ValidateRoutePools
// helper method for validation.
actualRoute.PrepareResultPools()
expectedRoute.PrepareResultPools()
// Note token in is chosen arbitrarily since it is irrelevant for this test
tokenIn := sdk.NewCoin(tc.tokenInDenom, sdk.NewInt(100))
_, _, err := actualRoute.PrepareResultPools(tokenIn)
s.Require().NoError(err)
_, _, err = expectedRoute.PrepareResultPools(tokenIn)
s.Require().NoError(err)

// Validates:
// 1. Correct pool data
Expand Down
9 changes: 9 additions & 0 deletions ingest/sqs/router/usecase/pools/routable_balancer_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ func (r *routableBalancerPoolImpl) GetPoolDenoms() []string {
func (*routableBalancerPoolImpl) GetType() poolmanagertypes.PoolType {
return poolmanagertypes.Balancer
}

// CalcSpotPrice implements domain.RoutablePool.
func (r *routableBalancerPoolImpl) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
spotPrice, err := r.ChainPool.SpotPrice(sdk.Context{}, quoteDenom, baseDenom)
if err != nil {
return osmomath.BigDec{}, err
}
return spotPrice, nil
}
9 changes: 9 additions & 0 deletions ingest/sqs/router/usecase/pools/routable_concentrated_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,12 @@ func (r *routableConcentratedPoolImpl) ChargeTakerFeeExactIn(tokenIn sdk.Coin) (
func (r *routableConcentratedPoolImpl) SetTokenOutDenom(tokenOutDenom string) {
r.TokenOutDenom = tokenOutDenom
}

// CalcSpotPrice implements domain.RoutablePool.
func (r *routableConcentratedPoolImpl) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
spotPrice, err := r.ChainPool.SpotPrice(sdk.Context{}, quoteDenom, baseDenom)
if err != nil {
return osmomath.BigDec{}, err
}
return spotPrice, nil
}
5 changes: 5 additions & 0 deletions ingest/sqs/router/usecase/pools/routable_result_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,8 @@ func (r *routableResultPoolImpl) SetTokenOutDenom(tokenOutDenom string) {
func (r *routableResultPoolImpl) GetSpreadFactor() math.LegacyDec {
return r.SpreadFactor
}

// CalcSpotPrice implements domain.RoutablePool.
func (r *routableResultPoolImpl) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
panic("not implemented")
}
11 changes: 10 additions & 1 deletion ingest/sqs/router/usecase/pools/routable_stableswap_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ func (r *routableStableswapPoolImpl) GetPoolDenoms() []string {
}

// GetType implements domain.RoutablePool.
func (*routableStableswapPoolImpl) GetType() poolmanagertypes.PoolType {
func (r *routableStableswapPoolImpl) GetType() poolmanagertypes.PoolType {
return poolmanagertypes.Balancer
}

// CalcSpotPrice implements domain.RoutablePool.
func (r *routableStableswapPoolImpl) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
spotPrice, err := r.ChainPool.SpotPrice(sdk.Context{}, quoteDenom, baseDenom)
if err != nil {
return osmomath.BigDec{}, err
}
return spotPrice, nil
}
5 changes: 5 additions & 0 deletions ingest/sqs/router/usecase/pools/routable_transmuter_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,8 @@ func (r *routableTransmuterPoolImpl) GetTakerFee() math.LegacyDec {
func (r *routableTransmuterPoolImpl) SetTokenOutDenom(tokenOutDenom string) {
r.TokenOutDenom = tokenOutDenom
}

// CalcSpotPrice implements domain.RoutablePool.
func (r *routableTransmuterPoolImpl) CalcSpotPrice(baseDenom string, quoteDenom string) (osmomath.BigDec, error) {
return osmomath.OneBigDec(), nil
}
28 changes: 26 additions & 2 deletions ingest/sqs/router/usecase/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,15 @@ type quoteImpl struct {
AmountOut osmomath.Int "json:\"amount_out\""
Route []domain.SplitRoute "json:\"route\""
EffectiveFee osmomath.Dec "json:\"effective_fee\""
PriceImpact osmomath.Dec "json:\"price_impact\""
}

var (
one = osmomath.OneDec()
)

var _ domain.Quote = &quoteImpl{}

// PrepareResult implements domain.Quote.
// PrepareResult mutates the quote to prepare
// it with the data formatted for output to the client.
Expand All @@ -29,6 +36,9 @@ func (q *quoteImpl) PrepareResult() ([]domain.SplitRoute, osmomath.Dec) {
totalAmountIn := q.AmountIn.Amount.ToLegacyDec()
totalFeeAcrossRoutes := osmomath.ZeroDec()

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

for i, route := range q.Route {
routeTotalFee := osmomath.ZeroDec()
routeAmountInFraction := route.GetAmountIn().ToLegacyDec().Quo(totalAmountIn)
Expand All @@ -49,7 +59,18 @@ func (q *quoteImpl) PrepareResult() ([]domain.SplitRoute, osmomath.Dec) {
// Update the spread factor pro-rated by the amount in
totalFeeAcrossRoutes.AddMut(routeTotalFee.MulMut(routeAmountInFraction))

q.Route[i].PrepareResultPools()
routeSpotPriceInOverOut, effectiveSpotPriceInOverOut, err := q.Route[i].PrepareResultPools(q.AmountIn)
if err != nil {
panic(err)
}

totalSpotPriceInOverOut = totalSpotPriceInOverOut.AddMut(routeSpotPriceInOverOut.MulMut(routeAmountInFraction))
totalEffectiveSpotPriceInOverOut = totalEffectiveSpotPriceInOverOut.AddMut(effectiveSpotPriceInOverOut.MulMut(routeAmountInFraction))
}

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

q.EffectiveFee = totalFeeAcrossRoutes
Expand Down Expand Up @@ -90,4 +111,7 @@ func (q *quoteImpl) String() string {
return builder.String()
}

var _ domain.Quote = &quoteImpl{}
// GetPriceImpact implements domain.Quote.
func (q *quoteImpl) GetPriceImpact() osmomath.Dec {
return q.PriceImpact
}
84 changes: 79 additions & 5 deletions ingest/sqs/router/usecase/quote_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ import (

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain"
"github.com/osmosis-labs/osmosis/v21/ingest/sqs/domain/mocks"
"github.com/osmosis-labs/osmosis/v21/ingest/sqs/router/usecase"
"github.com/osmosis-labs/osmosis/v21/ingest/sqs/router/usecase/pools"
"github.com/osmosis-labs/osmosis/v21/ingest/sqs/router/usecase/route"
"github.com/osmosis-labs/osmosis/v21/x/gamm/pool-models/balancer"
"github.com/osmosis-labs/osmosis/v21/x/poolmanager"
poolmanagertypes "github.com/osmosis-labs/osmosis/v21/x/poolmanager/types"
)

var (
defaultAmount = sdk.NewInt(100_000_00)
totalInAmount = defaultAmount
totalOutAmount = defaultAmount.MulRaw(4)
)

// TestPrepareResult prepares the result of the quote for output to the client.
// First, it strips away unnecessary fields from each pool in the route.
// Additionally, it computes the effective spread factor from all routes.
Expand All @@ -31,11 +39,6 @@ func (s *RouterTestSuite) TestPrepareResult() {
takerFeeTwo = osmomath.NewDecWithPrec(4, 4)
takerFeeThree = osmomath.NewDecWithPrec(3, 3)

defaultAmount = sdk.NewInt(100_000_00)

totalInAmount = defaultAmount
totalOutAmount = defaultAmount.MulRaw(4)

poolOneBalances = sdk.NewCoins(
sdk.NewCoin(USDT, defaultAmount.MulRaw(5)),
sdk.NewCoin(ETH, defaultAmount),
Expand Down Expand Up @@ -226,6 +229,77 @@ func (s *RouterTestSuite) TestPrepareResult() {
s.Require().Equal(expectedEffectiveSpreadFactor.String(), testQuote.GetEffectiveSpreadFactor().String())
}

// This test validates that price impact is computed correctly.
func (s *RouterTestSuite) TestPrepareResult_PriceImpact() {
s.Setup()

// Pool ETH / USDC -> 0.005 spread factor & 4 USDC for 1 ETH
poolID := s.PrepareCustomBalancerPool([]balancer.PoolAsset{
{
Token: sdk.NewCoin(ETH, defaultAmount),
Weight: sdk.NewInt(100),
},
{
Token: sdk.NewCoin(USDC, defaultAmount.MulRaw(4)),
Weight: sdk.NewInt(100),
},
}, balancer.PoolParams{
SwapFee: sdk.NewDecWithPrec(5, 3),
ExitFee: osmomath.ZeroDec(),
})

poolOne, err := s.App.PoolManagerKeeper.GetPool(s.Ctx, poolID)
s.Require().NoError(err)

// Compute spot price before swap
spotPriceInOverOut, err := poolOne.SpotPrice(sdk.Context{}, ETH, USDC)
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())

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

// Setup quote
testQuote := &usecase.QuoteImpl{
AmountIn: sdk.NewCoin(ETH, totalInAmount),
AmountOut: totalOutAmount,

// 2 routes with 50-50 split, each single hop
Route: []domain.SplitRoute{

// Route 1
&usecase.RouteWithOutAmount{
RouteImpl: route.RouteImpl{
Pools: []domain.RoutablePool{
mocks.WithMockedTokenOut(
mocks.WithTokenOutDenom(
mocks.WithChainPoolModel(DefaultMockPool, poolOne), USDC),
sdk.NewCoin(USDC, totalOutAmount),
),
},
},

InAmount: totalInAmount,
OutAmount: totalOutAmount,
},
},
EffectiveFee: osmomath.ZeroDec(),
}

// System under test.
testQuote.PrepareResult()

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

}

// validateRoutes validates that the given routes are equal.
// Specifically, validates:
// - Pools
Expand Down
Loading

0 comments on commit 532e8e2

Please sign in to comment.