Skip to content
This repository has been archived by the owner on Feb 1, 2024. It is now read-only.

Add trackSDEX strategy #39

Closed
wants to merge 5 commits into from
Closed
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
25 changes: 25 additions & 0 deletions examples/configs/trader/sample_trackSDEX.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Sample config file for the "trackSDEX" strategy

# what % deviation from the ideal price is allowed before we reset the price, specified as a decimal (0 < PRICE_TOLERANCE < 1.00)
PRICE_TOLERANCE=0.01

# what % deviation from the ideal amount is allowed before we reset the price, specified as a decimal (0 < AMOUNT_TOLERANCE < 1.00)
AMOUNT_TOLERANCE=0.01

# define the bid/ask spread that you are willing to provide. spread is a percentage specified as a decimal number (0 < spread < 1.00) - here it is 0.1%
SPREAD=0.002

# What base percent of your (one sided) balance to use on each level specified as a decimal number
BASE_PERCENT_PER_LEVEL = 0.01

# max number of levels to have on either side. Defines how deep of an orderbook you want to make.
# MAX_LEVELS * BASE_PERCENT_PER_LEVEL must be < 1.00, or you'll use more than your balance.
# You also need to account for your XLM reserve if trading vs XLM:
# Needed XLM reserve = MAX_LEVELS, so subtract that from your balance when setting these parameters.
# i.e. if 45 was 10% of your XLM balance, then MAX_LEVELS * BASE_PERCENT_PER_LEVEL must be < 0.9
MAX_LEVELS = 45

# the minimum portion of the total effective base value that either of the assets may represent before the bot stops placing orders on that side.
# this prevents your account from going to 0 for one asset and no longer being able to market make.
# the strategy also dynamically modifies the level amounts based on which asset you have more of, so this stop shouldn't trigger often
MAINTAIN_BALANCE_PERCENT = 0.25
16 changes: 16 additions & 0 deletions plugins/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ var strategies = map[string]StrategyContainer{
return makeDeleteStrategy(sdex, assetBase, assetQuote), nil
},
},
"trackSDEX": StrategyContainer{
SortOrder: 5,
Description: "Places buy and sell orders based on the SDEX orderbook price of an asset",
NeedsConfig: true,
Complexity: "Intermediate",
makeFn: func(sdex *SDEX, assetBase *horizon.Asset, assetQuote *horizon.Asset, stratConfigPath string) (api.Strategy, error) {
var cfg trackSDEXConfig
err := config.Read(stratConfigPath, &cfg)
utils.CheckConfigError(cfg, err, stratConfigPath)
s, e := makeTrackSDEXStrategy(sdex, assetBase, assetQuote, &cfg)
if e != nil {
return nil, fmt.Errorf("makeFn failed: %s", e)
}
return s, nil
},
},
}

// MakeStrategy makes a strategy
Expand Down
19 changes: 19 additions & 0 deletions plugins/sdexFeed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package plugins

import (
"github.com/lightyeario/kelp/support/utils"
"github.com/stellar/go/clients/horizon"
)

func GetSDEXPrice(sdex *SDEX, assetBase *horizon.Asset, assetQuote *horizon.Asset) (float64, error) {
orderBook, e := utils.GetOrderBook(sdex.API, assetBase, assetQuote)
if e != nil {
return 0, e
}
bids := orderBook.Bids
topBidPrice := utils.PriceAsFloat(bids[0].Price)
asks := orderBook.Asks
lowAskPrice := utils.PriceAsFloat(asks[0].Price)
centerPrice := (topBidPrice + lowAskPrice) / 2
return centerPrice, nil
}
116 changes: 116 additions & 0 deletions plugins/trackSDEXLevelProvider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package plugins

import (
"fmt"
"log"

"github.com/lightyeario/kelp/api"
"github.com/lightyeario/kelp/model"
"github.com/lightyeario/kelp/support/utils"
)

type SDEXLevelProvider struct {
spread float64
basePercentPerLevel float64
maxLevels int16
maintainBalancePercent float64
sdexMidPrice float64
isBuy bool
}

func makeSDEXLevelProvider(
spread float64,
basePercentPerLevel float64,
maxLevels int16,
maintainBalancePercent float64,
sdexMidPrice float64,
isBuy bool,
) api.LevelProvider {
validateTotalAmount(maxLevels, basePercentPerLevel)
return &SDEXLevelProvider{
spread: spread,
basePercentPerLevel: basePercentPerLevel,
maxLevels: maxLevels,
maintainBalancePercent: maintainBalancePercent,
sdexMidPrice: sdexMidPrice,
isBuy: isBuy,
}
}

func validateTotalAmount(maxLevels int16, basePercentPerLevel float64) {
l := float64(maxLevels)
totalAmount := l * basePercentPerLevel
if totalAmount > 1 {
log.Fatalf("Number of levels * percent per level must be < 1.0\n")
}

}

