diff --git a/api/timeController.go b/api/timeController.go new file mode 100644 index 000000000..48ed8d803 --- /dev/null +++ b/api/timeController.go @@ -0,0 +1,13 @@ +package api + +import "time" + +// TimeController controls the update loop for the bot +type TimeController interface { + // ShouldUpdate defines when to enter the bot's update cycle + // lastUpdateTime will never start off as the zero value + ShouldUpdate(lastUpdateTime time.Time, currentUpdateTime time.Time) bool + + // SleepTime computes how long we want to sleep before the next call to ShouldUpdate + SleepTime(lastUpdateTime time.Time, currentUpdateTime time.Time) time.Duration +} diff --git a/cmd/trade.go b/cmd/trade.go index 89d17377f..7bc62958d 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -145,6 +145,7 @@ func init() { deleteAllOffersAndExit(botConfig, client, sdex) } + timeController := plugins.MakeIntervalTimeController(time.Duration(botConfig.TickIntervalSeconds) * time.Second) bot := trader.MakeBot( client, botConfig.AssetBase(), @@ -152,7 +153,7 @@ func init() { botConfig.TradingAccount(), sdex, strat, - botConfig.TickIntervalSeconds, + timeController, threadTracker, fixedIterations, dataKey, diff --git a/plugins/intervalTimeController.go b/plugins/intervalTimeController.go new file mode 100644 index 000000000..214004068 --- /dev/null +++ b/plugins/intervalTimeController.go @@ -0,0 +1,35 @@ +package plugins + +import ( + "log" + "time" + + "github.com/lightyeario/kelp/api" +) + +// IntervalTimeController provides a standard time interval +type IntervalTimeController struct { + tickInterval time.Duration +} + +// MakeIntervalTimeController is a factory method +func MakeIntervalTimeController(tickInterval time.Duration) api.TimeController { + return &IntervalTimeController{tickInterval: tickInterval} +} + +var _ api.TimeController = &IntervalTimeController{} + +// ShouldUpdate impl +func (t *IntervalTimeController) ShouldUpdate(lastUpdateTime time.Time, currentUpdateTime time.Time) bool { + elapsedSinceUpdate := currentUpdateTime.Sub(lastUpdateTime) + shouldUpdate := elapsedSinceUpdate >= t.tickInterval + log.Printf("intervalTimeController tickInterval=%s, shouldUpdate=%v, elapsedSinceUpdate=%s\n", t.tickInterval, shouldUpdate, elapsedSinceUpdate) + return shouldUpdate +} + +// SleepTime impl +func (t *IntervalTimeController) SleepTime(lastUpdateTime time.Time, currentUpdateTime time.Time) time.Duration { + // use time till now as opposed to currentUpdateTime because we want the start of the clock cycle to be synchronized + elapsedSinceUpdate := time.Since(lastUpdateTime) + return time.Duration(t.tickInterval.Nanoseconds() - elapsedSinceUpdate.Nanoseconds()) +} diff --git a/trader/trader.go b/trader/trader.go index 57a3674d4..1d0aa1381 100644 --- a/trader/trader.go +++ b/trader/trader.go @@ -20,17 +20,17 @@ const maxLumenTrust float64 = math.MaxFloat64 // Trader represents a market making bot, which is composed of various parts include the strategy and various APIs. type Trader struct { - api *horizon.Client - assetBase horizon.Asset - assetQuote horizon.Asset - tradingAccount string - sdex *plugins.SDEX - strat api.Strategy // the instance of this bot is bound to this strategy - tickIntervalSeconds int32 - threadTracker *multithreading.ThreadTracker - fixedIterations *uint64 - dataKey *model.BotKey - alert api.Alert + api *horizon.Client + assetBase horizon.Asset + assetQuote horizon.Asset + tradingAccount string + sdex *plugins.SDEX + strat api.Strategy // the instance of this bot is bound to this strategy + timeController api.TimeController + threadTracker *multithreading.ThreadTracker + fixedIterations *uint64 + dataKey *model.BotKey + alert api.Alert // uninitialized maxAssetA float64 @@ -49,43 +49,55 @@ func MakeBot( tradingAccount string, sdex *plugins.SDEX, strat api.Strategy, - tickIntervalSeconds int32, + timeController api.TimeController, threadTracker *multithreading.ThreadTracker, fixedIterations *uint64, dataKey *model.BotKey, alert api.Alert, ) *Trader { return &Trader{ - api: api, - assetBase: assetBase, - assetQuote: assetQuote, - tradingAccount: tradingAccount, - sdex: sdex, - strat: strat, - tickIntervalSeconds: tickIntervalSeconds, - threadTracker: threadTracker, - fixedIterations: fixedIterations, - dataKey: dataKey, - alert: alert, + api: api, + assetBase: assetBase, + assetQuote: assetQuote, + tradingAccount: tradingAccount, + sdex: sdex, + strat: strat, + timeController: timeController, + threadTracker: threadTracker, + fixedIterations: fixedIterations, + dataKey: dataKey, + alert: alert, } } // Start starts the bot with the injected strategy func (t *Trader) Start() { + log.Println("----------------------------------------------------------------------------------------------------") + var lastUpdateTime time.Time + for { - log.Println("----------------------------------------------------------------------------------------------------") - t.update() - if t.fixedIterations != nil { - *t.fixedIterations = *t.fixedIterations - 1 - if *t.fixedIterations <= 0 { - log.Printf("finished requested number of iterations, waiting for all threads to finish...\n") - t.threadTracker.Wait() - log.Printf("...all threads finished, stopping bot update loop\n") - return + currentUpdateTime := time.Now() + if lastUpdateTime.IsZero() || t.timeController.ShouldUpdate(lastUpdateTime, currentUpdateTime) { + t.update() + if t.fixedIterations != nil { + *t.fixedIterations = *t.fixedIterations - 1 + if *t.fixedIterations <= 0 { + log.Printf("finished requested number of iterations, waiting for all threads to finish...\n") + t.threadTracker.Wait() + log.Printf("...all threads finished, stopping bot update loop\n") + return + } } + + // wait for any goroutines from the current update to finish so we don't have inconsistent state reads + t.threadTracker.Wait() + log.Println("----------------------------------------------------------------------------------------------------") + lastUpdateTime = currentUpdateTime } - log.Printf("sleeping for %d seconds...\n", t.tickIntervalSeconds) - time.Sleep(time.Duration(t.tickIntervalSeconds) * time.Second) + + sleepTime := t.timeController.SleepTime(lastUpdateTime, currentUpdateTime) + log.Printf("sleeping for %s...\n", sleepTime) + time.Sleep(sleepTime) } }