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

Add advanced trading features to DEXs, e.g. stop losses/buys #4

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# Modifications
This is a fork of the Cosmos DEX for the Cosmos DeFi hackathon at SF Blockchain Week 2019, for which we came 4th overall out of 50 or so teams. This is one of the 2 submitted projects, which adds the ability to do things in the future to a DEX, which is part of my overall idea for 'Active Smart Contracts' (ASCs). The general problem being solved is that blockchains are passive and require a user to submit a transaction every time they want to do something on-chain. In terms of trading, this requires a user to be online 24/7 or have a bot trading for them because trades almost always depend on certain conditions: limit orders, stop losses etc. This lack of basic trading features puts DEXs far behind CEXs in terms of usability and is severely inhibiting the growth of the DEX space. A user needs to be able to set some conditions for some actions (if the price of X token falls below Y value, buy etc) in the future, go offline, and have those actions executed under the right conditions.

The solution to allow actions on a blockchain to happen in the future is a cryptoeconomic one - essentially incentivising others to submit your transactions under certain conditions for you. In Cosmos, however, you can 'register' events to happen at the end of every block. I added a new type of action here, a stop loss, which constantly checks the registered stop losses to see if any of the conditions are true which enable them to be executed.

The rest of this README is the original.






# Disclaimer

The code hosted in this repository is a **technology preview** and is suitable for **demo purposes only**. The features provided by this draft implementation are not meant to be functionally complete and are not suitable for deployment in production.
Expand Down
3 changes: 2 additions & 1 deletion app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func NewDexApp(
bam.MainStoreKey, auth.StoreKey, staking.StoreKey,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
params.StoreKey, assettypes.StoreKey, markettypes.StoreKey,
ordertypes.StoreKey,
ordertypes.StoreKey, ordertypes.LastPriceKey,
)
tkeys := sdk.NewTransientStoreKeys(staking.TStoreKey, params.TStoreKey)

Expand Down Expand Up @@ -199,6 +199,7 @@ func NewDexApp(
app.MarketKeeper,
app.AssetKeeper,
keys[ordertypes.StoreKey],
keys[ordertypes.LastPriceKey],
queue,
app.Cdc,
)
Expand Down
1 change: 1 addition & 0 deletions x/order/client/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
Short: "manages orders",
}
txCmd.AddCommand(client.PostCommands(
GetCmdStop(cdc),
GetCmdPost(cdc),
GetCmdCancel(cdc),
)...)
Expand Down
63 changes: 63 additions & 0 deletions x/order/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"errors"
"fmt"
"math"
"strconv"
"strings"
Expand All @@ -17,6 +18,68 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

func GetCmdStop(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "stop [market-id] [direction] [price] [quantity] [time-in-force-blocks] [init-price] [relayer-address-hex] [relayer-fee]",
Short: "posts an order under certain price conditions, i.e. a stop-loss or stop-buy",
Args: cobra.ExactArgs(8),
RunE: func(cmd *cobra.Command, args []string) error {
ctx, _, err := cliutil.BuildEnsuredCtx(cdc)
if err != nil {
return err
}

marketID := store.NewEntityIDFromString(args[0])
var direction matcheng.Direction
dirArg := strings.ToLower(args[1])
if dirArg == "bid" {
direction = matcheng.Bid
} else if dirArg == "ask" {
direction = matcheng.Ask
} else {
return errors.New("invalid direction")
}

price, err := sdk.ParseUint(args[2])
if err != nil {
return err
}
quantity, err := sdk.ParseUint(args[3])
if err != nil {
return err
}
tif, err := strconv.ParseUint(args[4], 10, 64)
if err != nil {
return err
}
if tif > math.MaxUint16 {
return errors.New("time in force too large")
}

initPrice, err := sdk.ParseUint(args[5])
if err != nil {
return err
}
relayedAddress, err := sdk.AccAddressFromHex(args[6])
if err != nil {
return err
}
err = sdk.VerifyAddressFormat(relayedAddress)
if err != nil {
return errors.New("invalid address format")
}
relayerFee, err := sdk.ParseCoins(args[7])
if err != nil {
return err
}

msg := types.NewMsgStop(ctx.GetFromAddress(), marketID, direction, price, quantity, uint16(tif), initPrice, relayedAddress, relayerFee)
fmt.Println(msg)
return nil
},
}
}

