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

Add sdex price feed #90

Merged
merged 16 commits into from
Jan 31, 2019
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 4 additions & 0 deletions cmd/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ func init() {
sdexAssetMap,
)

// setting the temp hack variables for the sdex price feeds
plugins.PrivateSdexHack.API = sdex.API
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be client (not sdex.API) and utils.ParseNetwork(botConfig.HorizonURL) instead of sdex.Network (can extract into a network variable first.

it's reasonable for MakeSDEX to do something like this if it needs to: API = nil since that's an abstraction we get with the make factory method. So when we reach into the sdex.API here we're breaking that abstraction and creating a dependency where there shouldn't be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

plugins.PrivateSdexHack.Network = sdex.Network

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
14 changes: 14 additions & 0 deletions plugins/priceFeed.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ 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 variable for SDEX price feeds pending refactor
var PrivateSdexHack struct {
Reidmcc marked this conversation as resolved.
Show resolved Hide resolved
API *horizon.Client
Network build.Network
}

// MakePriceFeed makes a PriceFeed
func MakePriceFeed(feedType string, url string) (api.PriceFeed, error) {
switch feedType {
Expand Down Expand Up @@ -38,6 +46,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
22 changes: 19 additions & 3 deletions plugins/sdex.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ func MakeSDEX(
threadTracker: threadTracker,
operationalBuffer: operationalBuffer,
operationalBufferNonNativePct: operationalBufferNonNativePct,
simMode: simMode,
pair: pair,
assetMap: assetMap,
simMode: simMode,
pair: pair,
assetMap: assetMap,
}

log.Printf("Using network passphrase: %s\n", sdex.Network.Passphrase)
Expand Down Expand Up @@ -813,3 +813,19 @@ func (sdex *SDEX) GetLatestTradeCursor() (interface{}, error) {

return records[0].PT, nil
}

// GetOrderBook gets the SDEX order book
func (sdex *SDEX) GetOrderBook(pair *model.TradingPair) (orderBook horizon.OrderBookSummary, e error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should conform to the API method described in the previous comments:

GetOrderBook(pair *model.TradingPair, maxCount int32) (*model.OrderBook, error)

this would require transforming orders from horizon's format to the model.OrderBook format
I can give you sample code for this if needed, I have something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sample code would be great

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Reidmcc the sample code below converts a single manageOffer to a model.Order.
You'd need to do something similar, converting from horizon.OrderBookSummary to model.OrderBook (which mostly contains a list of model.Order). Hope this is useful.

func manageOffer2Order(mob *build.ManageOfferBuilder, baseAsset horizon.Asset, quoteAsset horizon.Asset) (*model.Order, error) {
	orderAction := model.OrderActionSell
	price := model.NumberFromFloat(float64(mob.MO.Price.N)/float64(mob.MO.Price.D), utils.SdexPrecision)
	volume := model.NumberFromFloat(float64(mob.MO.Amount)/math.Pow(10, 7), utils.SdexPrecision)
	isBuy, e := assetsEqual(quoteAsset, mob.MO.Selling)
	if e != nil {
		return nil, fmt.Errorf("could not compare assets, error: %s", e)
	}
	if isBuy {
		orderAction = model.OrderActionBuy
		// TODO need to test price and volume conversions correctly
		// volume calculation needs to happen first since it uses the non-inverted price when multiplying
		volume = model.NumberFromFloat(volume.AsFloat()*price.AsFloat(), utils.SdexPrecision)
		price = model.InvertNumber(price)
	}

	return &model.Order{
		Pair: &model.TradingPair{
			Base:  model.FromHorizonAsset(baseAsset),
			Quote: model.FromHorizonAsset(quoteAsset),
		},
		OrderAction: orderAction,
		OrderType:   model.OrderTypeLimit,
		Price:       price,
		Volume:      volume,
		Timestamp:   model.MakeTimestamp(time.Now().UnixNano() / int64(time.Millisecond)),
	}, nil
}

Copy link
Contributor Author

@Reidmcc Reidmcc Jan 23, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, I just committed my own version before I saw this, let me know if I've got it.

//b, e := api.LoadOrderBook(*assetBase, *assetQuote)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can remove commented out code for now

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

baseAsset, quoteAsset, e := sdex.pair2Assets()
if e != nil {
log.Printf("%s", e)
return
}
b, e := sdex.API.LoadOrderBook(baseAsset, quoteAsset)
if e != nil {
log.Printf("Can't get SDEX orderbook: %s\n", e)
return
}
return b, e
}
90 changes: 90 additions & 0 deletions plugins/sdexFeed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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, "/")

baseParts := strings.Split(urlParts[0], ":")
baseCode := baseParts[0]
baseIssuer := baseParts[1]
baseConvert, e := utils.ParseAsset(baseCode, baseIssuer)
if e != nil {
return nil, fmt.Errorf("unable to convert base asset url to sdex asset: %s", e)
}

quoteParts := strings.Split(urlParts[1], ":")
quoteCode := quoteParts[0]
quoteIssuer := quoteParts[1]
quoteConvert, e := utils.ParseAsset(quoteCode, quoteIssuer)
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(*baseConvert)),
Quote: model.Asset(utils.Asset2CodeString(*quoteConvert)),
}

sdexAssetMap := map[model.Asset]horizon.Asset{
tradingPair.Base: *baseConvert,
tradingPair.Quote: *quoteConvert,
}

feedSDEX := MakeSDEX(
Reidmcc marked this conversation as resolved.
Show resolved Hide resolved
PrivateSdexHack.API,
"",
"",
"",
"",
PrivateSdexHack.Network,
Reidmcc marked this conversation as resolved.
Show resolved Hide resolved
nil,
0,
0,
true,
tradingPair,
sdexAssetMap,
)

return &sdexFeed{
sdex: feedSDEX,
assetBase: baseConvert,
assetQuote: quoteConvert,
}, nil
}

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

bids := orderBook.Bids
topBidPrice := utils.PriceAsFloat(bids[0].Price)
Reidmcc marked this conversation as resolved.
Show resolved Hide resolved

asks := orderBook.Asks
lowAskPrice := utils.PriceAsFloat(asks[0].Price)

centerPrice := (topBidPrice + lowAskPrice) / 2
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)
Reidmcc marked this conversation as resolved.
Show resolved Hide resolved
}
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
}