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

[TRA-134] construct simple vault orders #1206

Merged
merged 2 commits into from
Mar 20, 2024
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
24 changes: 24 additions & 0 deletions protocol/lib/big_math.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,27 @@ func warmCache() map[uint64]*big.Int {

return bigExponentValues
}

// BigRatRoundToNearestMultiple rounds `value` up/down to the nearest multiple of `base`.
// Returns 0 if `base` is 0.
func BigRatRoundToNearestMultiple(
value *big.Rat,
base uint32,
up bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: should we use an enum for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think a boolean should suffice?

) uint64 {
if base == 0 {
return 0
}

quotient := new(big.Rat).Quo(
value,
new(big.Rat).SetUint64(uint64(base)),
)
quotientFloored := new(big.Int).Div(quotient.Num(), quotient.Denom())

if up && quotientFloored.Cmp(quotient.Num()) != 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

if quotient.Num() == quotientFloored, doesnt that mean that quotient.Denom() = 1? Why does that mean we want to round up?

Copy link
Contributor

@vincentwschau vincentwschau Mar 20, 2024

Choose a reason for hiding this comment

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

quotientFloored.Cmp(quotient.Num()) != 0 is only true when quotient.Num() != quotientFloored (ref).
So this conditional is saying:

  • if we want to round up (up == true) and if quotient.Num() != quotientFloored then add one to the floored quotient

So if quotient.Denom() == 1, then quotientFloored.Cmp(quotient.Num()) == 0 and this conditional wouldn't round up.

return (quotientFloored.Uint64() + 1) * uint64(base)
}

return quotientFloored.Uint64() * uint64(base)
}
92 changes: 92 additions & 0 deletions protocol/lib/big_math_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -869,3 +869,95 @@ func TestMustConvertBigIntToInt32(t *testing.T) {
})
}
}

func TestBigRatRoundToNearestMultiple(t *testing.T) {
tests := map[string]struct {
value *big.Rat
base uint32
up bool
expectedResult uint64
}{
"Round 5 down to a multiple of 2": {
value: big.NewRat(5, 1),
base: 2,
up: false,
expectedResult: 4,
},
"Round 5 up to a multiple of 2": {
value: big.NewRat(5, 1),
base: 2,
up: true,
expectedResult: 6,
},
"Round 7 down to a multiple of 14": {
value: big.NewRat(7, 1),
base: 14,
up: false,
expectedResult: 0,
},
"Round 7 up to a multiple of 14": {
value: big.NewRat(7, 1),
base: 14,
up: true,
expectedResult: 14,
},
"Round 123 down to a multiple of 123": {
value: big.NewRat(123, 1),
base: 123,
up: false,
expectedResult: 123,
},
"Round 123 up to a multiple of 123": {
value: big.NewRat(123, 1),
base: 123,
up: true,
expectedResult: 123,
},
"Round 100/6 down to a multiple of 3": {
value: big.NewRat(100, 6),
base: 3,
up: false,
expectedResult: 15,
},
"Round 100/6 up to a multiple of 3": {
value: big.NewRat(100, 6),
base: 3,
up: true,
expectedResult: 18,
},
"Round 7/2 down to a multiple of 1": {
value: big.NewRat(7, 2),
base: 1,
up: false,
expectedResult: 3,
},
"Round 7/2 up to a multiple of 1": {
value: big.NewRat(7, 2),
base: 1,
up: true,
expectedResult: 4,
},
"Round 10 down to a multiple of 0": {
value: big.NewRat(10, 1),
base: 0,
up: false,
expectedResult: 0,
},
"Round 10 up to a multiple of 0": {
value: big.NewRat(10, 1),
base: 0,
up: true,
expectedResult: 0,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
result := lib.BigRatRoundToNearestMultiple(
tc.value,
tc.base,
tc.up,
)
require.Equal(t, tc.expectedResult, result)
})
}
}
166 changes: 166 additions & 0 deletions protocol/x/vault/keeper/orders.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
package keeper

