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

(CL): Add Liquidity Net In Direction Query #4714

Merged
merged 6 commits into from
Mar 29, 2023
Merged
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 go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.19
require (
github.com/CosmWasm/wasmd v0.30.0
github.com/cosmos/cosmos-proto v1.0.0-alpha8
github.com/cosmos/cosmos-sdk v0.46.11
github.com/cosmos/cosmos-sdk v0.47.1
github.com/cosmos/go-bip39 v1.0.0
github.com/cosmos/ibc-go/v4 v4.3.0
github.com/gogo/protobuf v1.3.3
Expand Down
30 changes: 29 additions & 1 deletion proto/osmosis/concentrated-liquidity/pool-model/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ service Query {
"/osmosis/concentratedliquidity/v1beta1/total_liquidity_for_range";
}

// LiquidityNetInDirection returns liquidity net in the direction given.
// Uses the bound if specified, if not uses either min tick / max tick
// depending on the direction.
Comment on lines +52 to +54
Copy link
Member

Choose a reason for hiding this comment

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

nit: if we go with the separate "current tick" and "current tick liquidity", it might be worth updating the comment to reflect these returns as well

rpc LiquidityNetInDirection(QueryLiquidityNetInDirectionRequest)
returns (QueryLiquidityNetInDirectionResponse) {
option (google.api.http).get = "/osmosis/concentratedliquidity/v1beta1/"
"query_liquidity_net_in_direction";
}

// ClaimableFees returns the amount of fees that can be claimed by a position
// with the given id.
rpc ClaimableFees(QueryClaimableFeesRequest)
Expand Down Expand Up @@ -154,7 +163,26 @@ message LiquidityDepthWithRange {
];
}

//=============================== TickLiquidityInBatches
//=============================== LiquidityNetInDirection
message QueryLiquidityNetInDirectionRequest {
uint64 pool_id = 1 [ (gogoproto.moretags) = "yaml:\"pool_id\"" ];
string token_in = 2 [ (gogoproto.moretags) = "yaml:\"token_in\"" ];
string bound_tick = 3 [
Copy link
Member

@jonator jonator Mar 28, 2023

Choose a reason for hiding this comment

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

How can we know we're requesting all ticks (in a direction)? Should we just pass in the hardcoded max_tick value?

I'm thinking in the frontend, the estimates will go like:

  • Try to reduce query size and roughly estimate how many ticks we'll need based on size of trade and liquidity in pool.

Above is an edge case, as I want the above to work in most common cases (trading a moderate amount against a higher liquidity pool), this is an edge case scenario:

  • If we run out of ticks in simulation iteration, we'll assume it's because we didn't request enough ticks. Then we'll try again with all ticks in a given direction, by passing in either maxTick into bound_tick or using some special token/flag?? @mattverse @p0mvn
  • Only after we've tried getting the remaining tick net values, can we conclude the pool doesn't have enough liquidity to handle the user's current trade, where we will show an error.

Copy link
Member

Choose a reason for hiding this comment

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

To estimate the expected final tick very roughly, we could take the current tick liquidity, take some proportion of that (e.g. 95%) because we assume that as we move away from the current tick, the liquidity decreases. Note, that we can come up with an algorithm to estimate the proportion too but can be hard coded to start. Finally, compute sqrt price delta in the following way:

  • swapping y for x:

$$\Delta \sqrt P = \Delta y / L$$

  • swapping x for y:

$$\Delta \sqrt P = L / \Delta x$$

Once we have that, we compute the "next expected sqrt price". From sqrt price, we can compute the expected final tick.

If that fails, we could then do some form of binary search where we take the estimated tick delta $$\Delta T$$ and if that wasn't enough, we query the next:

  • $$2 \Delta T$$
  • $$4 \Delta T$$

exclusively from the ticks we already received during prior querying to avoid redundant processing.

Similar to how Go slices reallocate buffer as we append to them.

Do you think we can keep the current design of querying until max bound for v1 and then work out a design for bounds estimates separately? @jonator @mattverse

I think merging this would allow @mattverse and I to parallelize and further iterate on improvements

Let me know what you all think

Copy link
Member

Choose a reason for hiding this comment

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

So we can run that calculation to get the next tick index then perhaps add a few extra ticks to avoid any rounding error?

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that sounds right

(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Int",
(gogoproto.moretags) = "yaml:\"bound_tick\"",
(gogoproto.nullable) = true
];
}
message QueryLiquidityNetInDirectionResponse {
repeated LiquidityDepth liquidity_depths = 1 [ (gogoproto.nullable) = false ];
string current_liquidity = 2 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.moretags) = "yaml:\"current_liquidity\"",
(gogoproto.nullable) = false
];
}

