diff --git a/examples/configs/trader/sample_template.cfg b/examples/configs/trader/sample_template.cfg new file mode 100644 index 000000000..debed973b --- /dev/null +++ b/examples/configs/trader/sample_template.cfg @@ -0,0 +1,27 @@ +# Sample config file for the "template" trading strategy + +# We sell the base asset, i.e. ASSET_CODE_A as defined in the trader config + +# Price Feeds +# In this template trading strategy we take the max value from the three price feeds + +# specification of feed type "exchange"; see sample_buysell.cfg for an example of what to put here +FEED_TYPE_A="exchange" +FEED_URL_A="kraken/XXLM/ZUSD/last" + +# B feed +FEED_TYPE_B="exchange" +FEED_URL_B="kraken/XXLM/ZUSD/mid" + +# C feed +# Note: this needs ccxt-rest to be running since it uses an exchange integration enabled by ccxt-rest, see README for details +# on how to run ccxt-rest +FEED_TYPE_C="exchange" +FEED_URL_C="ccxt-binance/XLM/USDT/ask" + +# see sample_buysell.cfg for an example of what to put in these common values below +PRICE_TOLERANCE=0.001 +AMOUNT_TOLERANCE=0.001 +RATE_OFFSET_PERCENT=0.0 +RATE_OFFSET=0.0 +RATE_OFFSET_PERCENT_FIRST=true \ No newline at end of file diff --git a/plugins/factory.go b/plugins/factory.go index 288157d62..0a0594f33 100644 --- a/plugins/factory.go +++ b/plugins/factory.go @@ -163,6 +163,35 @@ var strategies = map[string]StrategyContainer{ return s, nil }, }, + /* + add an entry for your new strategy here. + When you start your bot and specify the --strategy param, it searches for an entry in this map. + This makeFn will construct an instance of your trading strategy by using the config file passed with the --stratConf param + */ + "template": { // use any name you want here for your strategy, here "template" is what should be used with the --strategy param + SortOrder: 7, // documentation puposes, this should be the last number, used when running the "./kelp strategies" command + Description: "This is a template for a trading strategy. It creates two sell levels using the max price fetched from the three configured price feeds", + NeedsConfig: true, // documentation puposes, used when running the "./kelp strategies" command to indicate to the user that we need to pass in a config file + Complexity: "Beginner", // documentation puposes, used when running the "./kelp strategies" command + makeFn: func(strategyFactoryData strategyFactoryData) (api.Strategy, error) { + var cfg templateNewConfig + err := config.Read(strategyFactoryData.stratConfigPath, &cfg) + utils.CheckConfigError(cfg, err, strategyFactoryData.stratConfigPath) + utils.LogConfig(cfg) + s, e := makeTemplateNewStrategy( // call your make*Strategy function here. All the data you need to invoke this factory method should be on the strategyFactoryData object + strategyFactoryData.sdex, + strategyFactoryData.tradingPair, + strategyFactoryData.ieif, + strategyFactoryData.assetBase, + strategyFactoryData.assetQuote, + &cfg, + ) + if e != nil { + return nil, fmt.Errorf("makeFn failed: %s", e) + } + return s, nil + }, + }, } // MakeStrategy makes a strategy diff --git a/plugins/templateNewLevelProvider.go b/plugins/templateNewLevelProvider.go new file mode 100644 index 000000000..4b6b5e607 --- /dev/null +++ b/plugins/templateNewLevelProvider.go @@ -0,0 +1,94 @@ +package plugins + +import ( + "fmt" + + "github.com/stellar/kelp/api" + "github.com/stellar/kelp/model" +) + +// templateNewLevelProvider is a template that you can use to create your own trading strategies +type templateNewLevelProvider struct { + priceFeedA api.PriceFeed + priceFeedB api.PriceFeed + priceFeedC api.PriceFeed + offset rateOffset + orderConstraints *model.OrderConstraints +} + +// ensure it implements the LevelProvider interface +var _ api.LevelProvider = &templateNewLevelProvider{} + +// makeTemplateNewLevelProvider is a factory method +func makeTemplateNewLevelProvider( + priceFeedA api.PriceFeed, + priceFeedB api.PriceFeed, + priceFeedC api.PriceFeed, + offset rateOffset, + orderConstraints *model.OrderConstraints, +) (api.LevelProvider, error) { + return &templateNewLevelProvider{ + priceFeedA: priceFeedA, + priceFeedB: priceFeedB, + priceFeedC: priceFeedC, + offset: offset, + orderConstraints: orderConstraints, + }, nil +} + +// GetLevels impl. +func (p *templateNewLevelProvider) GetLevels(maxAssetBase float64, maxAssetQuote float64) ([]api.Level, error) { + // maxAssetBase is the current base asset balance + // maxAssetQuote is the current quote asset balance + + /* ------- TODO add whatever logic you want for your trading strategy here ------- */ + // fetch three prices for three feeds + priceA, e := p.priceFeedA.GetPrice() + if e != nil { + return nil, fmt.Errorf("could not fetch price from feed A") + } + priceB, e := p.priceFeedB.GetPrice() + if e != nil { + return nil, fmt.Errorf("could not fetch price from feed B") + } + priceC, e := p.priceFeedC.GetPrice() + if e != nil { + return nil, fmt.Errorf("could not fetch price from feed C") + } + + // take max price of the three retrieved + var price float64 + if priceA > priceB { + price = priceA + } else { + price = priceB + } + if priceC > price { + price = priceC + } + + // use an amount in terms of the base asset + amount := 100.0 + /* ------- /TODO add whatever logic you want for your trading strategy here ------- */ + + // convert the outcome of your trading strategy calculations into levels here + // a level is a price point on the orderbook. These should typically be higher than the mid price for maker orders + // this is the outcome of all the calculations in your trading strategy. Once you return this, the framework will + // handle how to transform your orders to these levels. + return []api.Level{ + api.Level{ + Price: *model.NumberFromFloat(price, p.orderConstraints.PricePrecision), + Amount: *model.NumberFromFloat(amount, p.orderConstraints.PricePrecision), + }, + api.Level{ // example to create a second level at a 10% higher price and twice the amount + Price: *model.NumberFromFloat(price*1.10, p.orderConstraints.PricePrecision), + Amount: *model.NumberFromFloat(amount*2, p.orderConstraints.PricePrecision), + }, + // ... create as many levels as you want here for more depth + }, nil +} + +// GetFillHandlers impl +func (p *templateNewLevelProvider) GetFillHandlers() ([]api.FillHandler, error) { + return nil, nil +} diff --git a/plugins/templateNewStrategy.go b/plugins/templateNewStrategy.go new file mode 100644 index 000000000..e09397278 --- /dev/null +++ b/plugins/templateNewStrategy.go @@ -0,0 +1,111 @@ +package plugins + +import ( + "fmt" + + hProtocol "github.com/stellar/go/protocols/horizon" + "github.com/stellar/kelp/api" + "github.com/stellar/kelp/model" + "github.com/stellar/kelp/support/utils" +) + +// templateNewConfig contains the configuration params for this templateNewStrategy +type templateNewConfig struct { + // parameters specific to this template new trading strategy + FeedTypeA string `valid:"-" toml:"FEED_TYPE_A"` + FeedURLA string `valid:"-" toml:"FEED_URL_A"` + FeedTypeB string `valid:"-" toml:"FEED_TYPE_B"` + FeedURLB string `valid:"-" toml:"FEED_URL_B"` + FeedTypeC string `valid:"-" toml:"FEED_TYPE_C"` + FeedURLC string `valid:"-" toml:"FEED_URL_C"` + // common parameters below + PriceTolerance float64 `valid:"-" toml:"PRICE_TOLERANCE"` + AmountTolerance float64 `valid:"-" toml:"AMOUNT_TOLERANCE"` + RateOffsetPercent float64 `valid:"-" toml:"RATE_OFFSET_PERCENT"` + RateOffset float64 `valid:"-" toml:"RATE_OFFSET"` + RateOffsetPercentFirst bool `valid:"-" toml:"RATE_OFFSET_PERCENT_FIRST"` +} + +// String impl. +func (c templateNewConfig) String() string { + return utils.StructString(c, 0, nil) +} + +// makeTemplateNewStrategy is a factory method for SellTwapStrategy +func makeTemplateNewStrategy( + sdex *SDEX, + pair *model.TradingPair, + ieif *IEIF, + assetBase *hProtocol.Asset, + assetQuote *hProtocol.Asset, + config *templateNewConfig, +) (api.Strategy, error) { + // instantiate the three price feeds using the strings from the configs + priceFeedA, e := MakePriceFeed(config.FeedTypeA, config.FeedURLA) + if e != nil { + return nil, fmt.Errorf("error when making the feed A: %s", e) + } + priceFeedB, e := MakePriceFeed(config.FeedTypeB, config.FeedURLB) + if e != nil { + return nil, fmt.Errorf("error when making the feed B: %s", e) + } + priceFeedC, e := MakePriceFeed(config.FeedTypeC, config.FeedURLC) + if e != nil { + return nil, fmt.Errorf("error when making the feed C: %s", e) + } + // build the remaining dependencies that are used to build the templateNewLevelProvider + orderConstraints := sdex.GetOrderConstraints(pair) + offset := rateOffset{ + percent: config.RateOffsetPercent, + absolute: config.RateOffset, + percentFirst: config.RateOffsetPercentFirst, + } + // build the templateNewLevelProvider, to be used as the sell side + sellLevelProvider, e := makeTemplateNewLevelProvider( + priceFeedA, + priceFeedB, + priceFeedC, + offset, + orderConstraints, + ) + if e != nil { + return nil, fmt.Errorf("error when making a templateNewLevelProvider: %s", e) + } + + // since we are using the levelProvider framework, we need to inject our levelProvider into the sellSideStrategy. + // This also works on the buy side with the same levelProvider implementation (separate instance if it holds state), see comments below. + sellSideStrategy := makeSellSideStrategy( + sdex, + orderConstraints, + ieif, + assetBase, // pass in the base asset as an argument to the baseAsset parameter + assetQuote, // pass in the quote asset as an argument to the quoteAsset parameter + sellLevelProvider, + config.PriceTolerance, + config.AmountTolerance, + false, + ) + + // make a delete strategy as an example for a one-sided strategy + // if you are looking for an example of a strategy that uses both sides with the same levelProvider, look at buysellStrategy.go, only the initialization of the buy side needs to change + // All side strategies are written in the "context" of the sell side for simplicity which allows you to return prices in increasing order for deeper levels. The downside of this is that + // the Kelp strategy framework requires you to switch sides of base/quote here for the buy side. + buySideStrategy := makeDeleteSideStrategy( + sdex, + assetQuote, // pass in the quote asset as an argument to the baseAsset parameter + assetBase, // pass in the base asset as an argument to the quoteAsset parameter + ) + + // always pass the base asset as base to the compose strategy. Here we are combining the two side strategies into one single strategy + return makeComposeStrategy( + assetBase, // always the base asset + assetQuote, // always the quote asset + buySideStrategy, // buy side + sellSideStrategy, // sell side + ), nil + + // Note: it is more complicated if you do not use the levelProvider framework. The mirror strategy is an example of this. + // The reason it is more complex is because you have to worry about a lot of details such as placing offer amounts for insufficient funds, + // handling both the buy side and sell side in your logic, etc. All of this and more is taken care of for you when using the level provider + // framework. All new strategies created in Kelp typically use the level provider framework, as demonstrated above. +}