import (
"fmt"
"math/big"

errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/dydxprotocol/v4-chain/protocol/lib"
clobtypes "github.com/dydxprotocol/v4-chain/protocol/x/clob/types"
satypes "github.com/dydxprotocol/v4-chain/protocol/x/subaccounts/types"
"github.com/dydxprotocol/v4-chain/protocol/x/vault/types"
)

// TODO (TRA-118): store vault strategy constants in x/vault state.
const (
Copy link
Contributor Author

Choose a reason for hiding this comment

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

some of the constants here will be used when strategy is more fully implemented

// Determines how many layers of orders a vault places.
// E.g. if num_levels=2, a vault places 2 asks and 2 bids.
NUM_LAYERS = uint8(2)
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we call this layers instead of max_orders_placed_by_vault_per_side. Does layers mean something in trading?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh it's just a made-up term. a vault places orders in terms of layers (layer 1, layer 2, ... where each layer gets further and further away from oracle price)

// Determines minimum base spread when a vault quotes around reservation price.
MIN_BASE_SPREAD_PPM = uint32(3_000) // 30bps
// Determines the amount to add to min_price_change_ppm to arrive at base spread.
BASE_SPREAD_MIN_PRICE_CHANGE_PREMIUM_PPM = uint32(1_500) // 15bps
// Determines how aggressive a vault skews its orders.
SKEW_FACTOR_PPM = uint32(500_000) // 0.5
// Determines the percentage of vault equity that each order is sized at.
ORDER_SIZE_PCT_PPM = uint32(100_000) // 10%
// Determines how long a vault's orders are valid for.
ORDER_EXPIRATION_SECONDS = uint32(5) // 5 seconds
)

