Skip to content

Commit

Permalink
loadbot: Add live price oracles.
Browse files Browse the repository at this point in the history
Add a boolean livemidgap that allows loadbot start with a live rate
based on mainnet coin USD values. This also alters ts to keep the midgap
close to live rates rather than random values.
  • Loading branch information
JoeGruffins committed Oct 4, 2023
1 parent f389639 commit b63f7b2
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 34 deletions.
18 changes: 9 additions & 9 deletions client/core/exchangeratefetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ var (

// fiatRateFetchers is the list of all supported fiat rate fetchers.
var fiatRateFetchers = map[string]rateFetcher{
coinpaprika: fetchCoinpaprikaRates,
dcrdataDotOrg: fetchDcrdataRates,
messari: fetchMessariRates,
coinpaprika: FetchCoinpaprikaRates,
dcrdataDotOrg: FetchDcrdataRates,
messari: FetchMessariRates,
}

// fiatRateInfo holds the fiat rate and the last update time for an
Expand Down Expand Up @@ -137,10 +137,10 @@ func newCommonRateSource(fetcher rateFetcher) *commonRateSource {
}
}

// fetchCoinpaprikaRates retrieves and parses fiat rate data from the
// FetchCoinpaprikaRates retrieves and parses fiat rate data from the
// Coinpaprika API. See https://api.coinpaprika.com/#operation/getTickersById
// for sample request and response information.
func fetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
func FetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
fiatRates := make(map[uint32]float64)
fetchRate := func(sa *SupportedAsset) {
assetID := sa.ID
Expand Down Expand Up @@ -190,9 +190,9 @@ func fetchCoinpaprikaRates(ctx context.Context, log dex.Logger, assets map[uint3
return fiatRates
}

// fetchDcrdataRates retrieves and parses fiat rate data from dcrdata
// FetchDcrdataRates retrieves and parses fiat rate data from dcrdata
// exchange rate API.
func fetchDcrdataRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
func FetchDcrdataRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
assetBTC := assets[btcBipID]
assetDCR := assets[dcrBipID]
noBTCAsset := assetBTC == nil || assetBTC.Wallet == nil
Expand Down Expand Up @@ -222,10 +222,10 @@ func fetchDcrdataRates(ctx context.Context, log dex.Logger, assets map[uint32]*S
return fiatRates
}

// fetchMessariRates retrieves and parses fiat rate data from the Messari API.
// FetchMessariRates retrieves and parses fiat rate data from the Messari API.
// See https://messari.io/api/docs#operation/Get%20Asset%20Market%20Data for
// sample request and response information.
func fetchMessariRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
func FetchMessariRates(ctx context.Context, log dex.Logger, assets map[uint32]*SupportedAsset) map[uint32]float64 {
fiatRates := make(map[uint32]float64)
fetchRate := func(sa *SupportedAsset) {
assetID := sa.ID
Expand Down
21 changes: 18 additions & 3 deletions dex/testing/eth/harness.sh
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,28 @@ cat > "${NODES_ROOT}/genesis.json" <<EOF
"extradata": "0x00000000000000000000000000000000000000000000000000000000000000009ebba10a6136607688ca4f27fab70e23938cd0270000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"alloc": {
"18d65fb8d60c1199bb1ad381be47aa692b482605": {
"balance": "26000000000000000000000"
"balance": "260000000000000000000000"
},
"4f8ef3892b65ed7fc356ff473a2ef2ae5ec27a06": {
"balance": "11000000000000000000000"
"balance": "110000000000000000000000"
},
"946dfaB1AD7caCFeF77dE70ea68819a30acD4577": {
"balance": "11000000000000000000000"
"balance": "110000000000000000000000"
},
"dd93b447f7eBCA361805eBe056259853F3912E04": {
"balance": "110000000000000000000000"
},
"1D4F2ee206474B136Af4868B887C7b166693c194": {
"balance": "110000000000000000000000"
},
"946dfaB1AD7caCFeF77dE70ea68819a30acD4577": {
"balance": "110000000000000000000000"
},
"3e983c7e08d22CaB842Bd54111087E09C578b3c3": {
"balance": "110000000000000000000000"
},
"653445e4C2f48B370fE10e59940E5e9Cb853D03F": {
"balance": "110000000000000000000000"
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion dex/testing/loadbot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ The **heavy** program is like **compound** on steroids. Four

The **whale** program runs multiple **traders** that randomly push the market in
one direction or the other. Can be run separately with other programs to create
a volatile market.
a volatile market. Can be used with the `--livemidgap` flag to always push the
price towards the global rate for that market.

### Logging

Expand Down
86 changes: 74 additions & 12 deletions dex/testing/loadbot/loadbot.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import (
_ "decred.org/dcrdex/client/asset/firo"
_ "decred.org/dcrdex/client/asset/ltc"
_ "decred.org/dcrdex/client/asset/zec"
"decred.org/dcrdex/client/core"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/calc"
"decred.org/dcrdex/dex/config"
Expand Down Expand Up @@ -103,6 +104,7 @@ var (

ctx, quit = context.WithCancel(context.Background())

supportedAssets = assetMap()
alphaAddrBase, betaAddrBase, alphaAddrQuote, betaAddrQuote, market, baseSymbol, quoteSymbol string
alphaCfgBase, betaCfgBase,
alphaCfgQuote, betaCfgQuote map[string]string
Expand All @@ -115,11 +117,11 @@ var (
rateShift, rateIncrease int64
conversionFactors = make(map[string]uint64)

ethInitFee = (dexeth.InitGas(1, 0) + dexeth.RefundGas(0)) * ethFeeRate
ethRedeemFee = dexeth.RedeemGas(1, 0) * ethFeeRate
defaultMidGap, marketBuyBuffer, whalePercent float64
keepMidGap, oscillate, randomOsc, ignoreErrors bool
oscInterval, oscStep, whaleFrequency uint64
ethInitFee = (dexeth.InitGas(1, 0) + dexeth.RefundGas(0)) * ethFeeRate
ethRedeemFee = dexeth.RedeemGas(1, 0) * ethFeeRate
defaultMidGap, marketBuyBuffer, whalePercent float64
keepMidGap, oscillate, randomOsc, ignoreErrors, liveMidGap bool
oscInterval, oscStep, whaleFrequency uint64

processesMtx sync.Mutex
processes []*process
Expand Down Expand Up @@ -372,7 +374,8 @@ func run() error {
flag.Uint64Var(&oscStep, "oscstep", 50, "for compound and sidestacker, the number of rate step to increase or decrease per epoch")
flag.BoolVar(&ignoreErrors, "ignoreerrors", false, "log and ignore errors rather than the default behavior of stopping loadbot")
flag.Uint64Var(&whaleFrequency, "whalefrequency", 4, "controls the frequency with which the whale \"whales\" after it is ready. To whale is to choose a rate and attempt to buy up the entire book at that price. If frequency is N, the whale will whale an average of 1 out of every N+1 epochs (default 4)")
flag.Float64Var(&whalePercent, "whalepercent", 0.1, "The percent of the current mid gap to whale within. If 0.1 the whale will pick a target price between 0.9 and 1.1 percent of the current mid gap (default 0.1)")
flag.Float64Var(&whalePercent, "whalepercent", 0.1, "The percent of the current mid gap to whale within. If 0.1 the whale will pick a target price between 0.9 and 1.1 percent of the current mid gap (default 0.1). Ignored if livemidgap is true")
flag.BoolVar(&liveMidGap, "livemidgap", false, "set true to start with a fetched midgap. Also forces the whale to only whale towards the mid gap")
flag.Parse()

if programName == "" {
Expand Down Expand Up @@ -462,6 +465,19 @@ func run() error {
// Adjust to be comparable to the dcr_btc market.
defaultMidGap = defaultBtcPerDcr * float64(rateStep) / 100

loggerMaker, err = dex.NewLoggerMaker(os.Stdout, logLevel)
if err != nil {
return fmt.Errorf("error creating LoggerMaker: %v", err)
}
log /* global */ = loggerMaker.NewLogger("LOADBOT")

if liveMidGap {
defaultMidGap, err = liveRate()
if err != nil {
return fmt.Errorf("error retrieving live rates: %v", err)
}
}

if epochDuration == 0 {
return fmt.Errorf("failed to find %q market in harness config. Available markets: %s", market, strings.Join(markets, ", "))
}
Expand All @@ -484,12 +500,6 @@ func run() error {
alphaCfgQuote = loadNodeConfig(quoteSymbol, alpha)
betaCfgQuote = loadNodeConfig(quoteSymbol, beta)

loggerMaker, err = dex.NewLoggerMaker(os.Stdout, logLevel)
if err != nil {
return fmt.Errorf("error creating LoggerMaker: %v", err)
}
log /* global */ = loggerMaker.NewLogger("LOADBOT")

log.Infof("Running program %s", programName)

getAddress := func(symbol, node string) (string, error) {
Expand Down Expand Up @@ -747,3 +757,55 @@ func symmetricWalletConfig(numCoins int, midGap uint64) (
maxBaseQty, maxQuoteQty = minBaseQty*2, minQuoteQty*2
return
}

// assetMap returns a map of asset information for supported assets.
func assetMap() map[uint32]*core.SupportedAsset {
supported := asset.Assets()
assets := make(map[uint32]*core.SupportedAsset, len(supported))
for assetID, asset := range supported {
wallet := &core.WalletState{}
assets[assetID] = &core.SupportedAsset{
ID: assetID,
Symbol: asset.Symbol,
Wallet: wallet,
Info: asset.Info,
Name: asset.Info.Name,
UnitInfo: asset.Info.UnitInfo,
}
for tokenID, token := range asset.Tokens {
assets[tokenID] = &core.SupportedAsset{
ID: tokenID,
Symbol: dex.BipIDSymbol(tokenID),
Wallet: wallet,
Token: token,
Name: token.Name,
UnitInfo: token.UnitInfo,
}
}
}
return assets
}

func liveRate() (float64, error) {
b, ok := supportedAssets[baseID]
if !ok {
return 0.0, fmt.Errorf("%d not a supported asset", baseID)
}
q, ok := supportedAssets[quoteID]
if !ok {
return 0.0, fmt.Errorf("%d not a supported asset", quoteID)
}
as := map[uint32]*core.SupportedAsset{baseID: b, quoteID: q}
liveRates := core.FetchCoinpaprikaRates(ctx, log, as)
if len(liveRates) != 2 {
liveRates = core.FetchMessariRates(ctx, log, as)
}
if len(liveRates) != 2 {
liveRates = core.FetchDcrdataRates(ctx, log, as)
}
if len(liveRates) != 2 {
return 0.0, errors.New("unabel to get live rates")
}
conversionRatio := float64(q.UnitInfo.Conventional.ConversionFactor) / float64(b.UnitInfo.Conventional.ConversionFactor)
return liveRates[baseID] / liveRates[quoteID] * conversionRatio, nil
}
39 changes: 30 additions & 9 deletions dex/testing/loadbot/whale.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ func (w *whale) HandleNotification(m *Mantle, note core.Notification) {
// Once registration is complete, register for a book feed.
if n.Topic() == core.TopicAccountRegistered {
book := m.book()
midGap := midGap(book)
minBaseQty, maxBaseQty, minQuoteQty, maxQuoteQty := symmetricWalletConfig(20, uint64(float64(midGap)*(1+(whalePercent*2 /* twice for buffering */))))
rate := midGap(book)
if !liveMidGap {
rate += uint64(float64(rate) * whalePercent)
}
minBaseQty, maxBaseQty, minQuoteQty, maxQuoteQty := symmetricWalletConfig(20, rate)
wmm := walletMinMax{
baseID: {min: minBaseQty, max: maxBaseQty},
quoteID: {min: minQuoteQty, max: maxQuoteQty},
Expand Down Expand Up @@ -137,8 +140,11 @@ func (w *whale) HandleNotification(m *Mantle, note core.Notification) {
// Refresh balances one epoch at a time.
case c < numWhale:
book := m.book()
midGap := midGap(book)
minBaseQty, maxBaseQty, minQuoteQty, maxQuoteQty := symmetricWalletConfig(20, midGap+rateStep)
rate := midGap(book)
if !liveMidGap {
rate += uint64(float64(rate) * whalePercent)
}
minBaseQty, maxBaseQty, minQuoteQty, maxQuoteQty := symmetricWalletConfig(20, rate)
wmm := walletMinMax{
baseID: {min: minBaseQty, max: maxBaseQty},
quoteID: {min: minQuoteQty, max: maxQuoteQty},
Expand All @@ -164,11 +170,26 @@ func (w *whale) HandleNotification(m *Mantle, note core.Notification) {
func (*whale) whale(m *Mantle) {
book := m.book()
midGap := midGap(book)
tweak := rand.Float64() * float64(1-(2*rand.Intn(2))) * float64(midGap) * whalePercent
target := rateStep
if tweak > 0 || int64(midGap)+int64(tweak) > int64(rateStep) {
target = truncate(int64(midGap)+int64(tweak), int64(rateStep))
var target uint64
// If we are trying to stay close to the mid gap, have the whale push
// us there. Otherwise set a random target.
if liveMidGap {
r, err := liveRate()
if err != nil {
m.log.Errorf("error retrieving live rates: %v", err)
target = midGap
} else {
target = truncate(int64(r*float64(rateEncFactor)), int64(rateStep))
}
} else {
tweak := rand.Float64() * float64(1-(2*rand.Intn(2))) * float64(midGap) * whalePercent
target = rateStep
if tweak > 0 || int64(midGap)+int64(tweak) > int64(rateStep) {
target = truncate(int64(midGap)+int64(tweak), int64(rateStep))
}
}
conversionRatio := float64(conversionFactors[quoteSymbol]) / float64(conversionFactors[baseSymbol])
m.log.Infof("TS whaling at %v rate.", float64(target)/float64(rateEncFactor)/conversionRatio)
sell := true
if target > midGap {
sell = false
Expand Down Expand Up @@ -204,7 +225,7 @@ func (*whale) whale(m *Mantle) {
rem = 0
}

m.log.Infof("Whaling with %d lots at %v", lots, float64(target)/float64(conversionFactors[quoteSymbol]))
m.log.Infof("Whaling with %d lots at %v", lots, float64(target)/float64(rateEncFactor)/conversionRatio)

for _, man := range wMantles {
for i := 0; i < int(nMaxOrd); i++ {
Expand Down

0 comments on commit b63f7b2

Please sign in to comment.