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

Implement buy twap strategy (closes #522) #548

Closed
wants to merge 6 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
120 changes: 120 additions & 0 deletions examples/configs/trader/sample_buytwap.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Sample config file for the "buy twap" strategy

# This strategy requires the database and the fill handler to be enabled in the trader.cfg file

# We are buying the base asset here, i.e. ASSET_CODE_A as defined in the trader config

# Price Feeds
# Note: we take the value from the A feed and divide it by the value retrieved from the B feed below.
# the type of feeds can be one of crypto, fiat, fixed, exchange, sdex, function.

# specification of feed type "exchange"
START_ASK_FEED_TYPE="exchange"
# the specification of the A feed
# the format is <exchange name>/<base-asset-code-defined-by-exchange>/<quote-asset-code-defined-by-exchange>/<modifier>
# exchange name:
# use "kraken" or any of the ccxt-exchanges (run `kelp exchanges` for full list)
# examples: "kraken", "ccxt-kraken", "ccxt-binance", "ccxt-poloniex", "ccxt-bittrex"
# base asset code defined by exchange:
# this is the asset code defined by the exchange for the asset whose price you want to fetch (base asset).
# this code can be retrieved from the exchange's website or from the ccxt manual for ccxt-based exchanges.
# quote asset code defined by exchange:
# this is the asset code defined by the exchange for asset in which you want to quote the price (quote asset).
# this code can be retrieved from the exchange's website or from the ccxt manual for ccxt-based exchanges.
# modifier:
# this is a modifier that can be included only for feed type "exchange".
# a modifier allows you to fetch the "mid" price, "ask" price, "bid" price, or "last" price for now.
# if left unspecified then this is defaulted to "mid" for backwards compatibility (until v2.0 is released) (LOH-2)
# uncomment below to use binance, poloniex, or bittrex as your price feed. You will need to set up CCXT to use this, see the "Using CCXT" section in the README for details.
# be careful about using USD vs. USDT since some exchanges support only one, or both, or in some cases neither.
#DATA_FEED_A_URL="ccxt-kraken/XLM/USD/last"
#DATA_FEED_A_URL="ccxt-binance/XLM/USDT/ask"
#DATA_FEED_A_URL="ccxt-poloniex/XLM/USDT/bid"
# bittrex does not have an XLM/USD market so this config lists XLM/BTC instead; you should NOT use this when trying to price an asset based on the XLM/USD price (unless you know what you are doing).
#DATA_FEED_A_URL="ccxt-bittrex/XLM/BTC"
START_ASK_FEED_URL="kraken/XXLM/ZUSD/mid"

# sample priceFeed with the "crypto" type
#DATA_TYPE_A="crypto"
# this is the URL to a coinmarketcap feed which the bot understands.
#DATA_FEED_A_URL="https://api.coinmarketcap.com/v1/ticker/stellar/"

# 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:"

# sample priceFeed of type "function"
# this feed type uses one of the pre-defined functions to recursively operate on other price feeds
# all URLs for this type of feed are formatted like so: function_name(feed_type/feed_url[,feed_type/feed_url])
#DATA_TYPE_A = "function"
# the supported functions for now are only the "max" and "invert" functions, example usage:
# "max": max(exchange/ccxt-kraken/XLM/USD/mid,exchange/ccxt-binance/XLM/USDT/mid) -- will give you the larger price
# between kraken's mid price and binance's mid price
# "invert": invert(exchange/ccxt-kraken/XLM/USD/mid) -- will give you the effective USD/XLM price
#DATA_FEED_A_URL = "max(exchange/ccxt-kraken/XLM/USD/mid,exchange/ccxt-binance/XLM/USDT/mid)"

# 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

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

# how much percent to offset your rates by, specified as a decimal (ex: 0.05 = 5%). Can be used in conjunction with RATE_OFFSET below.
# A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed
# A negative value indicates that your base asset (ASSET_A) has a lower rate than the rate received from your price feed
RATE_OFFSET_PERCENT=0.0
# how much to offset your rates by, specified in number of units of the quote asset (ASSET_B) as a decimal.
# Can be used in conjunction with RATE_OFFSET_PERCENT above.
# A positive value indicates that your base asset (ASSET_A) has a higher rate than the rate received from your price feed
# A negative value indicates that your base asset (ASSET_A) has a lower rate than the rate received from your price feed
RATE_OFFSET=0.0
# specifies the order in which to offset the rates. If true then we apply the RATE_OFFSET_PERCENT first otherwise we apply the RATE_OFFSET first
# example rate calculation when set to true: ((rate_from_price_feed_a/rate_from_price_feed_b) * (1 + rate_offset_percent)) + rate_offset
# example rate calculation when set to false: ((rate_from_price_feed_a/rate_from_price_feed_b) + rate_offset) * (1 + rate_offset_percent)
RATE_OFFSET_PERCENT_FIRST=true

# NUM_HOURS_TO_SELL is an integer that defines the number of hours in which to complete the sales
# It is actually the number of hours to buy, but is called "SELL" for compatibility with pre-existing data structures
NUM_HOURS_TO_SELL = 23

# PARENT_BUCKET_SIZE_SECONDS is an integer value which represents the number of seconds to count as a single parent bucket.
# this should perfectly divide the number of seconds in a day (24 * 60 * 60)
# TODO how does this handle leap days?
PARENT_BUCKET_SIZE_SECONDS = 600

# DISTRIBUTE_SURPLUS_OVER_REMAINING_INTERVALS_PERCENT_CEILING is specified as a decimal value from 0.0-1.0 inclusive.
# we take the percent of remaining bucket intervals (ceiling of that number) to arrive at the number of intervals over which to distribute the surplus.
# ceiling(4.5) = 5
# ceiling(5.1) = 6
# example: a value of 0.05 here will distribute the surplus over 1/20th of the remaining bucket intervals
# if there are 345 bucket intervals remaining and this value is set to 0.05, the bot will use this calculation:
# ceiling(0.05 * 345)
# = ceiling(17.25)
# = 17 buckets over which to distribute the excess surplus
# therefore, the bot will distribute the surplus over the remaining 17 bucket intervals (of total 345 bucket intervals)
# Note that setting this to 0.0 will discard any surplus and setting it to 1.0 will distribute over all remaining bucket intervals
DISTRIBUTE_SURPLUS_OVER_REMAINING_INTERVALS_PERCENT_CEILING = 0.05

# EXPONENTIAL_SMOOTHING_FACTOR is a decimal (0 <= x <= 1)
# a larger number results in a smoother distribution across the remaining intervals
# set this to 1.0 for a linear distribution over the chosen bucket intervals and 0.0 to buy the entire surplus in the next bucket interval
EXPONENTIAL_SMOOTHING_FACTOR = 0.50

# MIN_CHILD_ORDER_SIZE_PERCENT_OF_PARENT is a decimal value (0 <= x <= 1) which defines the lower bound of the randomization function to be
# used when picking the size of the order to be placed in a bot update cycle. The randomized number is then multiplied by the total capacity
# for the current bucket interval. If the available capacity for the interval is less than this amount then we will use the available capacity.
MIN_CHILD_ORDER_SIZE_PERCENT_OF_PARENT = 0.2

# DAY_OF_WEEK_DAILY_CAP is a volume filter specified individually for every day of the week
# make sure any filters in your trader.cfg file is compliant with this configuration
[DAY_OF_WEEK_DAILY_CAP]
Mo = "volume/daily/buy/base/10000.0/exact"
Tu = "volume/daily/buy/base/10000.0/exact"
We = "volume/daily/buy/base/10000.0/exact"
Th = "volume/daily/buy/base/10000.0/exact"
Fr = "volume/daily/buy/base/10000.0/exact"
Sa = "volume/daily/buy/base/10000.0/exact"
Su = "volume/daily/buy/base/10000.0/exact"
63 changes: 63 additions & 0 deletions examples/walkthroughs/trader/buy_twap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# TWAP Buy

This guide shows you how to setup the **kelp** bot using the [buy_twap](../../../plugins/buyTwapStrategy.go) strategy. This strategy will buy tokens using the well-known [TWAP metric](https://en.wikipedia.org/wiki/Time-weighted_average_price). We'll configure it to buy a `COUPON` token; `COUPON` can be any token you want and is only used as a sample token for the purpose of this walkthrough guide.

The bot places a single buy order which is refreshed and randomized at every bot update as configured in `trader.cfg`. There is a fixed buy capacity for each `bucket` based on the daily buy limit configuration. Any value that is unbought in a given bucket (surplus) is distributed over the remaining buckets. The config values control the bucket size along with other factors to determine the distribution of the surplus.

## Account Setup

First, go through the [Account Setup guide](account_setup.md) to set up your Stellar accounts and the necessary configuration file, `trader.cfg`. In the `buy_twap` strategy the bot is programmed to buy `ASSET_CODE_A` from the `trader.cfg` file.

## Install Bots

Download the pre-compiled binaries for **kelp** for your platform from the [Github Releases Page](https://github.com/stellar/kelp/releases). If you have downloaded the correct version for your platform you can run it directly.

## BuyTwap Strategy Configuration

Use the [sample configuration file for the buy_twap strategy](../../configs/trader/sample_buytwap.cfg) as a template. We will walkthrough the configuration parameters below.

### Price Feeds

BuyTwap requires one price feed, `START_ASK_FEED`. This computes the price used when placing the single buy order.

To give `COUPON` a stable price against USD, we're going to set `START_ASK_FEED_TYPE` to `"exchange"` and `START_ASK_FEED_URL` to `"kraken/XXLM/ZUSD"`. This points our bot to Kraken's price for 1 XLM, quoted in USD.

### Tolerances

For the purposes of this walkthrough, we set the `PRICE_TOLERANCE` value to `0.001` which means that a _0.1% change in price will trigger the bot to refresh its orders_. Similarly, we set the `AMOUNT_TOLERANCE` value to `0.001` which means that we need _at least a 0.1% change in amount for the bot to refresh its orders_. If either one of these conditions is met then the bot will refresh its orders. In practice, you should set any value you're comfortable with.

Note that this strategy randomizes the order size at every update, so that should be taken into consideration when setting these tolerance values.

### Buckets

The `PARENT_BUCKET_SIZE_SECONDS` configuration defines the size in seconds of each bucket. There are 86,400 seconds in each day. The number of buckets is determined by dividing 86,400 by `PARENT_BUCKET_SIZE_SECONDS`, so `PARENT_BUCKET_SIZE_SECONDS` needs to perfectly divide 86,400.

In our sample configuration we have set this to 600 seconds, which is equal to 10 minutes. This will give us 144 buckets every day.

### Distributing the Surplus

There are two config params that control how the surplus is distributed, `DISTRIBUTE_SURPLUS_OVER_REMAINING_INTERVALS_PERCENT_CEILING` and `EXPONENTIAL_SMOOTHING_FACTOR`.

`DISTRIBUTE_SURPLUS_OVER_REMAINING_INTERVALS_PERCENT_CEILING` is a decimal value between 0.0 and 1.0, both inclusive. Setting this to 0.0 will discard any surplus and setting it to 1.0 will distribute the surplus over all the remaining buckets. Setting it to 0.50 will distribute it over 50% of the remaining buckets.

`EXPONENTIAL_SMOOTHING_FACTOR` determines how _smoothly_ we should distribute any surplus. This is a decimal value between 0.0 and 1.0, both inclusive. Setting this to 0.0 will buy the entire surplus in the next bucket interval (least smooth). Setting this to 1.0 will distribute the surplus evenly (linearly) over the chosen bucket intervals (most smooth).

### Amounts

The `DAY_OF_WEEK_DAILY_CAP` determines the daily limit of how many tokens of the base asset to buy. This has to be of the form `"volume/daily/buy/base/X/exact"` where `X` is the daily limit in base units to be bought. This configuration is a map of volume filters for each day of the week, which gives you flexibility to have a per-day configuration. A filter for each day must be specified.

The capacity of each bucket is the daily buy amount divided by the number of buckets + the surplus allocated to that bucket.

As mentioned above, the order is randomized and refreshed at each bot update at the interval described in the `trader.cfg` file. The minimum order size for this randomization is controlled by `MIN_CHILD_ORDER_SIZE_PERCENT_OF_PARENT` which is a decimal from 0.0 to 1.0, both inclusive. A value of 0.0 means that there is no minimum order size, whereas a value of 1.0 indicates that the order size should be the capacity of the bucket (i.e. no ramdomization). If the capacity for the bucket is less than the computed minimum amount then the remaining capacity is used as the order size.

## Run Kelp

Assuming your botConfig is called `trader.cfg` and your strategy config is called `buytwap.cfg`, you can run `kelp` with the following command:

```
kelp trade --botConf ./path/trader.cfg --strategy buy_twap --stratConf ./path/buytwap.cfg
```

# Above and Beyond

You can also play around with the configuration parameters of the [sample configuration file for the buy_twap strategy](../../configs/trader/sample_buytwap.cfg), look at some of the other strategies that are available out-of-the-box or dig into the code and _create your own strategy_.
77 changes: 77 additions & 0 deletions plugins/buyTwapStrategy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package plugins

import (
"fmt"
"time"

hProtocol "github.com/stellar/go/protocols/horizon"
"github.com/stellar/kelp/api"
"github.com/stellar/kelp/model"
)

// makeBuyTwapStrategy is a factory method for BuyTwapStrategy
func makeBuyTwapStrategy(
sdex *SDEX,
pair *model.TradingPair,
ieif *IEIF,
assetBase *hProtocol.Asset,
assetQuote *hProtocol.Asset,
filterFactory *FilterFactory,
config *sellTwapConfig,
) (api.Strategy, error) {
startPf, e := MakePriceFeed(config.StartAskFeedType, config.StartAskFeedURL)
if e != nil {
return nil, fmt.Errorf("error when making the start priceFeed: %s", e)
}

orderConstraints := sdex.GetOrderConstraints(pair)
offset := rateOffset{
percent: config.RateOffsetPercent,
absolute: config.RateOffset,
percentFirst: config.RateOffsetPercentFirst,
}
dowFilter, e := makeDowFilter(filterFactory, config.DayOfWeekDailyCap)
if e != nil {
return nil, fmt.Errorf("error when making dowFilter: %s", e)
}
levelProvider, e := makeSellTwapLevelProvider(
startPf,
offset,
orderConstraints,
dowFilter,
config.NumHoursToSell,
config.ParentBucketSizeSeconds,
config.DistributeSurplusOverRemainingIntervalsPercentCeiling,
config.ExponentialSmoothingFactor,
config.MinChildOrderSizePercentOfParent,
time.Now().UnixNano(),
)
if e != nil {
return nil, fmt.Errorf("error when making a sellTwapLevelProvider: %s", e)
}

// switch sides of base/quote here for buy side
buySideStrategy := makeSellSideStrategy(
sdex,
orderConstraints,
ieif,
assetQuote,
assetBase,
levelProvider,
config.PriceTolerance,
config.AmountTolerance,
true,
)

// use assetBase as param to assetBase argument, since the delete strategy is
// on the sell side. so the params should line up correctly with the arguments
deleteSideStrategy := makeDeleteSideStrategy(sdex, assetBase, assetQuote)

return makeComposeStrategy(
assetBase,
assetQuote,
buySideStrategy,
deleteSideStrategy,
), nil

}
26 changes: 26 additions & 0 deletions plugins/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,32 @@ var strategies = map[string]StrategyContainer{
return s, nil
},
},
"buy_twap": {
SortOrder: 7,
Description: "Creates buy offers by distributing orders over time for a given day using a twap metric",
NeedsConfig: true,
Complexity: "Intermediate",
makeFn: func(strategyFactoryData strategyFactoryData) (api.Strategy, error) {
// reuse the sellTwapConfig struct since we need the same info for buyTwap
var cfg sellTwapConfig
err := config.Read(strategyFactoryData.stratConfigPath, &cfg)
utils.CheckConfigError(cfg, err, strategyFactoryData.stratConfigPath)
utils.LogConfig(cfg)
s, e := makeBuyTwapStrategy(
strategyFactoryData.sdex,
strategyFactoryData.tradingPair,
strategyFactoryData.ieif,
strategyFactoryData.assetBase,
strategyFactoryData.assetQuote,
strategyFactoryData.filterFactory,
&cfg,
)
if e != nil {
return nil, fmt.Errorf("make Fn failed: %s", e)
}
return s, nil
},
},
}

// MakeStrategy makes a strategy
Expand Down
14 changes: 9 additions & 5 deletions plugins/filterFactory.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

hProtocol "github.com/stellar/go/protocols/horizon"
"github.com/stellar/kelp/model"
"github.com/stellar/kelp/queries"
)