//=============================== TotalLiquidityForRange
message QueryTotalLiquidityForRangeRequest {
uint64 pool_id = 1 [ (gogoproto.moretags) = "yaml:\"pool_id\"" ];
}
Expand Down
7 changes: 4 additions & 3 deletions proto/osmosis/poolmanager/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ service Query {
rpc EstimateSinglePoolSwapExactAmountIn(
EstimateSinglePoolSwapExactAmountInRequest)
returns (EstimateSwapExactAmountInResponse) {
option (google.api.http).get =
"/osmosis/poolmanager/v1beta1/{pool_id}/estimate/single_pool_swap_exact_amount_in";
option (google.api.http).get = "/osmosis/poolmanager/v1beta1/{pool_id}/"
"estimate/single_pool_swap_exact_amount_in";
}

// Estimates swap amount in given out.
Expand All @@ -45,7 +45,8 @@ service Query {
EstimateSinglePoolSwapExactAmountOutRequest)
returns (EstimateSwapExactAmountOutResponse) {
option (google.api.http).get =
"/osmosis/poolmanager/v1beta1/{pool_id}/estimate_out/single_pool_swap_exact_amount_out";
"/osmosis/poolmanager/v1beta1/{pool_id}/estimate_out/"
"single_pool_swap_exact_amount_out";
}

// Returns the total number of pools existing in Osmosis.
Expand Down
33 changes: 33 additions & 0 deletions x/concentrated-liquidity/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,39 @@ func (q Querier) TotalLiquidityForRange(goCtx context.Context, req *clquery.Quer
return &clquery.QueryTotalLiquidityForRangeResponse{Liquidity: liquidity}, nil
}

// LiquidityNetInDirection returns an array of LiquidityDepthWithRange, which contains the range(lower tick and upper tick) and the liquidity amount in the range.
Copy link
Member

Choose a reason for hiding this comment

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

nit: ditto for godoc updates to reflect additional returns

func (q Querier) LiquidityNetInDirection(goCtx context.Context, req *clquery.QueryLiquidityNetInDirectionRequest) (*clquery.QueryLiquidityNetInDirectionResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
}
ctx := sdk.UnwrapSDKContext(goCtx)

// convert values from pointers
var boundTick sdk.Int
if req.BoundTick == nil {
boundTick = sdk.Int{}
} else {
boundTick = *req.BoundTick
}

liquidityDepths, err := q.Keeper.GetLiquidityNetInDirection(
ctx,
req.PoolId,
req.TokenIn,
boundTick,
)
if err != nil {
return nil, err
}

pool, err := q.Keeper.getPoolById(ctx, req.PoolId)
if err != nil {
return nil, err
}

return &clquery.QueryLiquidityNetInDirectionResponse{LiquidityDepths: liquidityDepths, CurrentLiquidity: pool.GetLiquidity()}, nil
Copy link
Member

@p0mvn p0mvn Mar 27, 2023

Choose a reason for hiding this comment

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

It seems to me that we include "current_tick" and it's liquidity net in the liquidityDepths. However, current tick's liquidity_net is irrelevant for FE swap estimate.

In terms of the current tick, we only need to know a) the current tick index b) current tick liquidity (which we already return)

So I suggest only including liquidity net amounts starting from the "next tick" from current in the swap direction

Instead, we can have a separate field called CurrentTick similar to CurrentLiquidity

Let me know what you think

Copy link
Member

Choose a reason for hiding this comment

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

CC: @jonator for awareness on this suggestion

Copy link
Member

Choose a reason for hiding this comment

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

Consider renaming liquidityDepths -> liquidityNetAmounts or something like that because net amount is different from depths IMO

}

