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

Commit

Permalink
Dynamic fee (#115), closes #108
Browse files Browse the repository at this point in the history
  • Loading branch information
nikhilsaraf authored Mar 2, 2019
1 parent a1cb9e4 commit c0f7b5d
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 25 deletions.
1 change: 1 addition & 0 deletions cmd/terminate.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func init() {
false,
nil, // not needed here
map[model.Asset]horizon.Asset{},
plugins.SdexFixedFeeFn(0),
)
terminator := terminator.MakeTerminator(client, sdex, *configFile.TradingAccount, configFile.TickIntervalSeconds, configFile.AllowInactiveMinutes)
// --- end initialization of objects ----
Expand Down
10 changes: 10 additions & 0 deletions cmd/trade.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ func init() {
tradingPair.Base: assetBase,
tradingPair.Quote: assetQuote,
}
feeFn, e := plugins.SdexFeeFnFromStats(
botConfig.HorizonURL,
botConfig.Fee.CapacityTrigger,
botConfig.Fee.Percentile,
botConfig.Fee.MaxOpFeeStroops,
)
if e != nil {
logger.Fatal(l, fmt.Errorf("could not set up feeFn correctly: %s", e))
}
sdex := plugins.MakeSDEX(
client,
botConfig.SourceSecretSeed,
Expand All @@ -162,6 +171,7 @@ func init() {
*simMode,
tradingPair,
sdexAssetMap,
feeFn,
)

// setting the temp hack variables for the sdex price feeds
Expand Down
6 changes: 6 additions & 0 deletions examples/configs/trader/sample_trader.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ FILL_TRACKER_SLEEP_MILLIS=0
# the url for your horizon instance. If this url contains the string "test" then the bot assumes it is using the test network.
HORIZON_URL="https://horizon-testnet.stellar.org"

# specify parameters for how we compute the operation fee from the /fee_stats endpoint (in stroops)
[FEE]
CAPACITY_TRIGGER=0.8
PERCENTILE=90
MAX_OP_FEE_STROOPS=5000

# uncomment below to add support for monitoring.
# type of alerting system to use, currently only "PagerDuty" is supported.
#ALERT_TYPE="PagerDuty"
Expand Down
17 changes: 14 additions & 3 deletions plugins/sdex.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type SDEX struct {
simMode bool
pair *model.TradingPair
assetMap map[model.Asset]horizon.Asset // this is needed until we fully address putting SDEX behind the Exchange interface
opFeeStroopsFn OpFeeStroops

// uninitialized
seqNum uint64
Expand Down Expand Up @@ -84,6 +85,7 @@ func MakeSDEX(
simMode bool,
pair *model.TradingPair,
assetMap map[model.Asset]horizon.Asset,
opFeeStroopsFn OpFeeStroops,
) *SDEX {
sdex := &SDEX{
API: api,
Expand All @@ -95,9 +97,10 @@ func MakeSDEX(
threadTracker: threadTracker,
operationalBuffer: operationalBuffer,
operationalBufferNonNativePct: operationalBufferNonNativePct,
simMode: simMode,
pair: pair,
assetMap: assetMap,
simMode: simMode,
pair: pair,
assetMap: assetMap,
opFeeStroopsFn: opFeeStroopsFn,
}

log.Printf("Using network passphrase: %s\n", sdex.Network.Passphrase)
Expand Down Expand Up @@ -393,7 +396,15 @@ func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator, asyncCallback func(h
sdex.Network,
build.SourceAccount{AddressOrSeed: sdex.SourceAccount},
}
// compute fee per operation
opFee, e := sdex.opFeeStroopsFn()
if e != nil {
return fmt.Errorf("SubmitOps error when computing op fee: %s", e)
}
muts = append(muts, build.BaseFee{Amount: opFee})
// add transaction mutators
muts = append(muts, ops...)

tx, e := build.Transaction(muts...)
if e != nil {
return errors.Wrap(e, "SubmitOps error: ")
Expand Down
108 changes: 108 additions & 0 deletions plugins/sdexExtensions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package plugins

import (
"fmt"
"log"
"net/http"
"strconv"

"github.com/stellar/kelp/support/networking"
)

// OpFeeStroops computes fees per operation
type OpFeeStroops func() (uint64, error)

// SdexFixedFeeFn returns a fixedFee in stroops
func SdexFixedFeeFn(fixedFeeStroops uint64) OpFeeStroops {
return func() (uint64, error) {
return fixedFeeStroops, nil
}
}

const baseFeeStroops = 100

var validPercentiles = []uint8{10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 99}

// SdexFeeFnFromStats returns an OpFeeStroops that uses the /fee_stats endpoint
func SdexFeeFnFromStats(
horizonBaseURL string,
capacityTrigger float64,
percentile uint8,
maxOpFeeStroops uint64,
) (OpFeeStroops, error) {
isValid := false
for _, p := range validPercentiles {
if percentile == p {
isValid = true
break
}
}
if !isValid {
return nil, fmt.Errorf("unable to create SdexFeeFnFromStats since percentile is invalid (%d). Allowed values: %v", percentile, validPercentiles)
}

if capacityTrigger <= 0 {
return nil, fmt.Errorf("unable to create SdexFeeFnFromStats, capacityTrigger should be > 0: %f", capacityTrigger)
}

if maxOpFeeStroops < baseFeeStroops {
return nil, fmt.Errorf("unable to create SdexFeeFnFromStats, maxOpFeeStroops should be >= %d (baseFeeStroops): %d", baseFeeStroops, maxOpFeeStroops)
}

return func() (uint64, error) {
return getFeeFromStats(horizonBaseURL, capacityTrigger, percentile, maxOpFeeStroops)
}, nil
}

func getFeeFromStats(horizonBaseURL string, capacityTrigger float64, percentile uint8, maxOpFeeStroops uint64) (uint64, error) {
feeStatsURL := horizonBaseURL + "/fee_stats"

feeStatsResponseMap := map[string]string{}
e := networking.JSONRequest(http.DefaultClient, "GET", feeStatsURL, "", map[string]string{}, &feeStatsResponseMap, "")
if e != nil {
return 0, fmt.Errorf("error fetching fee stats (URL=%s): %s", feeStatsURL, e)
}

// parse ledgerCapacityUsage
ledgerCapacityUsage, e := strconv.ParseFloat(feeStatsResponseMap["ledger_capacity_usage"], 64)
if e != nil {
return 0, fmt.Errorf("could not parse ledger_capacity_usage ('%s') as float64: %s", feeStatsResponseMap["ledger_capacity_usage"], e)
}

// case where we don't trigger the dynamic fees logic
if ledgerCapacityUsage < capacityTrigger {
var lastFeeInt int
lastFeeInt, e = strconv.Atoi(feeStatsResponseMap["last_ledger_base_fee"])
if e != nil {
return 0, fmt.Errorf("could not parse last_ledger_base_fee ('%s') as int: %s", feeStatsResponseMap["last_ledger_base_fee"], e)
}
lastFee := uint64(lastFeeInt)
if lastFee <= maxOpFeeStroops {
log.Printf("lastFee <= maxOpFeeStroops; using last_ledger_base_fee of %d stroops (maxOpFeeStroops = %d)\n", lastFee, maxOpFeeStroops)
return lastFee, nil
}
log.Printf("lastFee > maxOpFeeStroops; using maxOpFeeStroops of %d stroops (lastFee = %d)\n", maxOpFeeStroops, lastFee)
return maxOpFeeStroops, nil
}

// parse percentile value
var pStroopsInt64 uint64
pKey := fmt.Sprintf("p%d_accepted_fee", percentile)
if pStroops, ok := feeStatsResponseMap[pKey]; ok {
var pStroopsInt int
pStroopsInt, e = strconv.Atoi(pStroops)
if e != nil {
return 0, fmt.Errorf("could not parse percentile value (%s='%s'): %s", pKey, pStroops, e)
}
pStroopsInt64 = uint64(pStroopsInt)
} else {
return 0, fmt.Errorf("could not fetch percentile value (%s) from feeStatsResponseMap: %s", pKey, e)
}

if pStroopsInt64 <= maxOpFeeStroops {
log.Printf("pStroopsInt64 <= maxOpFeeStroops; using %s of %d stroops (maxOpFeeStroops = %d)\n", pKey, pStroopsInt64, maxOpFeeStroops)
return pStroopsInt64, nil
}
log.Printf("pStroopsInt64 > maxOpFeeStroops; using maxOpFeeStroops of %d stroops (%s = %d)\n", maxOpFeeStroops, pKey, pStroopsInt64)
return maxOpFeeStroops, nil
}
1 change: 1 addition & 0 deletions plugins/sdexFeed.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func makeSDEXFeed(url string) (*sdexFeed, error) {
true,
tradingPair,
sdexAssetMap,
SdexFixedFeeFn(0),
)

return &sdexFeed{
Expand Down
4 changes: 2 additions & 2 deletions support/networking/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ func JSONRequest(
if e != nil {
return fmt.Errorf("could not read 'Content-Type' header in http response: %s | response body: %s", e, bodyString)
}
if contentType != "application/json" {
return fmt.Errorf("invalid 'Content-Type' header in http response ('%s'), expecting 'application/json', response body: %s", contentType, bodyString)
if contentType != "application/json" && contentType != "application/hal+json" {
return fmt.Errorf("invalid 'Content-Type' header in http response ('%s'), expecting 'application/json' or 'application/hal+json', response body: %s", contentType, bodyString)
}

if errorKey != "" {
Expand Down
48 changes: 28 additions & 20 deletions trader/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,28 +10,36 @@ import (
// XLM is a constant for XLM
const XLM = "XLM"

// FeeConfig represents input data for how to deal with network fees
type FeeConfig struct {
CapacityTrigger float64 `valid:"-" toml:"CAPACITY_TRIGGER"` // trigger when "ledger_capacity_usage" in /fee_stats is >= this value
Percentile uint8 `valid:"-" toml:"PERCENTILE"` // percentile computation to use from /fee_stats (10, 20, ..., 90, 95, 99)
MaxOpFeeStroops uint64 `valid:"-" toml:"MAX_OP_FEE_STROOPS"` // max fee in stroops per operation to use
}

// BotConfig represents the configuration params for the bot
type BotConfig struct {
SourceSecretSeed string `valid:"-" toml:"SOURCE_SECRET_SEED"`
TradingSecretSeed string `valid:"-" toml:"TRADING_SECRET_SEED"`
AssetCodeA string `valid:"-" toml:"ASSET_CODE_A"`
IssuerA string `valid:"-" toml:"ISSUER_A"`
AssetCodeB string `valid:"-" toml:"ASSET_CODE_B"`
IssuerB string `valid:"-" toml:"ISSUER_B"`
TickIntervalSeconds int32 `valid:"-" toml:"TICK_INTERVAL_SECONDS"`
MaxTickDelayMillis int64 `valid:"-" toml:"MAX_TICK_DELAY_MILLIS"`
DeleteCyclesThreshold int64 `valid:"-" toml:"DELETE_CYCLES_THRESHOLD"`
SubmitMode string `valid:"-" toml:"SUBMIT_MODE"`
FillTrackerSleepMillis uint32 `valid:"-" toml:"FILL_TRACKER_SLEEP_MILLIS"`
HorizonURL string `valid:"-" toml:"HORIZON_URL"`
AlertType string `valid:"-" toml:"ALERT_TYPE"`
AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY"`
MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT"`
MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT"`
MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY"`
GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID"`
GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET"`
AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS"`
SourceSecretSeed string `valid:"-" toml:"SOURCE_SECRET_SEED"`
TradingSecretSeed string `valid:"-" toml:"TRADING_SECRET_SEED"`
AssetCodeA string `valid:"-" toml:"ASSET_CODE_A"`
IssuerA string `valid:"-" toml:"ISSUER_A"`
AssetCodeB string `valid:"-" toml:"ASSET_CODE_B"`
IssuerB string `valid:"-" toml:"ISSUER_B"`
TickIntervalSeconds int32 `valid:"-" toml:"TICK_INTERVAL_SECONDS"`
MaxTickDelayMillis int64 `valid:"-" toml:"MAX_TICK_DELAY_MILLIS"`
DeleteCyclesThreshold int64 `valid:"-" toml:"DELETE_CYCLES_THRESHOLD"`
SubmitMode string `valid:"-" toml:"SUBMIT_MODE"`
FillTrackerSleepMillis uint32 `valid:"-" toml:"FILL_TRACKER_SLEEP_MILLIS"`
HorizonURL string `valid:"-" toml:"HORIZON_URL"`
Fee FeeConfig `valid:"-" toml:"FEE"`
AlertType string `valid:"-" toml:"ALERT_TYPE"`
AlertAPIKey string `valid:"-" toml:"ALERT_API_KEY"`
MonitoringPort uint16 `valid:"-" toml:"MONITORING_PORT"`
MonitoringTLSCert string `valid:"-" toml:"MONITORING_TLS_CERT"`
MonitoringTLSKey string `valid:"-" toml:"MONITORING_TLS_KEY"`
GoogleClientID string `valid:"-" toml:"GOOGLE_CLIENT_ID"`
GoogleClientSecret string `valid:"-" toml:"GOOGLE_CLIENT_SECRET"`
AcceptableEmails string `valid:"-" toml:"ACCEPTABLE_GOOGLE_EMAILS"`

tradingAccount *string
sourceAccount *string // can be nil
Expand Down

0 comments on commit c0f7b5d

Please sign in to comment.