var filterIDRegex *regexp.Regexp
Expand Down Expand Up @@ -88,6 +89,12 @@ func makeVolumeFilterConfig(configInput string) (*VolumeFilterConfig, error) {
return nil, fmt.Errorf("invalid input (%s), the second part needs to equal or start with \"daily\"", configInput)
}

action, e := queries.ParseDailyVolumeAction(parts[2])
if e != nil {
return nil, fmt.Errorf("could not parse volume filter action from input (%s): %s", configInput, e)
}
config.action = action

errInvalid := fmt.Errorf("invalid input (%s), the modifier for \"daily\" can be either \"market_ids\" or \"account_ids\" like so 'daily:market_ids=[4c19915f47,db4531d586]' or 'daily:account_ids=[account1,account2]' or 'daily:market_ids=[4c19915f47,db4531d586]:account_ids=[account1,account2]'", configInput)
if len(limitWindowParts) == 2 {
e = addModifierToConfig(config, limitWindowParts[1])
Expand All @@ -108,17 +115,14 @@ func makeVolumeFilterConfig(configInput string) (*VolumeFilterConfig, error) {
return nil, fmt.Errorf("invalid input (%s), the second part needs to be \"daily\" and can have only one modifier \"market_ids\" like so 'daily:market_ids=[4c19915f47,db4531d586]'", configInput)
}

if parts[2] != "sell" {
return nil, fmt.Errorf("invalid input (%s), the third part needs to be \"sell\"", configInput)
}
limit, e := strconv.ParseFloat(parts[4], 64)
if e != nil {
return nil, fmt.Errorf("could not parse the fourth part as a float value from config value (%s): %s", configInput, e)
}
if parts[3] == "base" {
config.SellBaseAssetCapInBaseUnits = &limit
config.BaseAssetCapInBaseUnits = &limit
} else if parts[3] == "quote" {
config.SellBaseAssetCapInQuoteUnits = &limit
config.BaseAssetCapInQuoteUnits = &limit
} else {
return nil, fmt.Errorf("invalid input (%s), the third part needs to be \"base\" or \"quote\"", configInput)
}
Expand Down
Loading