Skip to content

Commit

Permalink
mm: Basic MM basis only determined by oracles
Browse files Browse the repository at this point in the history
Using the market mid-gap, especially on low liquidity markets causes
problems. Options to do this are removed.
  • Loading branch information
martonp committed Jul 23, 2024
1 parent fa356ff commit c5b2e8b
Show file tree
Hide file tree
Showing 7 changed files with 37 additions and 373 deletions.
5 changes: 1 addition & 4 deletions client/mm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,7 @@ type BotConfig struct {
}

func (c *BotConfig) requiresPriceOracle() bool {
if c.BasicMMConfig != nil {
return c.BasicMMConfig.OracleWeighting != nil && *c.BasicMMConfig.OracleWeighting > 0
}
return false
return c.BasicMMConfig != nil
}

func (c *BotConfig) requiresCEX() bool {
Expand Down
109 changes: 12 additions & 97 deletions client/mm/mm_basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,10 @@ import (
"sync/atomic"

"decred.org/dcrdex/client/core"
"decred.org/dcrdex/client/orderbook"
"decred.org/dcrdex/dex"
"decred.org/dcrdex/dex/calc"
)

const (
// Our mid-gap rate derived from the local DEX order book is converted to an
// effective mid-gap that can only vary by up to 3% from the oracle rate.
// This is to prevent someone from taking advantage of a sparse market to
// force a bot into giving a favorable price. In reality a market maker on
// an empty market should use a high oracle bias anyway, but this should
// prevent catastrophe.
maxOracleMismatch = 0.03
)

// GapStrategy is a specifier for an algorithm to choose the maker bot's target
// spread.
type GapStrategy string
Expand Down Expand Up @@ -81,39 +70,13 @@ type BasicMarketMakingConfig struct {
// before they are replaced (units: ratio of price). Default: 0.1%.
// 0 <= x <= 0.01.
DriftTolerance float64 `json:"driftTolerance"`

// OracleWeighting affects how the target price is derived based on external
// market data. OracleWeighting, r, determines the target price with the
// formula:
// target_price = dex_mid_gap_price * (1 - r) + oracle_price * r
// OracleWeighting is limited to 0 <= x <= 1.0.
// Fetching of price data is disabled if OracleWeighting = 0.
OracleWeighting *float64 `json:"oracleWeighting"`

// OracleBias applies a bias in the positive (higher price) or negative
// (lower price) direction. -0.05 <= x <= 0.05.
OracleBias float64 `json:"oracleBias"`

// EmptyMarketRate can be set if there is no market data available, and is
// ignored if there is market data available.
EmptyMarketRate float64 `json:"emptyMarketRate"`
}

func needBreakEvenHalfSpread(strat GapStrategy) bool {
return strat == GapStrategyAbsolutePlus || strat == GapStrategyPercentPlus || strat == GapStrategyMultiplier
}

func (c *BasicMarketMakingConfig) Validate() error {
if c.OracleBias < -0.05 || c.OracleBias > 0.05 {
return fmt.Errorf("bias %f out of bounds", c.OracleBias)
}
if c.OracleWeighting != nil {
w := *c.OracleWeighting
if w < 0 || w > 1 {
return fmt.Errorf("oracle weighting %f out of bounds", w)
}
}

if c.DriftTolerance == 0 {
c.DriftTolerance = 0.001
}
Expand Down Expand Up @@ -182,7 +145,6 @@ type basicMMCalculator interface {

type basicMMCalculatorImpl struct {
*market
book dexOrderBook
oracle oracle
core botCoreAdaptor
cfg *BasicMarketMakingConfig
Expand All @@ -200,67 +162,19 @@ type basicMMCalculatorImpl struct {
// If there is no fiat rate available, the empty market rate in the
// configuration is used.
func (b *basicMMCalculatorImpl) basisPrice() uint64 {
midGap, err := b.book.MidGap()
if err != nil && !errors.Is(err, orderbook.ErrEmptyOrderbook) {
b.log.Errorf("MidGap error: %v", err)
return 0
}

basisPrice := float64(midGap) // float64 message-rate units

var oracleWeighting, oraclePrice float64
if b.cfg.OracleWeighting != nil && *b.cfg.OracleWeighting > 0 {
oracleWeighting = *b.cfg.OracleWeighting
oraclePrice = b.oracle.getMarketPrice(b.baseID, b.quoteID)
if oraclePrice == 0 {
b.log.Warnf("no oracle price available for %s bot", b.name)
}
}

if oraclePrice > 0 {
msgOracleRate := float64(b.msgRate(oraclePrice))

// Apply the oracle mismatch filter.
if basisPrice > 0 {
low, high := msgOracleRate*(1-maxOracleMismatch), msgOracleRate*(1+maxOracleMismatch)
if basisPrice < low {
b.log.Debugf("local mid-gap is below safe range. Using effective mid-gap of %.2f below the oracle rate.", maxOracleMismatch*100)
basisPrice = low
} else if basisPrice > high {
b.log.Debugf("local mid-gap is above safe range. Using effective mid-gap of %.2f above the oracle rate.", maxOracleMismatch*100)
basisPrice = high
}
}
oracleRate := b.msgRate(b.oracle.getMarketPrice(b.baseID, b.quoteID))
b.log.Tracef("oracle rate = %s", b.fmtRate(oracleRate))

if b.cfg.OracleBias != 0 {
msgOracleRate *= 1 + b.cfg.OracleBias
if oracleRate == 0 {
oracleRate = b.core.ExchangeRateFromFiatSources()
if oracleRate == 0 {
return 0
}

if basisPrice == 0 { // no mid-gap available. Use the oracle price.
basisPrice = msgOracleRate
b.log.Tracef("basisPrice: using basis price %s from oracle because no mid-gap was found in order book", b.fmtRate(uint64(msgOracleRate)))
} else {
basisPrice = msgOracleRate*oracleWeighting + basisPrice*(1-oracleWeighting)
b.log.Tracef("basisPrice: oracle-weighted basis price = %s", b.fmtRate(uint64(msgOracleRate)))
}
}

if basisPrice > 0 {
return steppedRate(uint64(basisPrice), b.rateStep)
b.log.Tracef("using fiat rate = %s", b.fmtRate(oracleRate))
}

// TODO: add a configuration to turn off use of fiat rate?
fiatRate := b.core.ExchangeRateFromFiatSources()
if fiatRate > 0 {
return steppedRate(fiatRate, b.rateStep)
}

if b.cfg.EmptyMarketRate > 0 {
emptyMsgRate := b.msgRate(b.cfg.EmptyMarketRate)
return steppedRate(emptyMsgRate, b.rateStep)
}

return 0
return steppedRate(oracleRate, b.rateStep)
}

// halfSpread calculates the distance from the mid-gap where if you sell a lot
Expand Down Expand Up @@ -419,10 +333,11 @@ func (m *basicMarketMaker) rebalance(newEpoch uint64) {
return
}
defer m.rebalanceRunning.Store(false)

m.log.Tracef("rebalance: epoch %d", newEpoch)
basisPrice := m.calculator.basisPrice()
if basisPrice == 0 {
m.log.Errorf("No basis price available and no empty-market rate set")
m.log.Errorf("No basis price available")
return
}

Expand All @@ -431,6 +346,7 @@ func (m *basicMarketMaker) rebalance(newEpoch uint64) {
m.log.Errorf("Could not calculate fee-gap stats: %v", err)
return
}

m.registerFeeGap(feeGap)
var feeAdj uint64
if needBreakEvenHalfSpread(m.cfg().GapStrategy) {
Expand All @@ -448,14 +364,13 @@ func (m *basicMarketMaker) rebalance(newEpoch uint64) {
}

func (m *basicMarketMaker) botLoop(ctx context.Context) (*sync.WaitGroup, error) {
book, bookFeed, err := m.core.SyncBook(m.host, m.baseID, m.quoteID)
_, bookFeed, err := m.core.SyncBook(m.host, m.baseID, m.quoteID)
if err != nil {
return nil, fmt.Errorf("failed to sync book: %v", err)
}

m.calculator = &basicMMCalculatorImpl{
market: m.market,
book: book,
oracle: m.oracle,
core: m.core,
cfg: m.cfg(),
Expand Down
77 changes: 17 additions & 60 deletions client/mm/mm_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,87 +37,44 @@ func TestBasisPrice(t *testing.T) {
}

tests := []*struct {
name string
midGap uint64
oraclePrice uint64
oracleBias float64
oracleWeight float64
conversions map[uint32]float64
fiatRate uint64
exp uint64
name string
oraclePrice uint64
fiatRate uint64
exp uint64
}{
{
name: "just mid-gap is enough",
midGap: 123e5,
exp: 123e5,
name: "oracle price",
oraclePrice: 2000,
fiatRate: 1000,
exp: 2000,
},
{
name: "mid-gap + oracle weight",
midGap: 1950,
oraclePrice: 2000,
oracleWeight: 0.5,
exp: 1975,
name: "no oracle price",
oraclePrice: 0,
fiatRate: 1000,
exp: 1000,
},
{
name: "adjusted mid-gap + oracle weight",
midGap: 1000, // adjusted to 1940
oraclePrice: 2000,
oracleWeight: 0.5,
exp: 1970,
},
{
name: "no mid-gap effectively sets oracle weight to 100%",
midGap: 0,
oraclePrice: 2000,
oracleWeight: 0.5,
exp: 2000,
},
{
name: "mid-gap + oracle weight + oracle bias",
midGap: 1950,
oraclePrice: 2000,
oracleBias: -0.01, // minus 20
oracleWeight: 0.75,
exp: 1972, // 0.25 * 1950 + 0.75 * (2000 - 20) = 1972
},
{
name: "no mid-gap and no oracle weight fails to produce result",
midGap: 0,
oraclePrice: 0,
oracleWeight: 0.75,
exp: 0,
},
{
name: "no mid-gap and no oracle weight, but fiat rate is set",
midGap: 0,
oraclePrice: 0,
oracleWeight: 0.75,
fiatRate: 1200,
exp: 1200,
name: "no oracle price or fiat rate",
oraclePrice: 0,
fiatRate: 0,
exp: 0,
},
}

for _, tt := range tests {
oracle := &tOracle{
marketPrice: mkt.MsgRateToConventional(tt.oraclePrice),
}
ob := &tOrderBook{
midGap: tt.midGap,
}
cfg := &BasicMarketMakingConfig{
OracleWeighting: &tt.oracleWeight,
OracleBias: tt.oracleBias,
}

tCore := newTCore()
adaptor := newTBotCoreAdaptor(tCore)
adaptor.fiatExchangeRate = tt.fiatRate

calculator := &basicMMCalculatorImpl{
market: mustParseMarket(mkt),
book: ob,
oracle: oracle,
cfg: cfg,
cfg: &BasicMarketMakingConfig{},
log: tLogger,
core: adaptor,
}
Expand Down
8 changes: 2 additions & 6 deletions client/webserver/live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2386,13 +2386,9 @@ func randomBotConfig(mkt *mm.MarketWithHost) *mm.BotConfig {
switch {
case typeRoll < 0.33: // basic MM
gapStrategy := gapStrategies[rand.Intn(len(gapStrategies))]
oracleWeight := rand.Float64()
oracleBias := rand.Float64()
basicCfg := &mm.BasicMarketMakingConfig{
GapStrategy: gapStrategies[rand.Intn(len(gapStrategies))],
DriftTolerance: rand.Float64() * 0.01,
OracleWeighting: &oracleWeight,
OracleBias: oracleBias,
GapStrategy: gapStrategies[rand.Intn(len(gapStrategies))],
DriftTolerance: rand.Float64() * 0.01,
}
cfg.BasicMMConfig = basicCfg
lots, gapFactors := newPlacements(gapStrategy)
Expand Down
58 changes: 0 additions & 58 deletions client/webserver/site/src/html/mmsettings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -685,64 +685,6 @@
<span class="fs14 grey ms-1">epochs</span>
</div>
</div>

{{- /* EMPTY MARKET RATE */ -}}
<div id="emptyMarketRateBox" class="flex-stretch-column pt-2 mt-2 border-top">
<label for="emptyMarketRateCheckbox" class="fs16 d-flex align-items-center justify-content-between">
<div class="flex-center">
<input type="checkbox" id="emptyMarketRateCheckbox" class="form-check-input me-2 mt-0" checked>
<span>[[[empty_market_rate]]]</span>
</div>
<span class="ico-info fs12" data-tooltip="[[[empty_market_rate_tooltip]]]"></span>
</label>
<div id="emptyMarketRateInputBox" class="mt-2 d-flex align-items-center justify-content-end">
<input type="number" id="emptyMarketRateInput" class="w-25 micro wide text-end">
<span class="fs14 grey ms-1">
<span data-quote-ticker></span>
<span>/</span>
<span data-base-ticker></span>
</span>
</div>
<div id="emptyMarketRateErr" class="pt-2 flex-center text-danger d-hide"></div>
</div>

{{- /* ORACLE SETTINGS */ -}}
<div id="oraclesSettingBox" class="flex-stretch-column pt-2 mt-2 border-top">
{{- /* USE ORACLE CHECKBOX */ -}}
<label for="useOracleCheckbox" class="fs16 d-flex align-items-center justify-content-between">
<div class="flex-center">
<input type="checkbox" id="useOracleCheckbox" class="form-check-input me-2 mt-0" checked>
<span>[[[use_oracles]]]</span>
</div>
<span class="ico-info fs12" data-tooltip="[[[use_oracles_tooltip]]]"></span>
</label>

{{- /* ORACLE WEIGHTING */ -}}
<div id="oracleWeightingBox" class="mt-2">
<div class="d-flex align-items-center justify-content-between">
<span>[[[Oracle weight]]]</span>
<span class="ico-info fs12" data-tooltip="[[[oracle_weighting_tooltip]]]"></span>
</div>
<div class="d-flex align-items-center justify-content-end">
<div id="oracleWeightingSlider" class="mini-slider flex-grow-1 me-2"></div>
<input type="number" id="oracleWeighting" class="micro text-end">
<span class="fs14 grey ms-1">%</span>
</div>
</div>

{{- /* ORACLE BIAS */ -}}
<div id="oracleBiasBox" class="mt-2">
<div class="d-flex align-items-center justify-content-between">
<span>[[[Oracle bias]]]</span>
<span class="ico-info fs12" data-tooltip="[[[oracle_bias_tooltip]]]"></span>
</div>
<div class="d-flex align-items-center justify-content-end">
<div id="oracleBiasSlider" class="mini-slider flex-grow-1 me-2"></div>
<input type="number" id="oracleBias" class="micro text-end">
<span class="fs14 grey ms-1">%</span>
</div>
</div>
</div>
</section>
</div>
</div>
Expand Down
Loading

0 comments on commit c5b2e8b

Please sign in to comment.