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

mm: Epoch reporting #2808

Merged
merged 1 commit into from
Oct 30, 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
1 change: 1 addition & 0 deletions client/cmd/testbinance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ func (f *fakeBinance) run(ctx context.Context) {
case <-ctx.Done():
return
}

f.withdrawalHistoryMtx.Lock()
for transferID, withdraw := range f.withdrawalHistory {
if withdraw.txID.Load() != nil {
Expand Down
10 changes: 10 additions & 0 deletions client/core/bookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,16 @@ func (dc *dexConnection) bookie(marketID string) *bookie {
return dc.books[marketID]
}

func (dc *dexConnection) midGap(base, quote uint32) (midGap uint64, err error) {
marketID := marketName(base, quote)
booky := dc.bookie(marketID)
if booky == nil {
return 0, fmt.Errorf("no bookie found for market %s", marketID)
}

return booky.MidGap()
}

// syncBook subscribes to the order book and returns the book and a BookFeed to
// receive order book updates. The BookFeed must be Close()d when it is no
// longer in use. Use stopBook to unsubscribed and clean up the feed.
Expand Down
100 changes: 80 additions & 20 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -5749,34 +5749,36 @@ func (c *Core) PreOrder(form *TradeForm) (*OrderEstimate, error) {
}, nil
}

// MultiTradeResult is returned from MultiTrade. Some orders may be placed
// successfully, while others may fail.
type MultiTradeResult struct {
Order *Order
Error error
}

// MultiTrade is used to place multiple standing limit orders on the same
// side of the same market simultaneously.
func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) ([]*Order, error) {
func (c *Core) MultiTrade(pw []byte, form *MultiTradeForm) []*MultiTradeResult {
results := make([]*MultiTradeResult, len(form.Placements))
reqs, err := c.prepareMultiTradeRequests(pw, form)
if err != nil {
return nil, err
for i := range results {
results[i] = &MultiTradeResult{Error: err}
}
return results
}

orders := make([]*Order, 0, len(reqs))

for _, req := range reqs {
// return last error below if none of the orders succeeded
for i, req := range reqs {
var corder *Order
corder, err = c.sendTradeRequest(req)
if err != nil {
c.log.Errorf("failed to send trade request: %v", err)
results[i] = &MultiTradeResult{Error: err}
continue
}
orders = append(orders, corder)
}
if len(orders) < len(reqs) {
c.log.Errorf("failed to send %d of %d trade requests", len(reqs)-len(orders), len(reqs))
}
if len(orders) == 0 {
return nil, err
results[i] = &MultiTradeResult{Order: corder}
}

return orders, nil
return results
}

// TxHistory returns all the transactions a wallet has made. If refID
Expand Down Expand Up @@ -5911,7 +5913,7 @@ func (c *Core) prepareForTradeRequestPrep(pw []byte, base, quote uint32, host st
return fail(err)
}
if dc.acct.suspended() {
return fail(newError(suspendedAcctErr, "may not trade while account is suspended"))
return fail(newError(suspendedAcctErr, "%w", ErrAccountSuspended))
}

mktID := marketName(base, quote)
Expand Down Expand Up @@ -5948,12 +5950,10 @@ func (c *Core) prepareForTradeRequestPrep(pw []byte, base, quote uint32, host st
w.mtx.RLock()
defer w.mtx.RUnlock()
if w.peerCount < 1 {
return fmt.Errorf("%s wallet has no network peers (check your network or firewall)",
unbip(w.AssetID))
return &WalletNoPeersError{w.AssetID}
}
if !w.syncStatus.Synced {
return fmt.Errorf("%s still syncing. progress = %.2f%%", unbip(w.AssetID),
w.syncStatus.BlockProgress()*100)
return &WalletSyncError{w.AssetID, w.syncStatus.BlockProgress()}
}
return nil
}
Expand Down Expand Up @@ -10897,3 +10897,63 @@ func (c *Core) RedeemGeocode(appPW, code []byte, msg string) (dex.Bytes, uint64,
func (c *Core) ExtensionModeConfig() *ExtensionModeConfig {
return c.extensionModeConfig
}

// calcParcelLimit computes the users score-scaled user parcel limit.
func calcParcelLimit(tier int64, score, maxScore int32) uint32 {
// Users limit starts at 2 parcels per tier.
lowerLimit := tier * dex.PerTierBaseParcelLimit
// Limit can scale up to 3x with score.
upperLimit := lowerLimit * dex.ParcelLimitScoreMultiplier
limitRange := upperLimit - lowerLimit
var scaleFactor float64
if score > 0 {
scaleFactor = float64(score) / float64(maxScore)
}
return uint32(lowerLimit) + uint32(math.Round(scaleFactor*float64(limitRange)))
}

// TradingLimits returns the number of parcels the user can trade on an
// exchange and the amount that are currently being traded.
func (c *Core) TradingLimits(host string) (userParcels, parcelLimit uint32, err error) {
dc, _, err := c.dex(host)
if err != nil {
return 0, 0, err
}

cfg := dc.config()
dc.acct.authMtx.RLock()
rep := dc.acct.rep
dc.acct.authMtx.RUnlock()

mkts := make(map[string]*msgjson.Market, len(cfg.Markets))
for _, mkt := range cfg.Markets {
mkts[mkt.Name] = mkt
}
mktTrades := make(map[string][]*trackedTrade)
for _, t := range dc.trackedTrades() {
mktTrades[t.mktID] = append(mktTrades[t.mktID], t)
}

parcelLimit = calcParcelLimit(rep.EffectiveTier(), rep.Score, int32(cfg.MaxScore))
for mktID, trades := range mktTrades {
mkt := mkts[mktID]
if mkt == nil {
c.log.Warnf("trade for unknown market %q", mktID)
continue
}

var midGap, mktWeight uint64
for _, t := range trades {
if t.isEpochOrder() && midGap == 0 {
midGap, err = dc.midGap(mkt.Base, mkt.Quote)
if err != nil && !errors.Is(err, orderbook.ErrEmptyOrderbook) {
return 0, 0, err
}
}
mktWeight += t.marketWeight(midGap, mkt.LotSize)
}
userParcels += uint32(mktWeight / (uint64(mkt.ParcelSize) * mkt.LotSize))
}

return userParcels, parcelLimit, nil
}
114 changes: 113 additions & 1 deletion client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection,
Base: tUTXOAssetA.ID,
Quote: tUTXOAssetB.ID,
LotSize: dcrBtcLotSize,
ParcelSize: 100,
ParcelSize: 1,
RateStep: dcrBtcRateStep,
EpochLen: 60000,
MarketBuyBuffer: 1.1,
Expand Down Expand Up @@ -11075,6 +11075,118 @@ func TestPokesCachePokes(t *testing.T) {
}
}

func TestTradingLimits(t *testing.T) {
rig := newTestRig()
defer rig.shutdown()

checkTradingLimits := func(expectedUserParcels, expectedParcelLimit uint32) {
t.Helper()

userParcels, parcelLimit, err := rig.core.TradingLimits(tDexHost)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if userParcels != expectedUserParcels {
t.Fatalf("expected user parcels %d, got %d", expectedUserParcels, userParcels)
}

if parcelLimit != expectedParcelLimit {
t.Fatalf("expected parcel limit %d, got %d", expectedParcelLimit, parcelLimit)
}
}

rig.dc.acct.rep.BondedTier = 10
book := newBookie(rig.dc, tUTXOAssetA.ID, tUTXOAssetB.ID, nil, tLogger)
rig.dc.books[tDcrBtcMktName] = book
checkTradingLimits(0, 20)

oids := []order.OrderID{
{0x01}, {0x02}, {0x03}, {0x04}, {0x05},
}

// Add an epoch order, 2 lots not likely taker
ord := &order.LimitOrder{
Force: order.StandingTiF,
P: order.Prefix{ServerTime: time.Now()},
T: order.Trade{
Sell: true,
Quantity: dcrBtcLotSize * 2,
},
}
tracker := &trackedTrade{
Order: ord,
preImg: newPreimage(),
mktID: tDcrBtcMktName,
db: rig.db,
dc: rig.dc,
metaData: &db.OrderMetaData{
Status: order.OrderStatusEpoch,
},
}
rig.dc.trades[oids[0]] = tracker
checkTradingLimits(2, 20)

// Add another epoch order, 2 lots, likely taker, so 2x
ord = &order.LimitOrder{
Force: order.ImmediateTiF,
P: order.Prefix{ServerTime: time.Now()},
T: order.Trade{
Sell: true,
Quantity: dcrBtcLotSize * 2,
},
}
tracker = &trackedTrade{
Order: ord,
preImg: newPreimage(),
mktID: tDcrBtcMktName,
db: rig.db,
dc: rig.dc,
metaData: &db.OrderMetaData{
Status: order.OrderStatusEpoch,
},
}
rig.dc.trades[oids[1]] = tracker
checkTradingLimits(6, 20)

// Add partially filled booked order
ord = &order.LimitOrder{
P: order.Prefix{ServerTime: time.Now()},
T: order.Trade{
Sell: true,
Quantity: dcrBtcLotSize * 2,
FillAmt: dcrBtcLotSize,
},
}
tracker = &trackedTrade{
Order: ord,
preImg: newPreimage(),
mktID: tDcrBtcMktName,
db: rig.db,
dc: rig.dc,
metaData: &db.OrderMetaData{
Status: order.OrderStatusBooked,
},
}
rig.dc.trades[oids[2]] = tracker
checkTradingLimits(7, 20)

// Add settling match to the booked order
tracker.matches = map[order.MatchID]*matchTracker{
{0x01}: {
MetaMatch: db.MetaMatch{
UserMatch: &order.UserMatch{
Quantity: dcrBtcLotSize,
},
MetaData: &db.MatchMetaData{
Proof: db.MatchProof{},
},
},
},
}
checkTradingLimits(8, 20)
}

func TestTakeAction(t *testing.T) {
rig := newTestRig()
defer rig.shutdown()
Expand Down
23 changes: 23 additions & 0 deletions client/core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,26 @@ func UnwrapErr(err error) error {
}
return UnwrapErr(InnerErr)
}

var (
ErrAccountSuspended = errors.New("may not trade while account is suspended")
)

// WalletNoPeersError should be returned when a wallet has no network peers.
type WalletNoPeersError struct {
AssetID uint32
}

func (e *WalletNoPeersError) Error() string {
return fmt.Sprintf("%s wallet has no network peers (check your network or firewall)", unbip(e.AssetID))
}

// WalletSyncError should be returned when a wallet is still syncing.
type WalletSyncError struct {
AssetID uint32
Progress float32
}

func (e *WalletSyncError) Error() string {
return fmt.Sprintf("%s still syncing. progress = %.2f%%", unbip(e.AssetID), e.Progress*100)
}
73 changes: 73 additions & 0 deletions client/core/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -3819,6 +3819,79 @@ func (t *trackedTrade) orderAccelerationParameters() (swapCoins, accelerationCoi
return swapCoins, accelerationCoins, dex.Bytes(t.metaData.ChangeCoin), requiredForRemainingSwaps, nil
}

func (t *trackedTrade) likelyTaker(midGap uint64) bool {
if t.Type() == order.MarketOrderType {
return true
}
lo := t.Order.(*order.LimitOrder)
if lo.Force == order.ImmediateTiF {
return true
}

if midGap == 0 {
return false
}

if lo.Sell {
return lo.Rate < midGap
}

return lo.Rate > midGap
}

func (t *trackedTrade) baseQty(midGap, lotSize uint64) uint64 {
qty := t.Trade().Quantity

if t.Type() == order.MarketOrderType && !t.Trade().Sell {
if midGap == 0 {
qty = lotSize
} else {
qty = calc.QuoteToBase(midGap, qty)
}
}

return qty
}

func (t *trackedTrade) epochWeight(midGap, lotSize uint64) uint64 {
if t.status() >= order.OrderStatusBooked {
return 0
}

if t.likelyTaker(midGap) {
return 2 * t.baseQty(midGap, lotSize)
}

return t.baseQty(midGap, lotSize)
}

func (t *trackedTrade) bookedWeight() uint64 {
if t.status() != order.OrderStatusBooked {
return 0
}

return t.Trade().Remaining()
}

func (t *trackedTrade) settlingWeight() (weight uint64) {
for _, match := range t.matches {
if (match.Side == order.Maker && match.Status >= order.MakerRedeemed) ||
(match.Side == order.Taker && match.Status >= order.MatchComplete) {
continue
}
weight += match.Quantity
}
return
}

func (t *trackedTrade) isEpochOrder() bool {
return t.status() == order.OrderStatusEpoch
}

func (t *trackedTrade) marketWeight(midGap, lotSize uint64) uint64 {
return t.epochWeight(midGap, lotSize) + t.bookedWeight() + t.settlingWeight()
}

// mapifyCoins converts the slice of coins to a map keyed by hex coin ID.
func mapifyCoins(coins asset.Coins) map[string]asset.Coin {
coinMap := make(map[string]asset.Coin, len(coins))
Expand Down
Loading
Loading