func (p *SDEXLevelProvider) GetLevels(maxAssetBase float64, maxAssetQuote float64) ([]api.Level, error) {
if maxAssetBase <= 0.0 {
return nil, fmt.Errorf("Had none of the base asset, unable to generate levels")
}

levels := []api.Level{}
totalAssetValue := maxAssetBase + (maxAssetQuote / p.sdexMidPrice)
balanceRatio := maxAssetBase / totalAssetValue
ratioGap := balanceRatio - 0.5

log.Printf("balanceRatio = %v", balanceRatio)

allowedSpend := maxAssetBase - totalAssetValue*p.maintainBalancePercent
if allowedSpend < 0.0 {
allowedSpend = 0.0
}
backupBalance := maxAssetBase - allowedSpend

spent := 0.0
levelCounter := 1.0
overSpent := false
for i := int16(0); i < p.maxLevels; i++ {
if spent >= maxAssetBase {
return nil, fmt.Errorf("Level provider spent more than asset balance")
}
if spent >= allowedSpend {
overSpent = true
}

level, amountSpent, e := p.getLevel(maxAssetBase, levelCounter, ratioGap, overSpent, backupBalance)
if e != nil {
return nil, e
}

spent += amountSpent
if overSpent {
backupBalance -= amountSpent
}
if spent < maxAssetBase {
levels = append(levels, level)
}
levelCounter += 1.0
}
return levels, nil
}

func (p *SDEXLevelProvider) getLevel(maxAssetBase float64, levelCounter float64, ratioGap float64, overSpent bool, backupBalance float64) (api.Level, float64, error) {

targetPrice := p.sdexMidPrice * (1.0 + p.spread*levelCounter)
targetAmount := maxAssetBase * p.basePercentPerLevel * (1.0 + ratioGap*2)
if p.isBuy {
targetAmount = maxAssetBase * p.basePercentPerLevel * (1.0 + ratioGap*2) * targetPrice
}

if overSpent {
targetAmount = backupBalance * 0.001
}

if overSpent && p.isBuy {
targetAmount = backupBalance * 0.001 * targetPrice
}

level := api.Level{
Price: *model.NumberFromFloat(targetPrice, utils.SdexPrecision),
Amount: *model.NumberFromFloat(targetAmount, utils.SdexPrecision),
}
return level, targetAmount, nil
}
83 changes: 83 additions & 0 deletions plugins/trackSDEXStrategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package plugins

import (
"fmt"

"github.com/lightyeario/kelp/api"
"github.com/lightyeario/kelp/support/utils"
"github.com/stellar/go/clients/horizon"
)

// trackSDEXConfig contains the configuration params for this strategy
type trackSDEXConfig struct {
Spread float64 `valid:"-" toml:"SPREAD"`
PriceTolerance float64 `valid:"-" toml:"PRICE_TOLERANCE"`
AmountTolerance float64 `valid:"-" toml:"AMOUNT_TOLERANCE"`
BasePercentPerLevel float64 `valid:"-" toml:"BASE_PERCENT_PER_LEVEL"`
MaxLevels int16 `valid:"-" toml:"MAX_LEVELS"`
MaintainBalancePercent float64 `valid:"-" toml:"MAINTAIN_BALANCE_PERCENT"`
}

// String impl.
func (c trackSDEXConfig) String() string {
return utils.StructString(c, nil)
}

// makeTrackSDEXStrategy is a factory method
func makeTrackSDEXStrategy(
sdex *SDEX,
assetBase *horizon.Asset,
assetQuote *horizon.Asset,
config *trackSDEXConfig,
) (api.Strategy, error) {
sdexMidPrice, e := GetSDEXPrice(
sdex,
assetBase,
assetQuote,
)
if e != nil {
return nil, fmt.Errorf("failed to get SDEX orderbook price", e)
}
sdexInversePrice := 1.0 / sdexMidPrice

sellSideStrategy := makeSellSideStrategy(
sdex,
assetBase,
assetQuote,
makeSDEXLevelProvider(
config.Spread,
config.BasePercentPerLevel,
config.MaxLevels,
config.MaintainBalancePercent,
sdexMidPrice,
false,
),
config.PriceTolerance,
config.AmountTolerance,
false,
)
// switch sides of base/quote here for buy side
buySideStrategy := makeSellSideStrategy(
sdex,
assetQuote,
assetBase,
makeSDEXLevelProvider(
config.Spread,
config.BasePercentPerLevel,
config.MaxLevels,
config.MaintainBalancePercent,
sdexInversePrice,
true,
),
config.PriceTolerance,
config.AmountTolerance,
true,
)

return makeComposeStrategy(
assetBase,
assetQuote,
buySideStrategy,
sellSideStrategy,
), nil
}
10 changes: 10 additions & 0 deletions support/utils/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,16 @@ func LoadAllOffers(account string, api *horizon.Client) (offersRet []horizon.Off
return
}

// GetOrderBook gets the orderbook for the A/B pair
func GetOrderBook(api *horizon.Client, assetBase *horizon.Asset, assetQuote *horizon.Asset) (orderBook horizon.OrderBookSummary, e error) {
b, e := api.LoadOrderBook(*assetBase, *assetQuote)
if e != nil {
log.Printf("Can't get SDEX orderbook: %s\n", e)
return
}
return b, e
}

// FilterOffers filters out the offers into selling and buying, where sellOffers sells the sellAsset and buyOffers buys the sellAsset
func FilterOffers(offers []horizon.Offer, sellAsset horizon.Asset, buyAsset horizon.Asset) (sellOffers []horizon.Offer, buyOffers []horizon.Offer) {
for _, offer := range offers {
Expand Down