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

create a template for how to create a new trading strategy #494

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions examples/configs/trader/sample_template.cfg
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions plugins/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
94 changes: 94 additions & 0 deletions plugins/templateNewLevelProvider.go
Original file line number Diff line number Diff line change
@@ -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
}
111 changes: 111 additions & 0 deletions plugins/templateNewStrategy.go
Original file line number Diff line number Diff line change
@@ -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.
}