func GetCmdPost(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "post [market-id] [direction] [price] [quantity] [time-in-force-blocks]",
Expand Down
55 changes: 55 additions & 0 deletions x/order/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ var logger = log.WithModule("order")
func NewHandler(keeper Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
switch msg := msg.(type) {
case types.MsgStop:
return handleMsgStop(ctx, keeper, msg)
case types.MsgPost:
return handleMsgPost(ctx, keeper, msg)
case types.MsgCancel:
Expand All @@ -25,6 +27,59 @@ func NewHandler(keeper Keeper) sdk.Handler {
}
}

func handleMsgStop(ctx sdk.Context, keeper Keeper, msg types.MsgStop) sdk.Result {
currentPrice := keeper.GetPrice(ctx, msg.Post.MarketID)
// Shouldn't be triggered in these ranges
if msg.Post.Price.GT(msg.InitPrice) && currentPrice.LT(msg.Post.Price) {
return sdk.Result{
Log: fmt.Sprintf("current price not in triggering range"),
}
}
if msg.Post.Price.LT(msg.InitPrice) && currentPrice.GT(msg.Post.Price) {
return sdk.Result{
Log: fmt.Sprintf("current price not in triggering range"),
}
}

order, err := keeper.Post(
ctx,
msg.Post.Owner,
msg.Post.MarketID,
msg.Post.Direction,
msg.Post.Price,
msg.Post.Quantity,
msg.Post.TimeInForce,
)
if err != nil {
return err.Result()
}
logger.Info(
"stop order",
"id", order.ID.String(),
"market_id", order.MarketID.String(),
"price", order.Price.String(),
"quantity", order.Quantity.String(),
"direction", order.Direction.String(),
)

err = keeper.bankKeeper.SendCoins(ctx, msg.Post.Owner, msg.Relayer, msg.RelayFee)
if err == nil {
return err.Result()
}
logger.Info(
"stop order",
"id", order.ID.String(),
"market_id", order.MarketID.String(),
"price", order.Price.String(),
"quantity", order.Quantity.String(),
"direction", order.Direction.String(),
)

return sdk.Result{
Log: fmt.Sprintf("order_id:%s", order.ID),
}
}

func handleMsgPost(ctx sdk.Context, keeper Keeper, msg types.MsgPost) sdk.Result {
order, err := keeper.Post(
ctx,
Expand Down
19 changes: 18 additions & 1 deletion x/order/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,18 @@ type Keeper struct {
marketKeeper market.Keeper
assetKeeper asset.Keeper
storeKey sdk.StoreKey
latestPrices sdk.StoreKey
queue types.Backend
cdc *codec.Codec
}

func NewKeeper(bk bank.Keeper, mk market.Keeper, ak asset.Keeper, sk sdk.StoreKey, queue types.Backend, cdc *codec.Codec) Keeper {
func NewKeeper(bk bank.Keeper, mk market.Keeper, ak asset.Keeper, sk, lp sdk.StoreKey, queue types.Backend, cdc *codec.Codec) Keeper {
return Keeper{
bankKeeper: bk,
marketKeeper: mk,
assetKeeper: ak,
storeKey: sk,
latestPrices: lp,
queue: queue,
cdc: cdc,
}
Expand Down Expand Up @@ -205,3 +207,18 @@ func (k Keeper) doIterator(iter sdk.Iterator, cb IteratorCB) {
func orderKey(id store.EntityID) []byte {
return store.PrefixKeyString(valKey, id.Bytes())
}

func (k Keeper) SetPrice(ctx sdk.Context, mID store.EntityID, price sdk.Uint) {
store := ctx.KVStore(k.latestPrices)
mn := mID.String()
stringPrice := price.String()
store.Set([]byte(mn), []byte(stringPrice))
}

func (k Keeper) GetPrice(ctx sdk.Context, mID store.EntityID) sdk.Uint {
store := ctx.KVStore(k.latestPrices)
mn := mID.String()
currentPriceBytes := store.Get([]byte(mn))
currentPriceString := string(currentPriceBytes)
return sdk.NewUintFromString(currentPriceString)
}
1 change: 1 addition & 0 deletions x/order/types/codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import "github.com/cosmos/cosmos-sdk/codec"
var ModuleCdc = codec.New()

func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgStop{}, "order/Stop", nil)
cdc.RegisterConcrete(MsgPost{}, "order/Post", nil)
cdc.RegisterConcrete(MsgCancel{}, "order/Cancel", nil)
}
68 changes: 68 additions & 0 deletions x/order/types/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,74 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

type MsgStop struct {
Post MsgPost
// The price at the time the tx is signed.
// Needed to determine which direction the price travels in order to
// know whether to trigger above or below MsgPost.Price
InitPrice sdk.Uint
Relayer sdk.AccAddress
RelayFee sdk.Coins
}

func NewMsgStop(owner sdk.AccAddress, marketID store.EntityID, direction matcheng.Direction, price sdk.Uint, quantity sdk.Uint, tif uint16, initPrice sdk.Uint, relayer sdk.AccAddress, relayFee sdk.Coins) MsgStop {
return MsgStop{
Post: MsgPost{
Owner: owner,
MarketID: marketID,
Direction: direction,
Price: price,
Quantity: quantity,
TimeInForce: tif,
},
InitPrice: initPrice,
Relayer: relayer,
RelayFee: relayFee,
}
}

func (msg MsgStop) Route() string {
return "order"
}

func (msg MsgStop) Type() string {
return "stop"
}

func (msg MsgStop) ValidateBasic() sdk.Error {
// Don't need to check a bool like Buy because t & f should be able to be used here?
if !msg.Post.MarketID.IsDefined() {
return sdk.ErrUnauthorized("invalid market ID")
}
if msg.Post.Price.IsZero() {
return sdk.ErrInvalidCoins("price cannot be zero")
}
if msg.Post.Quantity.IsZero() {
return sdk.ErrInvalidCoins("quantity cannot be zero")
}
if msg.Post.TimeInForce == 0 {
return sdk.ErrInternal("time in force cannot be zero")
}
if msg.Post.TimeInForce > MaxTimeInForce {
return sdk.ErrInternal("time in force cannot be larger than 600")
}
if msg.InitPrice.IsZero() {
return sdk.ErrInvalidCoins("pastPrice cannot be zero")
}
if msg.RelayFee.IsZero() {
return sdk.ErrInvalidCoins("relayFee cannot be zero")
}
return nil
}

func (msg MsgStop) GetSignBytes() []byte {
return serde.MustMarshalSortedJSON(msg)
}

func (msg MsgStop) GetSigners() []sdk.AccAddress {
return msg.Post.GetSigners()
}

type MsgPost struct {
Owner sdk.AccAddress
MarketID store.EntityID
Expand Down
7 changes: 4 additions & 3 deletions x/order/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import (
)

const (
ModuleName = "order"
RouterKey = ModuleName
StoreKey = ModuleName
ModuleName = "order"
RouterKey = ModuleName
StoreKey = ModuleName
LastPriceKey = "last_price"
)

const MaxTimeInForce = 600
Expand Down