Skip to content
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: 2 additions & 0 deletions proto/dex/enums.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ enum OrderType {
LIQUIDATION = 2;
FOKMARKET = 3; // fill-or-kill market order
FOKMARKETBYVALUE = 4; // fill-or-kill market by value order
STOPLOSS = 5;
STOPLIMIT = 6;
}

enum Unit {
Expand Down
2 changes: 2 additions & 0 deletions proto/dex/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "dex/long_book.proto";
import "dex/short_book.proto";
import "dex/twap.proto";
import "dex/tick_size.proto";
import "dex/order.proto";
// this line is used by starport scaffolding # genesis/proto/import

option go_package = "github.com/sei-protocol/sei-chain/x/dex/types";
Expand All @@ -19,5 +20,6 @@ message GenesisState {
repeated Twap twapList = 4;
repeated TickSize tickSizeList = 5; // if null, then no restriction, todo(zw) should set it to not nullable?
uint64 lastEpoch = 6;
repeated Order triggeredOrdersList = 7 [(gogoproto.nullable) = false];
// this line is used by starport scaffolding # genesis/proto/state
}
9 changes: 9 additions & 0 deletions proto/dex/order.proto
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ message Order {
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "nominal"
];
string triggerPrice = 14 [
(gogoproto.moretags) = "yaml:\"trigger_price\"",
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "trigger_price"
];
bool triggerStatus = 15 [
(gogoproto.jsontag) = "trigger_status"
];
}

