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

Commit

Permalink
Add sdex price feed (#90), closes #97
Browse files Browse the repository at this point in the history
  • Loading branch information
Reidmcc authored and nikhilsaraf committed Jan 31, 2019
1 parent 7366a03 commit 8afec86
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 25 deletions.
9 changes: 9 additions & 0 deletions cmd/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,15 @@ func init() {
sdexAssetMap,
)

// setting the temp hack variables for the sdex price feeds
e = plugins.SetPrivateSdexHack(client, utils.ParseNetwork(botConfig.HorizonURL))
if e != nil {
l.Info("")
l.Errorf("%s", e)
// we want to delete all the offers and exit here since there is something wrong with our setup
deleteAllOffersAndExit(l, botConfig, client, sdex)
}

dataKey := model.MakeSortedBotKey(assetBase, assetQuote)
strat, e := plugins.MakeStrategy(sdex, tradingPair, &assetBase, &assetQuote, *strategy, *stratConfigPath, *simMode)
if e != nil {
Expand Down
10 changes: 9 additions & 1 deletion examples/configs/trader/sample_buysell.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# fiat
# fixed
# exchange
# sdex
#
# We take the values from both feeds and divide them to get the center price.

Expand All @@ -25,7 +26,7 @@ DATA_FEED_A_URL="kraken/XXLM/ZUSD"
# this is the URL to a coinmarketcap feed which the bot understands.
#DATA_FEED_A_URL="https://api.coinmarketcap.com/v1/ticker/stellar/"

# this is a fixed value of 1 here because the exchange priceFeed provides a ratio of two assets.
# this is a fixed value of 1 here because the exchange and sdex priceFeeds provides a ratio of two assets.
DATA_TYPE_B="fixed"
DATA_FEED_B_URL="1.0"

Expand All @@ -34,6 +35,13 @@ DATA_FEED_B_URL="1.0"
# you can use a service like apilayer.net to get prices for fiat if you want real-time updates. You will need to fill in the access_key in this url
#DATA_FEED_B_URL="http://apilayer.net/api/live?access_key=&currencies=NGN"

# sample priceFeed with the "sdex" type
# this feed pulls from the SDEX, you can use the asset you're trading or something else, like the same coin from another issuer
# DATA_TYPE_A = "sdex"
# this is a string representing a SDEX pair; the format is CODE:ISSUER/CODE:ISSUER
# for XLM leave the issuer string blank
# DATA_FEED_A_URL="COUPON:GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI/XLM:"

# what value of a price change triggers re-creating an offer. Price change refers to the existing price of the offer vs. what price we want to set. value is a percentage specified as a decimal number (0 < value < 1.00)
PRICE_TOLERANCE=0.001

Expand Down
10 changes: 9 additions & 1 deletion examples/configs/trader/sample_sell.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# fiat
# fixed
# exchange
# sdex
#
# We take the values from both feeds and divide them to get the center price.

Expand All @@ -27,7 +28,7 @@ DATA_FEED_A_URL="kraken/XXLM/ZUSD"
# this is the URL to a coinmarketcap feed which the bot understands.
#DATA_FEED_A_URL="https://api.coinmarketcap.com/v1/ticker/stellar/"

# this is a fixed value of 1 here because the exchange priceFeed provides a ratio of two assets.
# this is a fixed value of 1 here because the exchange and sdex priceFeeds provides a ratio of two assets.
DATA_TYPE_B="fixed"
DATA_FEED_B_URL="1.0"

Expand All @@ -36,6 +37,13 @@ DATA_FEED_B_URL="1.0"
# you can use a service like apilayer.net to get prices for fiat if you want real-time updates. You will need to fill in the access_key in this url
#DATA_FEED_B_URL="http://apilayer.net/api/live?access_key=&currencies=NGN"

# sample priceFeed with the "sdex" type
# this feed pulls from the SDEX, you can use the asset you're trading or something else, like the same coin from another issuer
# DATA_TYPE_A = "sdex"
# this is a string representing a SDEX pair; the format is CODE:ISSUER/CODE:ISSUER
# for XLM leave the issuer string blank
# DATA_FEED_A_URL="COUPON:GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI/XLM:"

# what value of a price change triggers re-creating an offer. Price change refers to the existing price of the offer vs. what price we want to set. value is a percentage specified as a decimal number (0 < value < 1.00)
PRICE_TOLERANCE=0.001

Expand Down
30 changes: 30 additions & 0 deletions plugins/priceFeed.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,32 @@ import (

"github.com/interstellar/kelp/api"
"github.com/interstellar/kelp/model"
"github.com/stellar/go/build"
"github.com/stellar/go/clients/horizon"
)

// privateSdexHack is a temporary hack struct for SDEX price feeds pending refactor
type privateSdexHack struct {
API *horizon.Client
Network build.Network
}

// privateSdexHackVar is a temporary hack variable for SDEX price feeds pending refactor
var privateSdexHackVar *privateSdexHack

// SetPrivateSdexHack sets the privateSdexHack variable which is temporary until the pending SDEX price feed refactor
func SetPrivateSdexHack(api *horizon.Client, network build.Network) error {
if privateSdexHackVar != nil {
return fmt.Errorf("privateSdexHack is already set: %+v", privateSdexHackVar)
}

privateSdexHackVar = &privateSdexHack{
API: api,
Network: network,
}
return nil
}

// MakePriceFeed makes a PriceFeed
func MakePriceFeed(feedType string, url string) (api.PriceFeed, error) {
switch feedType {
Expand Down Expand Up @@ -38,6 +62,12 @@ func MakePriceFeed(feedType string, url string) (api.PriceFeed, error) {
}
tickerAPI := api.TickerAPI(exchange)
return newExchangeFeed(url, &tickerAPI, &tradingPair), nil
case "sdex":
sdex, e := makeSDEXFeed(url)
if e != nil {
return nil, fmt.Errorf("error occured while making the SDEX price feed: %s", e)
}
return sdex, nil
}
return nil, nil
}
Expand Down
73 changes: 73 additions & 0 deletions plugins/sdex.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"reflect"
"strconv"
"strings"
"time"

"github.com/interstellar/kelp/api"
"github.com/interstellar/kelp/model"
Expand Down Expand Up @@ -813,3 +814,75 @@ func (sdex *SDEX) GetLatestTradeCursor() (interface{}, error) {

return records[0].PT, nil
}

// GetOrderBook gets the SDEX orderbook
func (sdex *SDEX) GetOrderBook(pair *model.TradingPair, maxCount int32) (*model.OrderBook, error) {
if pair != sdex.pair {
return nil, fmt.Errorf("unregistered trading pair (%s) cannot be converted to horizon.Assets, instance's pair: %s", pair.String(), sdex.pair.String())
}

baseAsset, quoteAsset, e := sdex.pair2Assets()
if e != nil {
return nil, fmt.Errorf("cannot get SDEX orderbook: %s", e)
}

ob, e := sdex.API.LoadOrderBook(baseAsset, quoteAsset)
if e != nil {
return nil, fmt.Errorf("cannot get SDEX orderbook: %s", e)
}

ts := model.MakeTimestamp(time.Now().UnixNano() / int64(time.Millisecond))
transformedBids, e := sdex.transformHorizonOrders(pair, ob.Bids, model.OrderActionBuy, ts, maxCount)
if e != nil {
return nil, fmt.Errorf("could not transform bid side of SDEX orderbook: %s", e)
}

transformedAsks, e := sdex.transformHorizonOrders(pair, ob.Asks, model.OrderActionSell, ts, maxCount)
if e != nil {
return nil, fmt.Errorf("could not transform ask side of SDEX orderbook: %s", e)
}

return model.MakeOrderBook(
pair,
transformedAsks,
transformedBids,
), nil
}

func (sdex *SDEX) transformHorizonOrders(
pair *model.TradingPair,
side []horizon.PriceLevel,
orderAction model.OrderAction,
ts *model.Timestamp,
maxCount int32,
) ([]model.Order, error) {
transformed := []model.Order{}
for i, o := range side {
if i >= int(maxCount) {
break
}

floatPrice := float64(o.PriceR.N) / float64(o.PriceR.D)
price := model.NumberFromFloat(floatPrice, sdexOrderConstraints.PricePrecision)

volume, e := model.NumberFromString(o.Amount, sdexOrderConstraints.VolumePrecision)
if e != nil {
return nil, fmt.Errorf("could not parse amount for horizon order: %s", e)
}
// special handling of amount for bids
if orderAction.IsBuy() {
// use floatPrice here for more accuracy since floatPrice is what will be used in stellar-core
volume = volume.Scale(1.0 / floatPrice)
}

transformed = append(transformed, model.Order{
Pair: pair,
OrderAction: orderAction,
OrderType: model.OrderTypeLimit,
Price: price,
Volume: volume,
Timestamp: ts,
})
}
return transformed, nil
}
91 changes: 91 additions & 0 deletions plugins/sdexFeed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package plugins

import (
"fmt"
"strings"

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

// sdexFeed represents a pricefeed from the SDEX
type sdexFeed struct {
sdex *SDEX
assetBase *horizon.Asset
assetQuote *horizon.Asset
}

// ensure that it implements PriceFeed
var _ api.PriceFeed = &sdexFeed{}

// makeSDEXFeed creates a price feed from buysell's url fields
func makeSDEXFeed(url string) (*sdexFeed, error) {
urlParts := strings.Split(url, "/")

baseAsset, e := parseHorizonAsset(urlParts[0])
if e != nil {
return nil, fmt.Errorf("unable to convert base asset url to sdex asset: %s", e)
}
quoteAsset, e := parseHorizonAsset(urlParts[1])
if e != nil {
return nil, fmt.Errorf("unable to convert quote asset url to sdex asset: %s", e)
}

tradingPair := &model.TradingPair{
Base: model.Asset(utils.Asset2CodeString(*baseAsset)),
Quote: model.Asset(utils.Asset2CodeString(*quoteAsset)),
}
sdexAssetMap := map[model.Asset]horizon.Asset{
tradingPair.Base: *baseAsset,
tradingPair.Quote: *quoteAsset,
}
sdex := MakeSDEX(
privateSdexHackVar.API,
"",
"",
"",
"",
privateSdexHackVar.Network,
nil,
0,
0,
true,
tradingPair,
sdexAssetMap,
)

return &sdexFeed{
sdex: sdex,
assetBase: baseAsset,
assetQuote: quoteAsset,
}, nil
}

func parseHorizonAsset(assetString string) (*horizon.Asset, error) {
parts := strings.Split(assetString, ":")
code := parts[0]
issuer := parts[1]

asset, e := utils.ParseAsset(code, issuer)
if e != nil {
return nil, fmt.Errorf("could not read horizon asset from string (%s): %s", assetString, e)
}

return asset, e
}

// GetPrice returns the SDEX mid price for the trading pair
func (s *sdexFeed) GetPrice() (float64, error) {
orderBook, e := s.sdex.GetOrderBook(s.sdex.pair, 1)
if e != nil {
return 0, fmt.Errorf("unable to get sdex price: %s", e)
}

topBidPrice := orderBook.Bids()[0].Price
topAskPrice := orderBook.Asks()[0].Price

centerPrice := topBidPrice.Add(*topAskPrice).Scale(0.5).AsFloat()
return centerPrice, nil
}
19 changes: 19 additions & 0 deletions support/utils/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,22 @@ func CheckedString(v interface{}) string {
}
return fmt.Sprintf("%v", v)
}

// ParseAsset returns a horizon asset a string
func ParseAsset(code string, issuer string) (*horizon.Asset, error) {
if code != "XLM" && issuer == "" {
return nil, fmt.Errorf("error: issuer can only be empty if asset is XLM")
}

if code == "XLM" && issuer != "" {
return nil, fmt.Errorf("error: issuer needs to be empty if asset is XLM")
}

if code == "XLM" {
asset := Asset2Asset2(build.NativeAsset())
return &asset, nil
}

asset := Asset2Asset2(build.CreditAsset(code, issuer))
return &asset, nil
}
27 changes: 4 additions & 23 deletions trader/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"

"github.com/interstellar/kelp/support/utils"
"github.com/stellar/go/build"
"github.com/stellar/go/clients/horizon"
)

Expand Down Expand Up @@ -76,15 +75,15 @@ func (b *BotConfig) Init() error {
return fmt.Errorf("error: both assets cannot be the same '%s:%s'", b.AssetCodeA, b.IssuerA)
}

asset, e := parseAsset(b.AssetCodeA, b.IssuerA, "A")
asset, e := utils.ParseAsset(b.AssetCodeA, b.IssuerA)
if e != nil {
return e
return fmt.Errorf("Error while parsing Asset A: %s", e)
}
b.assetBase = *asset

asset, e = parseAsset(b.AssetCodeB, b.IssuerB, "B")
asset, e = utils.ParseAsset(b.AssetCodeB, b.IssuerB)
if e != nil {
return e
return fmt.Errorf("Error while parsing Asset B: %s", e)
}
b.assetQuote = *asset

Expand All @@ -99,21 +98,3 @@ func (b *BotConfig) Init() error {
b.sourceAccount, e = utils.ParseSecret(b.SourceSecretSeed)
return e
}

func parseAsset(code string, issuer string, letter string) (*horizon.Asset, error) {
if code != XLM && issuer == "" {
return nil, fmt.Errorf("error: ISSUER_%s can only be empty if ASSET_CODE_%s is '%s'", letter, letter, XLM)
}

if code == XLM && issuer != "" {
return nil, fmt.Errorf("error: ISSUER_%s needs to be empty if ASSET_CODE_%s is '%s'", letter, letter, XLM)
}

if code == XLM {
asset := utils.Asset2Asset2(build.NativeAsset())
return &asset, nil
}

asset := utils.Asset2Asset2(build.CreditAsset(code, issuer))
return &asset, nil
}

0 comments on commit 8afec86

Please sign in to comment.