diff --git a/api/exchange.go b/api/exchange.go index 2e5dec008..ee1845921 100644 --- a/api/exchange.go +++ b/api/exchange.go @@ -209,6 +209,7 @@ type Balance struct { // ExchangeShim is the interface we use as a generic API for all crypto exchanges type ExchangeShim interface { SubmitOps(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error + SubmitOpsSynch(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error // forced synchronous version of SubmitOps GetBalanceHack(asset horizon.Asset) (*Balance, error) LoadOffersHack() ([]horizon.Offer, error) Constrainable diff --git a/cmd/trade.go b/cmd/trade.go index f84005a82..a28f58fc8 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -234,6 +234,7 @@ func makeStrategy( ieif *plugins.IEIF, tradingPair *model.TradingPair, options inputs, + threadTracker *multithreading.ThreadTracker, ) api.Strategy { // setting the temp hack variables for the sdex price feeds e := plugins.SetPrivateSdexHack(client, plugins.MakeIEIF(true), network) @@ -241,7 +242,7 @@ func makeStrategy( l.Info("") l.Errorf("%s", e) // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } strategy, e := plugins.MakeStrategy(sdex, ieif, tradingPair, &assetBase, &assetQuote, *options.strategy, *options.stratConfigPath, *options.simMode) @@ -249,7 +250,7 @@ func makeStrategy( l.Info("") l.Errorf("%s", e) // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } return strategy } @@ -275,7 +276,7 @@ func makeBot( log.Println() log.Println(e) // we want to delete all the offers and exit here since there is something wrong with our setup - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } dataKey := model.MakeSortedBotKey(botConfig.AssetBase(), botConfig.AssetQuote()) alert, e := monitoring.MakeAlert(botConfig.AlertType, botConfig.AlertAPIKey) @@ -345,6 +346,7 @@ func runTradeCmd(options inputs) { ieif, tradingPair, options, + threadTracker, ) bot := makeBot( l, @@ -371,7 +373,7 @@ func runTradeCmd(options inputs) { // we want to delete all the offers and exit here because we don't want the bot to run if monitoring isn't working // if monitoring is desired but not working properly, we want the bot to be shut down and guarantee that there // aren't outstanding offers. - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } }() } @@ -444,11 +446,11 @@ func startFillTracking( l.Info("") l.Info("problem encountered while instantiating the fill tracker:") l.Errorf("%s", e) - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } if botConfig.FillTrackerSleepMillis != 0 { - fillTracker := plugins.MakeFillTracker(tradingPair, threadTracker, exchangeShim, botConfig.FillTrackerSleepMillis) + fillTracker := plugins.MakeFillTracker(tradingPair, threadTracker, exchangeShim, botConfig.FillTrackerSleepMillis, botConfig.FillTrackerDeleteCyclesThreshold) fillLogger := plugins.MakeFillLogger() fillTracker.RegisterHandler(fillLogger) if strategyFillHandlers != nil { @@ -462,17 +464,16 @@ func startFillTracking( e := fillTracker.TrackFills() if e != nil { l.Info("") - l.Info("problem encountered while running the fill tracker:") - l.Errorf("%s", e) + l.Errorf("problem encountered while running the fill tracker: %s", e) // we want to delete all the offers and exit here because we don't want the bot to run if fill tracking isn't working - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } }() } else if strategyFillHandlers != nil && len(strategyFillHandlers) > 0 { l.Info("") l.Error("error: strategy has FillHandlers but fill tracking was disabled (set FILL_TRACKER_SLEEP_MILLIS to a non-zero value)") // we want to delete all the offers and exit here because we don't want the bot to run if fill tracking isn't working - deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim) + deleteAllOffersAndExit(l, botConfig, client, sdex, exchangeShim, threadTracker) } } @@ -509,7 +510,20 @@ func validateTrustlines(l logger.Logger, client *horizon.Client, botConfig *trad l.Info("trustlines valid") } -func deleteAllOffersAndExit(l logger.Logger, botConfig trader.BotConfig, client *horizon.Client, sdex *plugins.SDEX, exchangeShim api.ExchangeShim) { +func deleteAllOffersAndExit( + l logger.Logger, + botConfig trader.BotConfig, + client *horizon.Client, + sdex *plugins.SDEX, + exchangeShim api.ExchangeShim, + threadTracker *multithreading.ThreadTracker, +) { + l.Info("") + l.Infof("waiting for all outstanding threads (%d) to finish before loading offers to be deleted...", threadTracker.NumActiveThreads()) + threadTracker.Stop(multithreading.StopModeError) + threadTracker.Wait() + l.Info("...all outstanding threads finished") + l.Info("") l.Info("deleting all offers and then exiting...") @@ -525,7 +539,7 @@ func deleteAllOffersAndExit(l logger.Logger, botConfig trader.BotConfig, client l.Infof("created %d operations to delete offers\n", len(dOps)) if len(dOps) > 0 { - e := exchangeShim.SubmitOps(dOps, func(hash string, e error) { + e := exchangeShim.SubmitOpsSynch(dOps, func(hash string, e error) { if e != nil { logger.Fatal(l, e) return @@ -539,7 +553,7 @@ func deleteAllOffersAndExit(l logger.Logger, botConfig trader.BotConfig, client for { sleepSeconds := 10 - l.Infof("sleeping for %d seconds until our deletion is confirmed and we exit...\n", sleepSeconds) + l.Infof("sleeping for %d seconds until our deletion is confirmed and we exit...(should never reach this line since we submit delete ops synchronously)\n", sleepSeconds) time.Sleep(time.Duration(sleepSeconds) * time.Second) } } else { diff --git a/examples/configs/trader/sample_trader.cfg b/examples/configs/trader/sample_trader.cfg index 000eb80e4..08259b2ae 100644 --- a/examples/configs/trader/sample_trader.cfg +++ b/examples/configs/trader/sample_trader.cfg @@ -31,11 +31,20 @@ SUBMIT_MODE="both" # the typical use case for this config value is to keep the orders on your orderbook intact if your price feed is unreachable for a small amount of time. # example: use -1 if you never want to delete all offers (this is not recommended). # example: use 0 if you want to delete all offers on any error. -# example: use 2 if you want to tolerate 2 continuous update cycle with errors, i.e. three continuous update cycles with errors will delete all offers. +# example: use 2 if you want to tolerate 2 continuous update cycles with errors, i.e. 3 continuous update cycles with errors will delete all offers. DELETE_CYCLES_THRESHOLD=0 # how many milliseconds to sleep before checking for fills again, a value of 0 disables fill tracking # fill tracking is not supported when trading on a non-SDEX exchange (i.e. set it to 0) FILL_TRACKER_SLEEP_MILLIS=0 +# how many continuous errors in each fill-tracking cycle can the bot accept before it will delete all offers to protect its exposure. +# this number has to be exceeded for all the offers to be deleted and any error will be counted only once per cycle. +# any time the bot completes a full run successfully this counter will be reset. +# the bot will continue running even if it hits an error or deletes all offers. +# the typical use case for this config value is to keep the orders on your orderbook intact if you the endpoint to read fills is unreachable for a small amount of time. +# example: use -1 if you never want to delete all offers (this is not recommended). +# example: use 0 if you want to delete all offers on any error. +# example: use 2 if you want to tolerate 2 continuous cycles with errors, i.e. 3 continuous cycles with errors will delete all offers. +FILL_TRACKER_DELETE_CYCLES_THRESHOLD=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" diff --git a/glide.lock b/glide.lock index 5c4638560..b5bd6a135 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 81ad219d2939a6ac4e4bbd94d3cebf8f1ea07291ae882f7f93ad781c8cf27ce6 -updated: 2019-03-19T16:40:20.118158839-07:00 +hash: 0ab15bf176f8313235c90ce78694e51ca8da996fd0c80187810a19536864ca1b +updated: 2019-03-26T14:36:41.239077092-07:00 imports: - name: cloud.google.com/go version: 793297ec250352b0ece46e103381a0fc3dab95a1 @@ -46,7 +46,7 @@ imports: - name: github.com/mitchellh/mapstructure version: 3536a929edddb9a5b34bd6861dc4a9647cb459fe - name: github.com/nikhilsaraf/go-tools - version: 19004f22be08c82a22e679726ca22853c65919ae + version: a26df67722de7fcf1a8e22cd934e63e553dd3875 subpackages: - multithreading - name: github.com/PagerDuty/go-pagerduty diff --git a/glide.yaml b/glide.yaml index 7c36f4139..f94b7b1fd 100644 --- a/glide.yaml +++ b/glide.yaml @@ -36,6 +36,6 @@ import: - package: github.com/lechengfan/googleauth version: 7595ba02fbce171759c10d69d96e4cd898d1fa93 - package: github.com/nikhilsaraf/go-tools - version: 19004f22be08c82a22e679726ca22853c65919ae + version: a26df67722de7fcf1a8e22cd934e63e553dd3875 - package: github.com/mitchellh/mapstructure version: v1.1.2 diff --git a/plugins/batchedExchange.go b/plugins/batchedExchange.go index a3676602f..ca63c47d6 100644 --- a/plugins/batchedExchange.go +++ b/plugins/batchedExchange.go @@ -169,6 +169,11 @@ func (b BatchedExchange) GetLatestTradeCursor() (interface{}, error) { return b.inner.GetLatestTradeCursor() } +// SubmitOpsSynch is the forced synchronous version of SubmitOps below (same for batchedExchange) +func (b BatchedExchange) SubmitOpsSynch(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error { + return b.SubmitOps(ops, asyncCallback) +} + // SubmitOps performs any finalization or submission step needed by the exchange func (b BatchedExchange) SubmitOps(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error { var e error diff --git a/plugins/fillTracker.go b/plugins/fillTracker.go index 3a02f7ac7..ef2262b03 100644 --- a/plugins/fillTracker.go +++ b/plugins/fillTracker.go @@ -13,10 +13,14 @@ import ( // FillTracker tracks fills type FillTracker struct { - pair *model.TradingPair - threadTracker *multithreading.ThreadTracker - fillTrackable api.FillTrackable - fillTrackerSleepMillis uint32 + pair *model.TradingPair + threadTracker *multithreading.ThreadTracker + fillTrackable api.FillTrackable + fillTrackerSleepMillis uint32 + fillTrackerDeleteCyclesThreshold int64 + + // initialized runtime vars + fillTrackerDeleteCycles int64 // uninitialized handlers []api.FillHandler @@ -31,12 +35,16 @@ func MakeFillTracker( threadTracker *multithreading.ThreadTracker, fillTrackable api.FillTrackable, fillTrackerSleepMillis uint32, + fillTrackerDeleteCyclesThreshold int64, ) api.FillTracker { return &FillTracker{ - pair: pair, - threadTracker: threadTracker, - fillTrackable: fillTrackable, - fillTrackerSleepMillis: fillTrackerSleepMillis, + pair: pair, + threadTracker: threadTracker, + fillTrackable: fillTrackable, + fillTrackerSleepMillis: fillTrackerSleepMillis, + fillTrackerDeleteCyclesThreshold: fillTrackerDeleteCyclesThreshold, + // initialized runtime vars + fillTrackerDeleteCycles: 0, } } @@ -45,6 +53,23 @@ func (f *FillTracker) GetPair() (pair *model.TradingPair) { return f.pair } +// countError updates the error count and returns true if the error limit has been exceeded +func (f *FillTracker) countError() bool { + if f.fillTrackerDeleteCyclesThreshold < 0 { + log.Printf("not deleting any offers because fillTrackerDeleteCyclesThreshold is negative\n") + return false + } + + f.fillTrackerDeleteCycles++ + if f.fillTrackerDeleteCycles <= f.fillTrackerDeleteCyclesThreshold { + log.Printf("not deleting any offers, fillTrackerDeleteCycles (=%d) needs to exceed fillTrackerDeleteCyclesThreshold (=%d)\n", f.fillTrackerDeleteCycles, f.fillTrackerDeleteCyclesThreshold) + return false + } + + log.Printf("deleting all offers, num. continuous fill tracking cycles with errors (including this one): %d; (fillTrackerDeleteCyclesThreshold to be exceeded=%d)\n", f.fillTrackerDeleteCycles, f.fillTrackerDeleteCyclesThreshold) + return true +} + // TrackFills impl func (f *FillTracker) TrackFills() error { // get the last cursor so we only start querying from the current position @@ -58,6 +83,7 @@ func (f *FillTracker) TrackFills() error { for { select { case e := <-ech: + // always return an error if any of the fill handlers return an eror return fmt.Errorf("caught an error when tracking fills: %s", e) default: // do nothing @@ -65,12 +91,18 @@ func (f *FillTracker) TrackFills() error { tradeHistoryResult, e := f.fillTrackable.GetTradeHistory(*f.GetPair(), lastCursor, nil) if e != nil { - return fmt.Errorf("error when fetching trades: %s", e) + eMsg := fmt.Sprintf("error when fetching trades: %s", e) + if f.countError() { + return fmt.Errorf(eMsg) + } + log.Printf("%s\n", eMsg) + f.sleep() + continue } if len(tradeHistoryResult.Trades) > 0 { // use a single goroutine so we handle trades sequentially and also respect the handler sequence - f.threadTracker.TriggerGoroutine(func(inputs []interface{}) { + e = f.threadTracker.TriggerGoroutine(func(inputs []interface{}) { ech := inputs[0].(chan error) defer handlePanic(ech) @@ -87,13 +119,27 @@ func (f *FillTracker) TrackFills() error { } } }, []interface{}{ech, f.handlers, tradeHistoryResult.Trades}) + if e != nil { + eMsg := fmt.Sprintf("error spawning fill handler: %s", e) + if f.countError() { + return fmt.Errorf(eMsg) + } + log.Printf("%s\n", eMsg) + f.sleep() + continue + } } lastCursor = tradeHistoryResult.Cursor - time.Sleep(time.Duration(f.fillTrackerSleepMillis) * time.Millisecond) + f.fillTrackerDeleteCycles = 0 + f.sleep() } } +func (f *FillTracker) sleep() { + time.Sleep(time.Duration(f.fillTrackerSleepMillis) * time.Millisecond) +} + func handlePanic(ech chan error) { if r := recover(); r != nil { e := r.(error) diff --git a/plugins/sdex.go b/plugins/sdex.go index de5dbde1b..c8aba18de 100644 --- a/plugins/sdex.go +++ b/plugins/sdex.go @@ -341,8 +341,18 @@ func (sdex *SDEX) createModifySellOffer(offer *horizon.Offer, selling horizon.As return &result, nil } +// SubmitOpsSynch is the forced synchronous version of SubmitOps below +func (sdex *SDEX) SubmitOpsSynch(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error { + return sdex.submitOps(ops, asyncCallback, false) +} + // SubmitOps submits the passed in operations to the network asynchronously in a single transaction func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator, asyncCallback func(hash string, e error)) error { + return sdex.submitOps(ops, asyncCallback, true) +} + +// submitOps submits the passed in operations to the network in a single transaction. Asynchronous or not based on flag. +func (sdex *SDEX) submitOps(ops []build.TransactionMutator, asyncCallback func(hash string, e error), asyncMode bool) error { sdex.incrementSeqNum() muts := []build.TransactionMutator{ build.Sequence{Sequence: sdex.seqNum}, @@ -372,13 +382,21 @@ func (sdex *SDEX) SubmitOps(ops []build.TransactionMutator, asyncCallback func(h // submit if !sdex.simMode { - log.Println("submitting tx XDR to network (async)") - sdex.threadTracker.TriggerGoroutine(func(inputs []interface{}) { - sdex.submit(txeB64, asyncCallback) - }, nil) + if asyncMode { + log.Println("submitting tx XDR to network (async)") + e = sdex.threadTracker.TriggerGoroutine(func(inputs []interface{}) { + sdex.submit(txeB64, asyncCallback, true) + }, nil) + if e != nil { + return fmt.Errorf("unable to trigger goroutine to submit tx XDR to network asynchronously: %s", e) + } + } else { + log.Println("submitting tx XDR to network (synch)") + sdex.submit(txeB64, asyncCallback, false) + } } else { log.Println("not submitting tx XDR to network in simulation mode, calling asyncCallback with empty hash value") - sdex.invokeAsyncCallback(asyncCallback, "", nil) + sdex.invokeAsyncCallback(asyncCallback, "", nil, asyncMode) } return nil } @@ -404,7 +422,7 @@ func (sdex *SDEX) sign(tx *build.TransactionBuilder) (string, error) { return txe.Base64() } -func (sdex *SDEX) submit(txeB64 string, asyncCallback func(hash string, e error)) { +func (sdex *SDEX) submit(txeB64 string, asyncCallback func(hash string, e error), asyncMode bool) { resp, err := sdex.API.SubmitTransaction(txeB64) if err != nil { if herr, ok := errors.Cause(err).(*horizon.Error); ok { @@ -412,7 +430,7 @@ func (sdex *SDEX) submit(txeB64 string, asyncCallback func(hash string, e error) rcs, err = herr.ResultCodes() if err != nil { log.Printf("(async) error: no result codes from horizon: %s\n", err) - sdex.invokeAsyncCallback(asyncCallback, "", err) + sdex.invokeAsyncCallback(asyncCallback, "", err, asyncMode) return } if rcs.TransactionCode == "tx_bad_seq" { @@ -423,22 +441,34 @@ func (sdex *SDEX) submit(txeB64 string, asyncCallback func(hash string, e error) } else { log.Printf("(async) error: tx failed for unknown reason, error message: %s\n", err) } - sdex.invokeAsyncCallback(asyncCallback, "", err) + sdex.invokeAsyncCallback(asyncCallback, "", err, asyncMode) return } - log.Printf("(async) tx confirmation hash: %s\n", resp.Hash) - sdex.invokeAsyncCallback(asyncCallback, resp.Hash, nil) + modeString := "(synch)" + if asyncMode { + modeString = "(async)" + } + log.Printf("%s tx confirmation hash: %s\n", modeString, resp.Hash) + sdex.invokeAsyncCallback(asyncCallback, resp.Hash, nil, asyncMode) } -func (sdex *SDEX) invokeAsyncCallback(asyncCallback func(hash string, e error), hash string, e error) { +func (sdex *SDEX) invokeAsyncCallback(asyncCallback func(hash string, err error), hash string, err error, asyncMode bool) { if asyncCallback == nil { return } - sdex.threadTracker.TriggerGoroutine(func(inputs []interface{}) { - asyncCallback(hash, e) - }, nil) + if asyncMode { + e := sdex.threadTracker.TriggerGoroutine(func(inputs []interface{}) { + asyncCallback(hash, err) + }, nil) + if e != nil { + log.Printf("unable to trigger goroutine for invokeAsyncCallback: %s", e) + return + } + } else { + asyncCallback(hash, err) + } } // Assets returns the base and quote asset used by sdex diff --git a/trader/config.go b/trader/config.go index 5235d2236..e5dd4973b 100644 --- a/trader/config.go +++ b/trader/config.go @@ -19,29 +19,30 @@ type FeeConfig struct { // 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"` - 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"` - TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE"` - ExchangeAPIKeys []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"` + FillTrackerDeleteCyclesThreshold int64 `valid:"-" toml:"FILL_TRACKER_DELETE_CYCLES_THRESHOLD"` + 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"` + TradingExchange string `valid:"-" toml:"TRADING_EXCHANGE"` + ExchangeAPIKeys []struct { Key string `valid:"-" toml:"KEY"` Secret string `valid:"-" toml:"SECRET"` } `valid:"-" toml:"EXCHANGE_API_KEYS"` diff --git a/trader/trader.go b/trader/trader.go index 97223e139..3ec79e3d4 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -143,7 +143,7 @@ func (t *Trader) deleteAllOffers() { return } - log.Printf("deleting all offers, num. continuous update cycles with errors (including this one): %d\n", t.deleteCycles) + log.Printf("deleting all offers, num. continuous update cycles with errors (including this one): %d; (deleteCyclesThreshold to be exceeded=%d)\n", t.deleteCycles, t.deleteCyclesThreshold) dOps := []build.TransactionMutator{} dOps = append(dOps, t.sdex.DeleteAllOffers(t.sellingAOffers)...) t.sellingAOffers = []horizon.Offer{}