message Cancellation {
Expand Down
2 changes: 2 additions & 0 deletions wasmbinding/test/encoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func TestEncodePlaceOrder(t *testing.T) {
Quantity: sdk.OneDec(),
Data: "{\"position_effect\":\"OPEN\", \"leverage\":\"1\"}",
Nominal: sdk.ZeroDec(),
TriggerPrice: sdk.ZeroDec(),
TriggerStatus: false,
}
fund := sdk.NewCoin("usei", sdk.NewInt(1000000000))
msg := bindings.PlaceOrders{
Expand Down
14 changes: 14 additions & 0 deletions x/dex/cache/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,20 @@ func (o *BlockOrders) GetLimitOrders(direction types.PositionDirection) []*types
return o.getOrdersByCriteria(types.OrderType_LIMIT, direction)
}

func (o *BlockOrders) GetTriggeredOrders() []*types.Order {
o.mu.Lock()
defer o.mu.Unlock()
return o.getOrdersByCriteriaMap(
map[types.OrderType]bool{
types.OrderType_STOPLOSS: true,
types.OrderType_STOPLIMIT: true,
},
map[types.PositionDirection]bool{
types.PositionDirection_LONG: true,
types.PositionDirection_SHORT: true,
})
}

func (o *BlockOrders) getOrdersByCriteria(orderType types.OrderType, direction types.PositionDirection) []*types.Order {
res := []*types.Order{}
for _, order := range o.internal {
Expand Down
42 changes: 42 additions & 0 deletions x/dex/cache/order_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,45 @@ func TestGetSortedMarketOrders(t *testing.T) {
require.Equal(t, uint64(6), marketSellsWithoutLiquidation[0].Id)
require.Equal(t, uint64(5), marketSellsWithoutLiquidation[1].Id)
}

func TestGetTriggeredOrders(t *testing.T) {
ctx := sdk.Context{}
stateOne := dex.NewMemState()
stateOne.GetBlockOrders(ctx, utils.ContractAddress(TEST_CONTRACT), utils.PairString(TEST_PAIR)).Add(&types.Order{
Id: 1,
Account: "test",
ContractAddr: TEST_CONTRACT,
PositionDirection: types.PositionDirection_LONG,
OrderType: types.OrderType_STOPLOSS,
Price: sdk.MustNewDecFromStr("150"),
TriggerPrice: sdk.MustNewDecFromStr("100"),
TriggerStatus: false,
})
stateOne.GetBlockOrders(ctx, utils.ContractAddress(TEST_CONTRACT), utils.PairString(TEST_PAIR)).Add(&types.Order{
Id: 2,
Account: "test",
ContractAddr: TEST_CONTRACT,
PositionDirection: types.PositionDirection_SHORT,
OrderType: types.OrderType_STOPLIMIT,
Price: sdk.MustNewDecFromStr("150"),
TriggerPrice: sdk.MustNewDecFromStr("200"),
TriggerStatus: false,
})
stateOne.GetBlockOrders(ctx, utils.ContractAddress(TEST_CONTRACT), utils.PairString(TEST_PAIR)).Add(&types.Order{
Id: 3,
Account: "test",
ContractAddr: TEST_CONTRACT,
PositionDirection: types.PositionDirection_LONG,
OrderType: types.OrderType_LIMIT,
Price: sdk.MustNewDecFromStr("100"),
})

triggeredOrders := stateOne.GetBlockOrders(ctx, utils.ContractAddress(TEST_CONTRACT), utils.PairString(TEST_PAIR)).GetTriggeredOrders()
var orderIds []uint64
for _, order := range triggeredOrders {
orderIds = append(orderIds, order.Id)
}

require.Equal(t, len(triggeredOrders), 2)
require.ElementsMatch(t, orderIds, []uint64{1, 2})
}
13 changes: 13 additions & 0 deletions x/dex/client/cli/tx/tx_place_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,19 @@ func CmdPlaceOrders() *cobra.Command {
}
newOrder.Nominal = argNominal
}
if newOrder.OrderType == types.OrderType_STOPLOSS || newOrder.OrderType == types.OrderType_STOPLIMIT {
triggerPrice, err := sdk.NewDecFromStr(orderDetails[7])
if err != nil {
return err
}
triggerStatus, err := strconv.ParseBool(orderDetails[8])
if err != nil {
return err
}

newOrder.TriggerPrice = triggerPrice
newOrder.TriggerStatus = triggerStatus
}
orders = append(orders, &newOrder)
}

Expand Down
56 changes: 56 additions & 0 deletions x/dex/contract/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func ExecutePair(
// Fill limit orders
limitOrderOutcome := exchange.MatchLimitOrders(ctx, orderbook)
totalOutcome := marketOrderOutcome.Merge(&limitOrderOutcome)
UpdateTriggeredOrderForPair(ctx, typedContractAddr, typedPairStr, dexkeeper, totalOutcome)

dexkeeperutils.SetPriceStateFromExecutionOutcome(ctx, dexkeeper, typedContractAddr, pair, totalOutcome)
dexkeeperutils.FlushOrderbook(ctx, dexkeeper, typedContractAddr, orderbook)
Expand Down Expand Up @@ -111,6 +112,60 @@ func matchMarketOrderForPair(
return marketBuyOutcome.Merge(&marketSellOutcome)
}

func MoveTriggeredOrderForPair(
ctx sdk.Context,
typedContractAddr dextypesutils.ContractAddress,
typedPairStr dextypesutils.PairString,
dexkeeper *keeper.Keeper,
) {
priceDenom, assetDenom := dextypesutils.GetPriceAssetString(typedPairStr)
triggeredOrders := dexkeeper.GetAllTriggeredOrdersForPair(ctx, string(typedContractAddr), priceDenom, assetDenom)
for i, order := range triggeredOrders {
if order.TriggerStatus {
if order.OrderType == types.OrderType_STOPLOSS {
triggeredOrders[i].OrderType = types.OrderType_MARKET
} else if order.OrderType == types.OrderType_STOPLIMIT {
triggeredOrders[i].OrderType = types.OrderType_LIMIT
}
dexkeeper.MemState.GetBlockOrders(ctx, typedContractAddr, typedPairStr).Add(&triggeredOrders[i])
dexkeeper.RemoveTriggeredOrder(ctx, string(typedContractAddr), order.Id, priceDenom, assetDenom)
}
}
}

func UpdateTriggeredOrderForPair(
ctx sdk.Context,
typedContractAddr dextypesutils.ContractAddress,
typedPairStr dextypesutils.PairString,
dexkeeper *keeper.Keeper,
totalOutcome exchange.ExecutionOutcome,
) {
// update existing trigger orders
priceDenom, assetDenom := dextypesutils.GetPriceAssetString(typedPairStr)
triggeredOrders := dexkeeper.GetAllTriggeredOrdersForPair(ctx, string(typedContractAddr), priceDenom, assetDenom)
for i, order := range triggeredOrders {
if order.PositionDirection == types.PositionDirection_LONG && order.TriggerPrice.LTE(totalOutcome.MaxPrice) {
triggeredOrders[i].TriggerStatus = true
dexkeeper.SetTriggeredOrder(ctx, string(typedContractAddr), triggeredOrders[i], priceDenom, assetDenom)
} else if order.PositionDirection == types.PositionDirection_SHORT && order.TriggerPrice.GTE(totalOutcome.MinPrice) {
triggeredOrders[i].TriggerStatus = true
dexkeeper.SetTriggeredOrder(ctx, string(typedContractAddr), triggeredOrders[i], priceDenom, assetDenom)
}
}

// update triggered orders in cache
orders := dexkeeper.MemState.GetBlockOrders(ctx, typedContractAddr, typedPairStr)
cacheTriggeredOrders := orders.GetTriggeredOrders()
for i, order := range cacheTriggeredOrders {
if order.PositionDirection == types.PositionDirection_LONG && order.TriggerPrice.LTE(totalOutcome.MaxPrice) {
cacheTriggeredOrders[i].TriggerStatus = true
} else if order.PositionDirection == types.PositionDirection_SHORT && order.TriggerPrice.GTE(totalOutcome.MinPrice) {
cacheTriggeredOrders[i].TriggerStatus = true
}
dexkeeper.SetTriggeredOrder(ctx, string(typedContractAddr), *cacheTriggeredOrders[i], priceDenom, assetDenom)
}
}

func GetMatchResults(
ctx sdk.Context,
typedContractAddr dextypesutils.ContractAddress,
Expand Down Expand Up @@ -168,6 +223,7 @@ func ExecutePairsInParallel(ctx sdk.Context, contractAddr string, dexkeeper *kee

pairCopy := pair
pairStr := dextypesutils.GetPairString(&pairCopy)
MoveTriggeredOrderForPair(ctx, typedContractAddr, pairStr, dexkeeper)
orderbook, found := orderBooks.Load(pairStr)
if !found {
panic(fmt.Sprintf("Orderbook not found for %s", pairStr))
Expand Down
159 changes: 159 additions & 0 deletions x/dex/contract/execution_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package contract_test

import (
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
keepertest "github.com/sei-protocol/sei-chain/testutil/keeper"
"github.com/sei-protocol/sei-chain/x/dex/contract"
"github.com/sei-protocol/sei-chain/x/dex/exchange"
"github.com/sei-protocol/sei-chain/x/dex/types"
"github.com/sei-protocol/sei-chain/x/dex/types/utils"
"github.com/stretchr/testify/require"
)

func TEST_PAIR() types.Pair {
return types.Pair{
PriceDenom: "usdc",
AssetDenom: "atom",
}
}

const (
TEST_CONTRACT = "test"
TestTimestamp uint64 = 10000
TestHeight uint64 = 1
)

func TestMoveTriggeredOrderIntoMemState(t *testing.T) {
pair := TEST_PAIR()
dexkeeper, ctx := keepertest.DexKeeper(t)
ctx = ctx.WithBlockHeight(int64(TestHeight)).WithBlockTime(time.Unix(int64(TestTimestamp), 0))
triggeredOrder := types.Order{
Id: 8,
Price: sdk.MustNewDecFromStr("20"),
Quantity: sdk.MustNewDecFromStr("5"),
Data: "",
PositionDirection: types.PositionDirection_LONG,
OrderType: types.OrderType_STOPLOSS,
PriceDenom: TEST_PAIR().PriceDenom,
AssetDenom: TEST_PAIR().AssetDenom,
TriggerPrice: sdk.MustNewDecFromStr("10"),
TriggerStatus: true,
}

dexkeeper.SetTriggeredOrder(ctx, TEST_CONTRACT, triggeredOrder, TEST_PAIR().PriceDenom, TEST_PAIR().AssetDenom)
contract.MoveTriggeredOrderForPair(
ctx,
utils.ContractAddress(TEST_CONTRACT),
utils.GetPairString(&pair),
dexkeeper,
)
orders := dexkeeper.MemState.GetBlockOrders(ctx, TEST_CONTRACT, utils.GetPairString(&pair))
cacheMarketOrders := orders.GetSortedMarketOrders(types.PositionDirection_LONG, false)
cacheTriggeredOrders := orders.GetTriggeredOrders()

triggeredBookOrders := dexkeeper.GetAllTriggeredOrdersForPair(ctx, TEST_CONTRACT, TEST_PAIR().PriceDenom, TEST_PAIR().AssetDenom)

require.Equal(t, len(triggeredBookOrders), 0)
require.Equal(t, len(cacheTriggeredOrders), 0)
require.Equal(t, len(cacheMarketOrders), 1)
require.Equal(t, cacheMarketOrders[0].Id, uint64(8))
require.Equal(t, cacheMarketOrders[0].OrderType, types.OrderType_MARKET)
require.Equal(t, cacheMarketOrders[0].PositionDirection, types.PositionDirection_LONG)
}

func TestUpdateTriggeredOrders(t *testing.T) {
pair := TEST_PAIR()
dexkeeper, ctx := keepertest.DexKeeper(t)
ctx = ctx.WithBlockHeight(int64(TestHeight)).WithBlockTime(time.Unix(int64(TestTimestamp), 0))
shortTriggeredOrder := types.Order{
Id: 1,
Price: sdk.MustNewDecFromStr("20"),
Quantity: sdk.MustNewDecFromStr("5"),
Data: "",
PositionDirection: types.PositionDirection_SHORT,
OrderType: types.OrderType_STOPLIMIT,
PriceDenom: TEST_PAIR().PriceDenom,
AssetDenom: TEST_PAIR().AssetDenom,
TriggerPrice: sdk.MustNewDecFromStr("6"),
TriggerStatus: false,
}
longTriggeredOrder := types.Order{
Id: 2,
Price: sdk.MustNewDecFromStr("20"),
Quantity: sdk.MustNewDecFromStr("5"),
Data: "",
PositionDirection: types.PositionDirection_LONG,
OrderType: types.OrderType_STOPLOSS,
PriceDenom: TEST_PAIR().PriceDenom,
AssetDenom: TEST_PAIR().AssetDenom,
TriggerPrice: sdk.MustNewDecFromStr("19"),
TriggerStatus: false,
}
shortNotTriggeredOrder := types.Order{
Id: 3,
Price: sdk.MustNewDecFromStr("20"),
Quantity: sdk.MustNewDecFromStr("5"),
Data: "",
PositionDirection: types.PositionDirection_SHORT,
OrderType: types.OrderType_STOPLOSS,
PriceDenom: TEST_PAIR().PriceDenom,
AssetDenom: TEST_PAIR().AssetDenom,
TriggerPrice: sdk.MustNewDecFromStr("4"),
TriggerStatus: false,
}
longNotTriggeredOrder := types.Order{
Id: 4,
Price: sdk.MustNewDecFromStr("20"),
Quantity: sdk.MustNewDecFromStr("5"),
Data: "",
PositionDirection: types.PositionDirection_LONG,
OrderType: types.OrderType_STOPLIMIT,
PriceDenom: TEST_PAIR().PriceDenom,
AssetDenom: TEST_PAIR().AssetDenom,
TriggerPrice: sdk.MustNewDecFromStr("21"),
TriggerStatus: false,
}

totalOutcome := exchange.ExecutionOutcome{
TotalNotional: sdk.MustNewDecFromStr("10"),
TotalQuantity: sdk.MustNewDecFromStr("10"),
Settlements: []*types.SettlementEntry{},
MinPrice: sdk.MustNewDecFromStr("5"),
MaxPrice: sdk.MustNewDecFromStr("20"),
}

dexkeeper.SetTriggeredOrder(ctx, TEST_CONTRACT, shortTriggeredOrder, TEST_PAIR().PriceDenom, TEST_PAIR().AssetDenom)
dexkeeper.SetTriggeredOrder(ctx, TEST_CONTRACT, longNotTriggeredOrder, TEST_PAIR().PriceDenom, TEST_PAIR().AssetDenom)
orders := dexkeeper.MemState.GetBlockOrders(ctx, TEST_CONTRACT, utils.GetPairString(&pair))
orders.Add(&shortNotTriggeredOrder)
orders.Add(&longTriggeredOrder)

contract.UpdateTriggeredOrderForPair(
ctx,
utils.ContractAddress(TEST_CONTRACT),
utils.GetPairString(&pair),
dexkeeper,
totalOutcome,
)

triggeredBookOrders := dexkeeper.GetAllTriggeredOrdersForPair(ctx, TEST_CONTRACT, TEST_PAIR().PriceDenom, TEST_PAIR().AssetDenom)

require.Equal(t, len(triggeredBookOrders), 4)
triggerStatusMap := map[uint64]bool{}

for _, order := range triggeredBookOrders {
triggerStatusMap[order.Id] = order.TriggerStatus
}
require.Contains(t, triggerStatusMap, uint64(1))
require.Contains(t, triggerStatusMap, uint64(2))
require.Contains(t, triggerStatusMap, uint64(3))
require.Contains(t, triggerStatusMap, uint64(4))

require.Equal(t, triggerStatusMap[uint64(1)], true)
require.Equal(t, triggerStatusMap[uint64(2)], true)
require.Equal(t, triggerStatusMap[uint64(3)], false)
require.Equal(t, triggerStatusMap[uint64(4)], false)
}
4 changes: 4 additions & 0 deletions x/dex/exchange/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ type ExecutionOutcome struct {
TotalNotional sdk.Dec
TotalQuantity sdk.Dec
Settlements []*types.SettlementEntry
MinPrice sdk.Dec
MaxPrice sdk.Dec
}

func (o *ExecutionOutcome) Merge(other *ExecutionOutcome) ExecutionOutcome {
return ExecutionOutcome{
TotalNotional: o.TotalNotional.Add(other.TotalNotional),
TotalQuantity: o.TotalQuantity.Add(other.TotalQuantity),
Settlements: append(o.Settlements, other.Settlements...),
MinPrice: sdk.MinDec(o.MinPrice, other.MinPrice),
MaxPrice: sdk.MaxDec(o.MaxPrice, other.MaxPrice),
}
}
Loading