func (q Querier) ClaimableFees(ctx context.Context, req *clquery.QueryClaimableFeesRequest) (*clquery.QueryClaimableFeesResponse, error) {
if req == nil {
return nil, status.Error(codes.InvalidArgument, "empty request")
Expand Down
78 changes: 78 additions & 0 deletions x/concentrated-liquidity/tick.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,84 @@ func (k Keeper) GetTickLiquidityForRange(ctx sdk.Context, poolId uint64) ([]quer
return liquidityDepthsForRange, nil
}

// GetLiquidityNetInDirection returns an array of liquidity depth in the given zeroForOne direction.
// Uses boundTick if given, and iterates until we hit bound Tick's index. If not, uses min tick || max tick depending on the direction of zeroForOne.
func (k Keeper) GetLiquidityNetInDirection(ctx sdk.Context, poolId uint64, tokenIn string, boundTick sdk.Int) ([]query.LiquidityDepth, error) {
// check if pool exists
if !k.poolExists(ctx, poolId) {
return []query.LiquidityDepth{}, types.PoolNotFoundError{PoolId: poolId}
}

// get min and max tick for the pool
p, err := k.getPoolById(ctx, poolId)
if err != nil {
return []query.LiquidityDepth{}, err
}
currentTick := p.GetCurrentTick()

// sanity check that given tokenIn is an asset in pool.
if tokenIn != p.GetToken0() && tokenIn != p.GetToken1() {
return []query.LiquidityDepth{}, types.TokenInDenomNotInPoolError{TokenInDenom: tokenIn}
}

// figure out zero for one depending on the token in.
zeroForOne := p.GetToken0() == tokenIn

// use max or min tick if provided bound is nil
exponentAtPriceOne := p.GetPrecisionFactorAtPriceOne()
minTick, maxTick := math.GetMinAndMaxTicksFromExponentAtPriceOneInternal(exponentAtPriceOne)
if boundTick.IsNil() {
if zeroForOne {
boundTick = sdk.NewInt(minTick)
} else {
boundTick = sdk.NewInt(maxTick)
}
} else { // if bound is not nil, sanity check that given bound is in the direction of swap
if (zeroForOne && boundTick.GT(currentTick)) || (!zeroForOne && boundTick.LT(currentTick)) {
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
return []query.LiquidityDepth{}, types.InvalidDirectionError{ZeroForOne: zeroForOne, PoolTick: currentTick.Int64(), TargetTick: boundTick.Int64()}
} else if boundTick.GT(sdk.NewInt(maxTick)) || boundTick.LT(sdk.NewInt(minTick)) {
return []query.LiquidityDepth{}, types.InvalidTickError{Tick: boundTick.Int64(), IsLower: false, MinTick: minTick, MaxTick: maxTick}
}
}

liquidityDepths := []query.LiquidityDepth{}
swapStrategy := swapstrategy.New(zeroForOne, sdk.ZeroDec(), k.storeKey, sdk.ZeroDec())

checkTickWithInBoundFn := func(zeroForOne bool, currentTick, boundTick sdk.Int) bool {
return (currentTick.GTE(boundTick) && zeroForOne) || (currentTick.LTE(boundTick) && !zeroForOne)
}

nextTick, ok := swapStrategy.NextInitializedTick(ctx, poolId, currentTick.Int64())
if !ok {
return []query.LiquidityDepth{}, nil
}
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
for checkTickWithInBoundFn(zeroForOne, nextTick, boundTick) {
tick, err := k.getTickByTickIndex(ctx, poolId, nextTick)
if err != nil {
return []query.LiquidityDepth{}, err
}

liquidityDepth := query.LiquidityDepth{
LiquidityNet: tick.LiquidityNet,
TickIndex: nextTick,
}
liquidityDepths = append(liquidityDepths, liquidityDepth)
currentTick = nextTick

nextInitTick, ok := swapStrategy.NextInitializedTick(ctx, poolId, currentTick.Int64())
// break and return the liquidity as is if
// - there are no more next tick that is initialized,
// - we hit upper limit

if !ok {
break
}
nextTick = nextInitTick
}

return liquidityDepths, nil
p0mvn marked this conversation as resolved.
Show resolved Hide resolved
}

// GetPerTickLiquidityDepthFromRange uses the given lower tick and upper tick, iterates over ticks, creates and returns LiquidityDepth array.
// LiquidityNet from the tick is used to indicate liquidity depths.
func (k Keeper) GetPerTickLiquidityDepthFromRange(ctx sdk.Context, poolId uint64, lowerTick, upperTick int64) ([]query.LiquidityDepth, error) {
Expand Down
Loading