Skip to content

Commit

Permalink
multi: market overview and price feed
Browse files Browse the repository at this point in the history
Add new price_feed subscription. The response to the subscription 
request contains the spot prices for all known markets. Subscribers will 
then receive an update to the spot price for every market at the end of 
every epoch.

server/market

BookRouter handles the subscriptions. The spot price is passed by Market 
as part of the sigDataEpochReport. It was already being generated by 
(*DataAPI).ReportEpoch, but we were discarding it.

dex/candles

(*Cache).Delta method updated to ignore leading and trailing zeros. 
Zeros occur when the book is empty.

client/core

Subscribe to the price feed right after fetching configuration, 
including after reconnect. Populate the new Market.Spot field. Send 
updates as notifications.

front-end

Display 24-hour percent change and current rate in the list on the left 
side of the markets view
  • Loading branch information
buck54321 authored Oct 19, 2021
1 parent 7c2058f commit bb05332
Show file tree
Hide file tree
Showing 24 changed files with 518 additions and 117 deletions.
45 changes: 45 additions & 0 deletions client/core/bookie.go
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,51 @@ func (dc *dexConnection) refreshServerConfig() error {
return nil
}

// subPriceFeed subscribes to the price_feed notification feed and primes the
// initial prices.
func (dc *dexConnection) subPriceFeed() {
var spots map[string]*msgjson.Spot
err := sendRequest(dc.WsConn, msgjson.PriceFeedRoute, nil, &spots, DefaultResponseTimeout)
if err != nil {
var msgErr *msgjson.Error
// Ignore old servers' errors.
if !errors.As(err, &msgErr) || msgErr.Code != msgjson.UnknownMessageType {
dc.log.Errorf("unable to fetch market overview: %w", err)
}
return
}

// We expect there to be a map in handlePriceUpdateNote.
if spots == nil {
spots = make(map[string]*msgjson.Spot)
}

dc.spotsMtx.Lock()
dc.spots = spots
dc.spotsMtx.Unlock()

dc.notify(newSpotPriceNote(dc.acct.host, spots))
}

// handlePriceUpdateNote handles the price_update note that is part of the
// price feed.
func handlePriceUpdateNote(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
spot := new(msgjson.Spot)
if err := msg.Unmarshal(spot); err != nil {
return fmt.Errorf("error unmarshaling price update: %v", err)
}
mktName, err := dex.MarketName(spot.BaseID, spot.QuoteID)
if err != nil {
return fmt.Errorf("error parsing market for base = %d, quote = %d: %v", spot.BaseID, spot.QuoteID, err)
}
dc.spotsMtx.Lock()
dc.spots[mktName] = spot
dc.spotsMtx.Unlock()

dc.notify(newSpotPriceNote(dc.acct.host, map[string]*msgjson.Spot{mktName: spot}))
return nil
}

// handleUnbookOrderMsg is called when an unbook_order notification is
// received.
func handleUnbookOrderMsg(_ *Core, dc *dexConnection, msg *msgjson.Message) error {
Expand Down
16 changes: 16 additions & 0 deletions client/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ type dexConnection struct {
pendingFee *pendingFeeState

reportingConnects uint32

spotsMtx sync.RWMutex
spots map[string]*msgjson.Spot
}

// DefaultResponseTimeout is the default timeout for responses after a request is
Expand Down Expand Up @@ -211,6 +214,13 @@ func (dc *dexConnection) marketMap() map[string]*Market {
marketMap[mktID] = mkt
}

// Populate spots.
dc.spotsMtx.RLock()
for mktID, mkt := range marketMap {
mkt.SpotPrice = dc.spots[mktID]
}
dc.spotsMtx.RUnlock()

return marketMap
}

Expand Down Expand Up @@ -5013,6 +5023,7 @@ func (c *Core) connectDEX(acctInfo *db.AccountInfo, temporary ...bool) (*dexConn
trades: make(map[order.OrderID]*trackedTrade),
apiVer: -1,
reportingConnects: reporting,
spots: make(map[string]*msgjson.Spot),
// On connect, must set: cfg, epoch, and assets.
}

Expand Down Expand Up @@ -5073,6 +5084,8 @@ func (c *Core) connectDEX(acctInfo *db.AccountInfo, temporary ...bool) (*dexConn
}
// handleConnectEvent sets dc.connected, even on first connect

go dc.subPriceFeed()

