Skip to content

Commit

Permalink
client/mm: Token asset balance management
Browse files Browse the repository at this point in the history
This updates the market making balance management system to handle token
assets.

A new `asset.TokenCoin` interface is added that provides a `ParentAssetValue`
function that returns amount of the parent asset that is reserved for fees.
This allows a `ParentAssetLockedAmt` field to be added to `core.Order`,
which allows the market making code to know how much of the parent asset
was reserved when making a trade.
  • Loading branch information
martonp committed Oct 15, 2023
1 parent a5151ea commit 812ec8a
Show file tree
Hide file tree
Showing 8 changed files with 1,223 additions and 156 deletions.
7 changes: 7 additions & 0 deletions client/asset/eth/fundingcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/binary"
"fmt"

"decred.org/dcrdex/client/asset"
"decred.org/dcrdex/dex"
"github.com/ethereum/go-ethereum/common"
)
Expand Down Expand Up @@ -78,6 +79,8 @@ type tokenFundingCoin struct {
fees uint64
}

var _ asset.TokenCoin = (*tokenFundingCoin)(nil)

// String creates a human readable string.
func (c *tokenFundingCoin) String() string {
return fmt.Sprintf("address: %s, amount:%v, fees:%v", c.addr, c.amt, c.fees)
Expand All @@ -102,6 +105,10 @@ func (c *tokenFundingCoin) Value() uint64 {
return c.amt
}

func (c *tokenFundingCoin) ParentValue() uint64 {
return c.fees
}

// decodeTokenFundingCoin decodes a byte slice into an tokenFundingCoinID.
func decodeTokenFundingCoin(coinID []byte) (*tokenFundingCoin, error) {
if len(coinID) != tokenFundingCoinIDSize {
Expand Down
7 changes: 7 additions & 0 deletions client/asset/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,13 @@ type Coin interface {
Value() uint64
}

// TokenCoin is extends to Coin interface to include the amount locked
// of the parent asset to be used for fees.
type TokenCoin interface {
Coin
ParentValue() uint64
}

type RecoveryCoin interface {
// RecoveryID is an ID that can be used to re-establish funding state during
// startup. If a Coin implements RecoveryCoin, the RecoveryID will be used
Expand Down
19 changes: 19 additions & 0 deletions client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,7 @@ func (t *trackedTrade) coreOrderInternal() *Order {

corder.Epoch = t.dc.marketEpoch(t.mktID, t.Prefix().ServerTime)
corder.LockedAmt = t.lockedAmount()
corder.FeeAssetLockedAmt = t.feeLockedAmount()
corder.ReadyToTick = t.readyToTick
corder.RedeemLockedAmt = t.redemptionLocked
corder.RefundLockedAmt = t.refundLocked
Expand Down Expand Up @@ -691,6 +692,24 @@ func (t *trackedTrade) lockedAmount() (locked uint64) {
return
}

func (t *trackedTrade) feeLockedAmount() (locked uint64) {
if t.coinsLocked {
// This implies either no swap has been sent, or the trade has been
// resumed on restart after a swap that produced locked change (partial
// fill and still booked) since restarting loads into coins/coinsLocked.
for _, coin := range t.coins {
if tokenCoin, is := coin.(asset.TokenCoin); is {
locked += tokenCoin.ParentValue()
}
}
} else if t.changeLocked && t.change != nil { // change may be returned but unlocked if the last swap has been sent
if tokenCoin, is := t.change.(asset.TokenCoin); is {
locked += tokenCoin.ParentValue()
}
}
return
}

// token is a shortened representation of the order ID.
func (t *trackedTrade) token() string {
id := t.ID()
Expand Down
49 changes: 26 additions & 23 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,29 +376,32 @@ func matchFromMetaMatchWithConfs(ord order.Order, metaMatch *db.MetaMatch, swapC
// Order is core's general type for an order. An order may be a market, limit,
// or cancel order. Some fields are only relevant to particular order types.
type Order struct {
Host string `json:"host"`
BaseID uint32 `json:"baseID"`
BaseSymbol string `json:"baseSymbol"`
QuoteID uint32 `json:"quoteID"`
QuoteSymbol string `json:"quoteSymbol"`
MarketID string `json:"market"`
Type order.OrderType `json:"type"`
ID dex.Bytes `json:"id"` // Can be empty if part of an InFlightOrder
Stamp uint64 `json:"stamp"` // Server's time stamp
SubmitTime uint64 `json:"submitTime"`
Sig dex.Bytes `json:"sig"`
Status order.OrderStatus `json:"status"`
Epoch uint64 `json:"epoch"`
Qty uint64 `json:"qty"`
Sell bool `json:"sell"`
Filled uint64 `json:"filled"`
Matches []*Match `json:"matches"`
Cancelling bool `json:"cancelling"`
Canceled bool `json:"canceled"`
FeesPaid *FeeBreakdown `json:"feesPaid"`
AllFeesConfirmed bool `json:"allFeesConfirmed"`
FundingCoins []*Coin `json:"fundingCoins"`
LockedAmt uint64 `json:"lockedamt"`
Host string `json:"host"`
BaseID uint32 `json:"baseID"`
BaseSymbol string `json:"baseSymbol"`
QuoteID uint32 `json:"quoteID"`
QuoteSymbol string `json:"quoteSymbol"`
MarketID string `json:"market"`
Type order.OrderType `json:"type"`
ID dex.Bytes `json:"id"` // Can be empty if part of an InFlightOrder
Stamp uint64 `json:"stamp"` // Server's time stamp
SubmitTime uint64 `json:"submitTime"`
Sig dex.Bytes `json:"sig"`
Status order.OrderStatus `json:"status"`
Epoch uint64 `json:"epoch"`
Qty uint64 `json:"qty"`
Sell bool `json:"sell"`
Filled uint64 `json:"filled"`
Matches []*Match `json:"matches"`
Cancelling bool `json:"cancelling"`
Canceled bool `json:"canceled"`
FeesPaid *FeeBreakdown `json:"feesPaid"`
AllFeesConfirmed bool `json:"allFeesConfirmed"`
FundingCoins []*Coin `json:"fundingCoins"`
LockedAmt uint64 `json:"lockedamt"`
// FeeAssetLockedAmt is the swap fees locked when the "from asset" of a
// trade is a token. This wil be 0 if the "from asset" is not a token.
FeeAssetLockedAmt uint64 `json:"feeAssetLockedAmt"`
RedeemLockedAmt uint64 `json:"redeemLockedAmt"`
RefundLockedAmt uint64 `json:"refundLockedAmt"`
AccelerationCoins []*Coin `json:"accelerationCoins"`
Expand Down
5 changes: 5 additions & 0 deletions client/mm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ type BotConfig struct {
QuoteBalanceType BalanceType `json:"quoteBalanceType"`
QuoteBalance uint64 `json:"quoteBalance"`

BaseFeeAssetBalanceType BalanceType `json:"baseFeeAssetBalanceType"`
BaseFeeAssetBalance uint64 `json:"baseFeeAssetBalance"`
QuoteFeeAssetBalanceType BalanceType `json:"quoteFeeAssetBalanceType"`
QuoteFeeAssetBalance uint64 `json:"quoteFeeAssetBalance"`

// Only applicable for arb bots.
CEXCfg *BotCEXCfg `json:"cexCfg"`

Expand Down
62 changes: 54 additions & 8 deletions client/mm/mm.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,49 @@ func (m *MarketMaker) setupBalances(cfgs []*BotConfig, cexes map[string]libxc.CE
},
}

trackTokenFeeAsset := func(base bool) error {
assetID := cfg.QuoteAsset
balType := cfg.QuoteFeeAssetBalanceType
balAmount := cfg.QuoteFeeAssetBalance
baseOrQuote := "quote"
if base {
assetID = cfg.BaseAsset
balType = cfg.BaseFeeAssetBalanceType
balAmount = cfg.BaseFeeAssetBalance
baseOrQuote = "base"
}
token := asset.TokenInfo(assetID)
if token == nil {
return nil
}
err := trackAssetOnDEX(token.ParentID)
if err != nil {
return err
}
tokenFeeAsset := dexBalanceTracker[token.ParentID]
tokenFeeAssetRequired := calcBalance(balType, balAmount, tokenFeeAsset.available)
if tokenFeeAssetRequired == 0 {
return fmt.Errorf("%s fee asset balance is zero for market %s", baseOrQuote, mktID)
}
if tokenFeeAssetRequired > tokenFeeAsset.available-tokenFeeAsset.reserved {
return fmt.Errorf("insufficient balance for asset %d", token.ParentID)
}
tokenFeeAsset.reserved += tokenFeeAssetRequired
if _, found := m.botBalances[mktID].balances[token.ParentID]; !found {
m.botBalances[mktID].balances[token.ParentID] = &botBalance{}
}
m.botBalances[mktID].balances[token.ParentID].Available += tokenFeeAssetRequired
return nil
}
err = trackTokenFeeAsset(true)
if err != nil {
return err
}
err = trackTokenFeeAsset(false)
if err != nil {
return err
}

// Calculate CEX balances
if cfg.CEXCfg != nil {
baseSymbol := dex.BipIDSymbol(cfg.BaseAsset)
Expand Down Expand Up @@ -897,12 +940,15 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) {
}

if !orderInfo.excessFeesReturned && o.AllFeesConfirmed {
fromFeeAsset := feeAsset(fromAsset)
toFeeAsset := feeAsset(toAsset)

// Return excess swap fees
maxSwapFees := swappedMatches * orderInfo.singleLotSwapFees
if maxSwapFees > o.FeesPaid.Swap {
balanceMods := []*balanceMod{
{balanceModIncrease, fromAsset, balTypeAvailable, maxSwapFees - o.FeesPaid.Swap},
{balanceModDecrease, fromAsset, balTypeFundingOrder, maxSwapFees},
{balanceModIncrease, fromFeeAsset, balTypeAvailable, maxSwapFees - o.FeesPaid.Swap},
{balanceModDecrease, fromFeeAsset, balTypeFundingOrder, maxSwapFees},
}
m.modifyBotBalance(orderInfo.bot, balanceMods)
} else if maxSwapFees < o.FeesPaid.Swap {
Expand All @@ -913,8 +959,8 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) {
if orderInfo.initialRedeemFeesLocked > 0 { // AccountLocker
if orderInfo.initialRedeemFeesLocked > o.FeesPaid.Redemption {
balanceMods := []*balanceMod{
{balanceModIncrease, toAsset, balTypeAvailable, orderInfo.initialRedeemFeesLocked - o.FeesPaid.Redemption},
{balanceModDecrease, toAsset, balTypeFundingOrder, orderInfo.initialRedeemFeesLocked},
{balanceModIncrease, toFeeAsset, balTypeAvailable, orderInfo.initialRedeemFeesLocked - o.FeesPaid.Redemption},
{balanceModDecrease, toFeeAsset, balTypeFundingOrder, orderInfo.initialRedeemFeesLocked},
}
m.modifyBotBalance(orderInfo.bot, balanceMods)
} else {
Expand All @@ -925,7 +971,7 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) {
maxRedeemFees := redeemedLots * orderInfo.singleLotRedeemFees
if maxRedeemFees > o.FeesPaid.Redemption {
balanceMods := []*balanceMod{
{balanceModIncrease, toAsset, balTypeAvailable, maxRedeemFees - o.FeesPaid.Redemption},
{balanceModIncrease, toFeeAsset, balTypeAvailable, maxRedeemFees - o.FeesPaid.Redemption},
}
m.modifyBotBalance(orderInfo.bot, balanceMods)
} else if maxRedeemFees < o.FeesPaid.Redemption {
Expand All @@ -938,16 +984,16 @@ func (m *MarketMaker) handleOrderUpdate(o *core.Order) {
if orderInfo.initialRefundFeesLocked > 0 { // AccountLocker
if orderInfo.initialRefundFeesLocked > o.FeesPaid.Refund {
balanceMods := []*balanceMod{
{balanceModIncrease, fromAsset, balTypeAvailable, orderInfo.initialRefundFeesLocked - o.FeesPaid.Refund},
{balanceModDecrease, fromAsset, balTypeFundingOrder, orderInfo.initialRefundFeesLocked},
{balanceModIncrease, fromFeeAsset, balTypeAvailable, orderInfo.initialRefundFeesLocked - o.FeesPaid.Refund},
{balanceModDecrease, fromFeeAsset, balTypeFundingOrder, orderInfo.initialRefundFeesLocked},
}
m.modifyBotBalance(orderInfo.bot, balanceMods)
}
} else {
maxRefundFees := refundedMatches * orderInfo.singleLotRefundFees
if maxRefundFees > o.FeesPaid.Refund {
balanceMods := []*balanceMod{
{balanceModIncrease, fromAsset, balTypeAvailable, maxRefundFees - o.FeesPaid.Refund},
{balanceModIncrease, fromFeeAsset, balTypeAvailable, maxRefundFees - o.FeesPaid.Refund},
}
m.modifyBotBalance(orderInfo.bot, balanceMods)
} else if maxRefundFees < o.FeesPaid.Refund {
Expand Down
Loading

0 comments on commit 812ec8a

Please sign in to comment.