// RefreshAllVaultOrders refreshes all orders for all vaults by
Expand All @@ -10,3 +35,144 @@ import (
// 2. Placing new orders.
func (k Keeper) RefreshAllVaultOrders(ctx sdk.Context) {
}

// GetVaultClobOrders returns a list of long term orders for a given vault, with its corresponding
// clob pair, perpetual, market parameter, and market price.
// Let n be number of layers, then the function returns orders at [a_1, b_1, a_2, b_2, ..., a_n, b_n]
// where a_i and b_i are the ask price and bid price at i-th layer. To compute a_i and b_i:
// - a_i = oraclePrice * (1 + spread)^i
// - b_i = oraclePrice * (1 - spread)^i
// TODO (TRA-144): Implement order size
// TODO (TRA-114): Implement skew
func (k Keeper) GetVaultClobOrders(
ctx sdk.Context,
vaultId types.VaultId,
) (orders []*clobtypes.Order, err error) {
// Get clob pair, perpetual, market parameter, and market price that correspond to this vault.
clobPair, exists := k.clobKeeper.GetClobPair(ctx, clobtypes.ClobPairId(vaultId.Number))
if !exists {
return orders, errorsmod.Wrap(
types.ErrClobPairNotFound,
fmt.Sprintf("VaultId: %v", vaultId),
)
}
perpId := clobPair.Metadata.(*clobtypes.ClobPair_PerpetualClobMetadata).PerpetualClobMetadata.PerpetualId
perpetual, err := k.perpetualsKeeper.GetPerpetual(ctx, perpId)
if err != nil {
return orders, errorsmod.Wrap(
err,
fmt.Sprintf("VaultId: %v", vaultId),
)
}
marketParam, exists := k.pricesKeeper.GetMarketParam(ctx, perpetual.Params.MarketId)
if !exists {
return orders, errorsmod.Wrap(
types.ErrMarketParamNotFound,
fmt.Sprintf("VaultId: %v", vaultId),
)
}
marketPrice, err := k.pricesKeeper.GetMarketPrice(ctx, perpetual.Params.MarketId)
if err != nil {
return orders, errorsmod.Wrap(
err,
fmt.Sprintf("VaultId: %v", vaultId),
)
}

// Get vault (subaccount 0 of corresponding module account).
vault := satypes.SubaccountId{
Owner: vaultId.ToModuleAccountAddress(),
Number: 0,
}
// Calculate spread.
spreadPpm := lib.Max(
MIN_BASE_SPREAD_PPM,
BASE_SPREAD_MIN_PRICE_CHANGE_PREMIUM_PPM+marketParam.MinPriceChangePpm,
)
Comment on lines +88 to +91
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this the spreadPpm?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

// Get market price in subticks.
subticks := clobtypes.PriceToSubticks(
marketPrice,
clobPair,
perpetual.Params.AtomicResolution,
lib.QuoteCurrencyAtomicResolution,
)
// Get order expiration time.
goodTilBlockTime := &clobtypes.Order_GoodTilBlockTime{
GoodTilBlockTime: uint32(ctx.BlockTime().Unix()) + ORDER_EXPIRATION_SECONDS,
}
// Construct one ask and one bid for each layer.
orders = make([]*clobtypes.Order, 2*NUM_LAYERS)
askSubticks := new(big.Rat).Set(subticks)
bidSubticks := new(big.Rat).Set(subticks)
for i := uint8(0); i < NUM_LAYERS; i++ {
// Calculate ask and bid subticks for this layer.
askSubticks = lib.BigRatMulPpm(askSubticks, lib.OneMillion+spreadPpm)
bidSubticks = lib.BigRatMulPpm(bidSubticks, lib.OneMillion-spreadPpm)

// Construct ask at this layer.
ask := clobtypes.Order{
OrderId: clobtypes.OrderId{
SubaccountId: vault,
ClientId: k.GetVaultClobOrderClientId(ctx, clobtypes.Order_SIDE_SELL, uint8(i+1)),
OrderFlags: clobtypes.OrderIdFlags_LongTerm,
ClobPairId: clobPair.Id,
},
Side: clobtypes.Order_SIDE_SELL,
Quantums: clobPair.StepBaseQuantums, // TODO (TRA-144): Implement order size
Subticks: lib.BigRatRoundToNearestMultiple(
askSubticks,
clobPair.SubticksPerTick,
true, // round up for asks
),
GoodTilOneof: goodTilBlockTime,
}

// Construct bid at this layer.
bid := clobtypes.Order{
OrderId: clobtypes.OrderId{
SubaccountId: vault,
ClientId: k.GetVaultClobOrderClientId(ctx, clobtypes.Order_SIDE_BUY, uint8(i+1)),
OrderFlags: clobtypes.OrderIdFlags_LongTerm,
ClobPairId: clobPair.Id,
},
Side: clobtypes.Order_SIDE_BUY,
Quantums: clobPair.StepBaseQuantums, // TODO (TRA-144): Implement order size
Subticks: lib.BigRatRoundToNearestMultiple(
bidSubticks,
clobPair.SubticksPerTick,
false, // round down for bids
),
GoodTilOneof: goodTilBlockTime,
}

orders[2*i] = &ask
orders[2*i+1] = &bid
}
Comment on lines +112 to +150
Copy link
Contributor

Choose a reason for hiding this comment

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

a lot of duplicate code here, could we move it into a subfunction and reuse?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

don't think it's too much duplicate code? I feel having ask and bid explicitly here makes it very clear what each order is like and keeps the function simple


return orders, nil
}

// GetVaultClobOrderClientId returns the client ID for a CLOB order where
// - 1st bit is `side-1` (subtract 1 as buy_side = 1, sell_side = 2)
//
// - 2nd bit is `block height % 2`
// - block height bit alternates between 0 and 1 to ensure that client IDs
// are different in two consecutive blocks (otherwise, order placement would
// fail because the same order IDs are already marked for cancellation)
//
// - next 8 bits are `layer`
func (k Keeper) GetVaultClobOrderClientId(
ctx sdk.Context,
side clobtypes.Order_Side,
layer uint8,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: layer = 0 is invalid, given layer always starts at 1?

Copy link
Contributor Author

@tqin7 tqin7 Mar 19, 2024

Choose a reason for hiding this comment

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

technically layer = 0 is okay for this function. it's up to other functions on what value of layer to pass in. adding a check on layer != 0 is ok but might make using this function a bit too complicated as parent func has to check whether there's an error

) uint32 {
sideBit := uint32(side - 1)
sideBit <<= 31

blockHeightBit := uint32(ctx.BlockHeight() % 2)
blockHeightBit <<= 30

layerBits := uint32(layer) << 22

return sideBit | blockHeightBit | layerBits
}
Loading
Loading