if listen {
c.log.Infof("Connected to DEX server at %s and listening for messages.", host)
} else {
Expand Down Expand Up @@ -5104,6 +5117,8 @@ func (c *Core) handleReconnect(host string) {
return
}

go dc.subPriceFeed()

if !dc.acct.locked() && dc.acct.feePaid() {
err = c.authDEX(dc)
if err != nil {
Expand Down Expand Up @@ -5350,6 +5365,7 @@ var noteHandlers = map[string]routeHandler{
msgjson.BookOrderRoute: handleBookOrderMsg,
msgjson.EpochOrderRoute: handleEpochOrderMsg,
msgjson.UnbookOrderRoute: handleUnbookOrderMsg,
msgjson.PriceUpdateRoute: handlePriceUpdateNote,
msgjson.UpdateRemainingRoute: handleUpdateRemainingMsg,
msgjson.EpochReportRoute: handleEpochReportMsg,
msgjson.SuspensionRoute: handleTradeSuspensionMsg,
Expand Down
1 change: 1 addition & 0 deletions client/core/core_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ func testDexConnection(ctx context.Context, crypter *tCrypter) (*dexConnection,
apiVer: serverdex.PreAPIVersion,
connected: 1,
reportingConnects: 1,
spots: make(map[string]*msgjson.Spot),
}, conn, acct
}

Expand Down
19 changes: 19 additions & 0 deletions client/core/notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"decred.org/dcrdex/client/db"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/msgjson"
)

// Notifications should use the following note type strings.
Expand All @@ -19,6 +20,7 @@ const (
NoteTypeEpoch = "epoch"
NoteTypeConnEvent = "conn"
NoteTypeBalance = "balance"
NoteTypeSpots = "spots"
NoteTypeWalletConfig = "walletconfig"
NoteTypeWalletState = "walletstate"
NoteTypeServerNotify = "notify"
Expand Down Expand Up @@ -354,6 +356,23 @@ func newBalanceNote(assetID uint32, bal *WalletBalance) *BalanceNote {
}
}

// SpotPriceNote is a notification of an update to the market's spot price.
type SpotPriceNote struct {
db.Notification
Host string `json:"host"`
Spots map[string]*msgjson.Spot `json:"spots"`
}

const TopicSpotsUpdate Topic = "SpotsUpdate"

func newSpotPriceNote(host string, spots map[string]*msgjson.Spot) *SpotPriceNote {
return &SpotPriceNote{
Notification: db.NewNotification(NoteTypeSpots, TopicSpotsUpdate, "", "", db.Data),
Host: host,
Spots: spots,
}
}

// DEXAuthNote is a notification regarding individual DEX authentication status.
type DEXAuthNote struct {
db.Notification
Expand Down
23 changes: 12 additions & 11 deletions client/core/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,17 +373,18 @@ func coreOrderFromTrade(ord order.Order, metaData *db.OrderMetaData) *Order {

// Market is market info.
type Market struct {
Name string `json:"name"`
BaseID uint32 `json:"baseid"`
BaseSymbol string `json:"basesymbol"`
QuoteID uint32 `json:"quoteid"`
QuoteSymbol string `json:"quotesymbol"`
LotSize uint64 `json:"lotsize"`
RateStep uint64 `json:"ratestep"`
EpochLen uint64 `json:"epochlen"`
StartEpoch uint64 `json:"startepoch"`
MarketBuyBuffer float64 `json:"buybuffer"`
Orders []*Order `json:"orders"`
Name string `json:"name"`
BaseID uint32 `json:"baseid"`
BaseSymbol string `json:"basesymbol"`
QuoteID uint32 `json:"quoteid"`
QuoteSymbol string `json:"quotesymbol"`
LotSize uint64 `json:"lotsize"`
RateStep uint64 `json:"ratestep"`
EpochLen uint64 `json:"epochlen"`
StartEpoch uint64 `json:"startepoch"`
MarketBuyBuffer float64 `json:"buybuffer"`
Orders []*Order `json:"orders"`
SpotPrice *msgjson.Spot `json:"spot"`
}

// BaseContractLocked is the amount of base asset locked in un-redeemed
Expand Down
35 changes: 35 additions & 0 deletions client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,9 @@ func mkMrkt(base, quote string) *core.Market {
marketStats[mktID] = [2]float64{midGap, maxQty}
}

rate := uint64(rand.Intn(1e3)) * rateStep
change24 := rand.Float64()*0.3 - .15

return &core.Market{
Name: fmt.Sprintf("%s_%s", base, quote),
BaseID: baseID,
Expand All @@ -166,6 +169,15 @@ func mkMrkt(base, quote string) *core.Market {
MarketBuyBuffer: rand.Float64() + 1,
EpochLen: uint64(epochDuration.Milliseconds()),
Orders: userOrders(mktID),
SpotPrice: &msgjson.Spot{
Stamp: encode.UnixMilliU(time.Now()),
BaseID: baseID,
QuoteID: quoteID,
Rate: rate,
// BookVolume: ,
Change24: change24,
// Vol24: ,
},
}
}

Expand Down Expand Up @@ -1259,13 +1271,36 @@ out:
quoteConnected = true
}
c.mtx.RUnlock()

if c.dexAddr == "" {
continue
}

c.noteFeed <- &core.EpochNotification{
Host: dexAddr,
MarketID: mktID,
Notification: db.NewNotification(core.NoteTypeEpoch, core.TopicEpoch, "", "", db.Data),
Epoch: getEpoch(),
}

rateStep := tExchanges[dexAddr].Markets[mktID].RateStep
rate := uint64(rand.Intn(1e3)) * rateStep
change24 := rand.Float64()*0.3 - .15

c.noteFeed <- &core.SpotPriceNote{
Host: dexAddr,
Notification: db.NewNotification(core.NoteTypeSpots, core.TopicSpotsUpdate, "", "", db.Data),
Spots: map[string]*msgjson.Spot{mktID: &msgjson.Spot{
Stamp: encode.UnixMilliU(time.Now()),
BaseID: baseID,
QuoteID: quoteID,
Rate: rate,
// BookVolume: ,
Change24: change24,
// Vol24: ,
}},
}

// randomize the balance
if baseID != 141 && baseConnected { // komodo unsupported
c.noteFeed <- randomBalanceNote(baseID)
Expand Down
1 change: 0 additions & 1 deletion client/webserver/site/src/css/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,6 @@ div.form-closer {

.micro-icon {
position: relative;
bottom: 2px;
}

#tooltip {
Expand Down
60 changes: 50 additions & 10 deletions client/webserver/site/src/css/market.scss
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
div.marketlist {
width: 175px;
// width: 175px;
border-right: 1px solid #626262;
z-index: 99;
background-color: #dbdbdb;
min-width: 175px;

.selected {
border-style: solid none solid none;
Expand All @@ -13,31 +14,70 @@ div.marketlist {
.header {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}

.xc:not(:first-child) .header {
padding-top: 15px;
}

.grey {
color: #555;
}
}

div.marketrow {
font-size: 17px;
padding: 0.3em;
font-family: 'demi-sans', sans-serif;
font-size: 16px;
padding: 3px 4px;
cursor: pointer;
}
white-space: nowrap;
display: flex;
flex-direction: column;
align-items: stretch;
margin: 3px;
background-color: #c2c9d4;
border-radius: 3px;
// border: 1px solid #7775;

div.marketrow:hover,
div.marketrow.selected {
background-color: #f0f7f7;
& > div {
display: flex;
align-items: center;
flex-direction: row;
line-height: 1;
padding: 2px 0;
}

&:hover,
&.selected {
padding: 2px 3px;
border: 1px solid #1b966d;
}

span.upgreen {
color: #35b97c;
}

span.downred {
color: #c33b3b;
}

span.pct-change {
min-width: 50px;
text-align: right;
}
}

label.market-search {
display: flex;
justify-content: space-between;
align-items: center;
align-items: stretch;
margin: 1px;
background-color: white;

input {
border: none;
width: 150px;
max-width: 75px;
}

span {
Expand Down
19 changes: 11 additions & 8 deletions client/webserver/site/src/css/market_dark.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,20 @@ body.dark {
}

div.marketlist {
background-color: #232428;
background-color: #1c1c1f;
border-right-color: black;

.grey {
color: #999;
}
}

div.marketrow:hover,
div.marketrow.selected {
background-color: #1d2936;
.marketrow {
background-color: #34383e;

&.selected {
border: 1px solid #7a97;
}
}

label.market-search {
Expand Down Expand Up @@ -66,10 +73,6 @@ body.dark {
border-radius: 3px;
}

.selected {
border-style: none;
}

.brdrleft {
border-left: 1px solid black;
}
Expand Down
Loading

0 comments on commit bb05332

Please sign in to comment.