From 3f1b1f3032cd5de08f6d3dc101ac5087a44a1917 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 7 May 2019 14:24:20 -0700 Subject: [PATCH 01/14] 1 - extract and unify common toml representations for ExchangeAPIKeys, ExchangeParams, and ExchangeHeaders --- cmd/trade.go | 9 +--- plugins/mirrorStrategy.go | 69 ++++---------------------- support/toml/config_mirror_strategy.go | 39 +++++++++++++++ support/toml/config_trader.go | 21 ++++++++ trader/config.go | 40 ++++++--------- 5 files changed, 88 insertions(+), 90 deletions(-) create mode 100644 support/toml/config_mirror_strategy.go create mode 100644 support/toml/config_trader.go diff --git a/cmd/trade.go b/cmd/trade.go index 44b5960c6..8266ca3c8 100644 --- a/cmd/trade.go +++ b/cmd/trade.go @@ -198,14 +198,6 @@ func makeExchangeShimSdex( var e error var exchangeShim api.ExchangeShim if !botConfig.IsTradingSdex() { - exchangeAPIKeys := []api.ExchangeAPIKey{} - for _, apiKey := range botConfig.ExchangeAPIKeys { - exchangeAPIKeys = append(exchangeAPIKeys, api.ExchangeAPIKey{ - Key: apiKey.Key, - Secret: apiKey.Secret, - }) - } - exchangeParams := []api.ExchangeParam{} for _, param := range botConfig.ExchangeParams { exchangeParams = append(exchangeParams, api.ExchangeParam{ @@ -222,6 +214,7 @@ func makeExchangeShimSdex( }) } + exchangeAPIKeys := botConfig.ExchangeAPIKeys.ToExchangeAPIKeys() var exchangeAPI api.Exchange exchangeAPI, e = plugins.MakeTradingExchange(botConfig.TradingExchange, exchangeAPIKeys, exchangeParams, exchangeHeaders, *options.simMode) if e != nil { diff --git a/plugins/mirrorStrategy.go b/plugins/mirrorStrategy.go index e798a19bf..e71ae4cd2 100644 --- a/plugins/mirrorStrategy.go +++ b/plugins/mirrorStrategy.go @@ -9,57 +9,10 @@ import ( "github.com/stellar/go/clients/horizon" "github.com/stellar/kelp/api" "github.com/stellar/kelp/model" + "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/support/utils" ) -type exchangeAPIKeysToml []struct { - Key string `valid:"-" toml:"KEY"` - Secret string `valid:"-" toml:"SECRET"` -} - -func (t *exchangeAPIKeysToml) toExchangeAPIKeys() []api.ExchangeAPIKey { - apiKeys := []api.ExchangeAPIKey{} - for _, apiKey := range *t { - apiKeys = append(apiKeys, api.ExchangeAPIKey{ - Key: apiKey.Key, - Secret: apiKey.Secret, - }) - } - return apiKeys -} - -type exchangeParamsToml []struct { - Param string `valid:"-" toml:"PARAM"` - Value string `valid:"-" toml:"VALUE"` -} - -func (t *exchangeParamsToml) toExchangeParams() []api.ExchangeParam { - exchangeParams := []api.ExchangeParam{} - for _, param := range *t { - exchangeParams = append(exchangeParams, api.ExchangeParam{ - Param: param.Param, - Value: param.Value, - }) - } - return exchangeParams -} - -type exchangeHeadersToml []struct { - Header string `valid:"-" toml:"HEADER"` - Value string `valid:"-" toml:"VALUE"` -} - -func (t *exchangeHeadersToml) toExchangeHeaders() []api.ExchangeHeader { - apiHeaders := []api.ExchangeHeader{} - for _, header := range *t { - apiHeaders = append(apiHeaders, api.ExchangeHeader{ - Header: header.Header, - Value: header.Value, - }) - } - return apiHeaders -} - // mirrorConfig contains the configuration params for this strategy type mirrorConfig struct { Exchange string `valid:"-" toml:"EXCHANGE"` @@ -71,13 +24,13 @@ type mirrorConfig struct { PricePrecisionOverride *int8 `valid:"-" toml:"PRICE_PRECISION_OVERRIDE"` VolumePrecisionOverride *int8 `valid:"-" toml:"VOLUME_PRECISION_OVERRIDE"` // Deprecated: use MIN_BASE_VOLUME_OVERRIDE instead - MinBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_BASE_VOLUME" deprecated:"true"` - MinBaseVolumeOverride *float64 `valid:"-" toml:"MIN_BASE_VOLUME_OVERRIDE"` - MinQuoteVolumeOverride *float64 `valid:"-" toml:"MIN_QUOTE_VOLUME_OVERRIDE"` - OffsetTrades bool `valid:"-" toml:"OFFSET_TRADES"` - ExchangeAPIKeys exchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS"` - ExchangeParams exchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS"` - ExchangeHeaders exchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS"` + MinBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_BASE_VOLUME" deprecated:"true"` + MinBaseVolumeOverride *float64 `valid:"-" toml:"MIN_BASE_VOLUME_OVERRIDE"` + MinQuoteVolumeOverride *float64 `valid:"-" toml:"MIN_QUOTE_VOLUME_OVERRIDE"` + OffsetTrades bool `valid:"-" toml:"OFFSET_TRADES"` + ExchangeAPIKeys toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS"` + ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS"` + ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS"` } // String impl. @@ -154,9 +107,9 @@ func makeMirrorStrategy(sdex *SDEX, ieif *IEIF, pair *model.TradingPair, baseAss var exchange api.Exchange var e error if config.OffsetTrades { - exchangeAPIKeys := config.ExchangeAPIKeys.toExchangeAPIKeys() - exchangeParams := config.ExchangeParams.toExchangeParams() - exchangeHeaders := config.ExchangeHeaders.toExchangeHeaders() + exchangeAPIKeys := config.ExchangeAPIKeys.ToExchangeAPIKeys() + exchangeParams := config.ExchangeParams.ToExchangeParams() + exchangeHeaders := config.ExchangeHeaders.ToExchangeHeaders() exchange, e = MakeTradingExchange(config.Exchange, exchangeAPIKeys, exchangeParams, exchangeHeaders, simMode) if e != nil { return nil, e diff --git a/support/toml/config_mirror_strategy.go b/support/toml/config_mirror_strategy.go new file mode 100644 index 000000000..e8d19bdda --- /dev/null +++ b/support/toml/config_mirror_strategy.go @@ -0,0 +1,39 @@ +package toml + +import "github.com/stellar/kelp/api" + +// ExchangeParamsToml is the toml representation of ExchangeParams +type ExchangeParamsToml []struct { + Param string `valid:"-" toml:"PARAM"` + Value string `valid:"-" toml:"VALUE"` +} + +// ToExchangeParams converts object +func (t *ExchangeParamsToml) ToExchangeParams() []api.ExchangeParam { + exchangeParams := []api.ExchangeParam{} + for _, param := range *t { + exchangeParams = append(exchangeParams, api.ExchangeParam{ + Param: param.Param, + Value: param.Value, + }) + } + return exchangeParams +} + +// ExchangeHeadersToml is the toml representation of ExchangeHeaders +type ExchangeHeadersToml []struct { + Header string `valid:"-" toml:"HEADER"` + Value string `valid:"-" toml:"VALUE"` +} + +// ToExchangeHeaders converts object +func (t *ExchangeHeadersToml) ToExchangeHeaders() []api.ExchangeHeader { + apiHeaders := []api.ExchangeHeader{} + for _, header := range *t { + apiHeaders = append(apiHeaders, api.ExchangeHeader{ + Header: header.Header, + Value: header.Value, + }) + } + return apiHeaders +} diff --git a/support/toml/config_trader.go b/support/toml/config_trader.go new file mode 100644 index 000000000..f2362a855 --- /dev/null +++ b/support/toml/config_trader.go @@ -0,0 +1,21 @@ +package toml + +import "github.com/stellar/kelp/api" + +// ExchangeAPIKeysToml is the toml representation of ExchangeAPIKeys +type ExchangeAPIKeysToml []struct { + Key string `valid:"-" toml:"KEY"` + Secret string `valid:"-" toml:"SECRET"` +} + +// ToExchangeAPIKeys converts object +func (t *ExchangeAPIKeysToml) ToExchangeAPIKeys() []api.ExchangeAPIKey { + apiKeys := []api.ExchangeAPIKey{} + for _, apiKey := range *t { + apiKeys = append(apiKeys, api.ExchangeAPIKey{ + Key: apiKey.Key, + Secret: apiKey.Secret, + }) + } + return apiKeys +} diff --git a/trader/config.go b/trader/config.go index a2060cafd..b622faaba 100644 --- a/trader/config.go +++ b/trader/config.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stellar/go/clients/horizon" + "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/support/utils" ) @@ -37,30 +38,21 @@ type BotConfig struct { CentralizedPricePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_PRICE_PRECISION_OVERRIDE"` CentralizedVolumePrecisionOverride *int8 `valid:"-" toml:"CENTRALIZED_VOLUME_PRECISION_OVERRIDE"` // Deprecated: use CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE instead - MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true"` - CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE"` - CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE"` - 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"` - ExchangeParams []struct { - Param string `valid:"-" toml:"PARAM"` - Value string `valid:"-" toml:"VALUE"` - } `valid:"-" toml:"EXCHANGE_PARAMS"` - ExchangeHeaders []struct { - Header string `valid:"-" toml:"HEADER"` - Value string `valid:"-" toml:"VALUE"` - } `valid:"-" toml:"EXCHANGE_HEADERS"` + MinCentralizedBaseVolumeDeprecated *float64 `valid:"-" toml:"MIN_CENTRALIZED_BASE_VOLUME" deprecated:"true"` + CentralizedMinBaseVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_BASE_VOLUME_OVERRIDE"` + CentralizedMinQuoteVolumeOverride *float64 `valid:"-" toml:"CENTRALIZED_MIN_QUOTE_VOLUME_OVERRIDE"` + 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 toml.ExchangeAPIKeysToml `valid:"-" toml:"EXCHANGE_API_KEYS"` + ExchangeParams toml.ExchangeParamsToml `valid:"-" toml:"EXCHANGE_PARAMS"` + ExchangeHeaders toml.ExchangeHeadersToml `valid:"-" toml:"EXCHANGE_HEADERS"` // initialized later tradingAccount *string From f1be6227e77ce1b47976fe54ed76992b93789e9d Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 7 May 2019 19:00:16 -0700 Subject: [PATCH 02/14] 2 - Autogenerate bot config and save to file --- gui/backend/api_server.go | 19 ++++++++-- gui/backend/autogenerate_bot.go | 67 +++++++++++++++++++++++++++++++++ gui/backend/routes.go | 1 + gui/backend/version.go | 2 +- support/toml/writer.go | 23 +++++++++++ trader/config.go | 45 ++++++++++++++++++++++ 6 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 gui/backend/autogenerate_bot.go create mode 100644 support/toml/writer.go diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index 5b0b8d9d8..3d34ee5cf 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -9,7 +9,9 @@ import ( // APIServer is an instance of the API service type APIServer struct { - binPath string + dirPath string + binPath string + configsPath string } // MakeAPIServer is a factory method @@ -19,14 +21,23 @@ func MakeAPIServer() (*APIServer, error) { return nil, fmt.Errorf("could not get binPath of currently running binary: %s", e) } + dirPath := filepath.Dir(binPath) + configsPath := dirPath + "/ops/configs" + return &APIServer{ - binPath: binPath, + dirPath: dirPath, + binPath: binPath, + configsPath: configsPath, }, nil } -func (s *APIServer) runCommand(cmd string) ([]byte, error) { +func (s *APIServer) runKelpCommand(cmd string) ([]byte, error) { cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) - bytes, e := exec.Command("bash", "-c", cmdString).Output() + return runBashCommand(cmdString) +} + +func runBashCommand(cmd string) ([]byte, error) { + bytes, e := exec.Command("bash", "-c", cmd).Output() if e != nil { return nil, fmt.Errorf("could not run bash command '%s': %s", cmd, e) } diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go new file mode 100644 index 000000000..fa044e367 --- /dev/null +++ b/gui/backend/autogenerate_bot.go @@ -0,0 +1,67 @@ +package backend + +import ( + "fmt" + "log" + "net/http" + + "github.com/stellar/go/keypair" + "github.com/stellar/kelp/support/toml" + "github.com/stellar/kelp/trader" +) + +func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { + kp, e := keypair.Random() + if e != nil { + w.Write([]byte(fmt.Sprintf("error generating keypair: %s\n", e))) + return + } + + centralizedPricePrecisionOverride := int8(6) + centralizedVolumePrecisionOverride := int8(1) + centralizedMinBaseVolumeOverride := float64(30.0) + centralizedMinQuoteVolumeOverride := float64(10.0) + sampleTrader := trader.MakeBotConfig( + "", + kp.Seed(), + "XLM", + "", + "COUPON", + "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI", + 300, + 0, + 5, + "both", + 0, + 0, + "https://horizon-testnet.stellar.org", + nil, + &trader.FeeConfig{ + CapacityTrigger: 0.8, + Percentile: 90, + MaxOpFeeStroops: 5000, + }, + ¢ralizedPricePrecisionOverride, + ¢ralizedVolumePrecisionOverride, + ¢ralizedMinBaseVolumeOverride, + ¢ralizedMinQuoteVolumeOverride, + ) + + _, e = runBashCommand("mkdir -p " + s.configsPath) + if e != nil { + w.Write([]byte(fmt.Sprintf("error running mkdir command: %s\n", e))) + return + } + + filePath := s.configsPath + "/autogenerated_test_bot.cfg" + log.Printf("writing autogenerated bot config to file: %s\n", filePath) + + e = toml.WriteFile(filePath, sampleTrader) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error writing toml file: %s\n", e))) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/gui/backend/routes.go b/gui/backend/routes.go index 8e0a9ea46..db99f0675 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -10,5 +10,6 @@ import ( func SetRoutes(r *chi.Mux, s *APIServer) { r.Route("/api/v1", func(r chi.Router) { r.Get("/version", http.HandlerFunc(s.version)) + r.Get("/autogenerate", http.HandlerFunc(s.autogenerateBot)) }) } diff --git a/gui/backend/version.go b/gui/backend/version.go index 6b1f01531..10fd5121a 100644 --- a/gui/backend/version.go +++ b/gui/backend/version.go @@ -6,7 +6,7 @@ import ( ) func (s *APIServer) version(w http.ResponseWriter, r *http.Request) { - bytes, e := s.runCommand("version | grep version | cut -d':' -f3") + bytes, e := s.runKelpCommand("version | grep version | cut -d':' -f3") if e != nil { w.Write([]byte(fmt.Sprintf("error in version command: %s\n", e))) return diff --git a/support/toml/writer.go b/support/toml/writer.go new file mode 100644 index 000000000..ccaf91483 --- /dev/null +++ b/support/toml/writer.go @@ -0,0 +1,23 @@ +package toml + +import ( + "bytes" + "fmt" + "io/ioutil" + + "github.com/BurntSushi/toml" +) + +// WriteFile is a helper method to write toml files +func WriteFile(filePath string, v interface{}) error { + var fileBuf bytes.Buffer + encoder := toml.NewEncoder(&fileBuf) + + e := encoder.Encode(v) + if e != nil { + return fmt.Errorf("error encoding file as toml: %s", e) + } + + ioutil.WriteFile(filePath, fileBuf.Bytes(), 0644) + return nil +} diff --git a/trader/config.go b/trader/config.go index b622faaba..85dbc0184 100644 --- a/trader/config.go +++ b/trader/config.go @@ -62,6 +62,51 @@ type BotConfig struct { isTradingSdex bool } +// MakeBotConfig factory method for BotConfig +func MakeBotConfig( + sourceSecretSeed string, + tradingSecretSeed string, + assetCodeA string, + issuerA string, + assetCodeB string, + issuerB string, + tickIntervalSeconds int32, + maxTickDelayMillis int64, + deleteCyclesThreshold int64, + submitMode string, + fillTrackerSleepMillis uint32, + fillTrackerDeleteCyclesThreshold int64, + horizonURL string, + ccxtRestURL *string, + fee *FeeConfig, + centralizedPricePrecisionOverride *int8, + centralizedVolumePrecisionOverride *int8, + centralizedMinBaseVolumeOverride *float64, + centralizedMinQuoteVolumeOverride *float64, +) *BotConfig { + return &BotConfig{ + SourceSecretSeed: sourceSecretSeed, + TradingSecretSeed: tradingSecretSeed, + AssetCodeA: assetCodeA, + IssuerA: issuerA, + AssetCodeB: assetCodeB, + IssuerB: issuerB, + TickIntervalSeconds: tickIntervalSeconds, + MaxTickDelayMillis: maxTickDelayMillis, + DeleteCyclesThreshold: deleteCyclesThreshold, + SubmitMode: submitMode, + FillTrackerSleepMillis: fillTrackerSleepMillis, + FillTrackerDeleteCyclesThreshold: fillTrackerDeleteCyclesThreshold, + HorizonURL: horizonURL, + CcxtRestURL: ccxtRestURL, + Fee: fee, + CentralizedPricePrecisionOverride: centralizedPricePrecisionOverride, + CentralizedVolumePrecisionOverride: centralizedVolumePrecisionOverride, + CentralizedMinBaseVolumeOverride: centralizedMinBaseVolumeOverride, + CentralizedMinQuoteVolumeOverride: centralizedMinQuoteVolumeOverride, + } +} + // String impl. func (b BotConfig) String() string { return utils.StructString(b, map[string]func(interface{}) interface{}{ From d9ecd34d55f81d6189c7279b98869fa97082f93f Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Tue, 7 May 2019 19:40:42 -0700 Subject: [PATCH 03/14] 3 - autogenerate button triggers bot creation --- gui/backend/autogenerate_bot.go | 11 +++++++++++ gui/model/bot.go | 21 +++++++++++++++++++++ gui/web/src/App.js | 4 +++- gui/web/src/components/screens/Bots/Bots.js | 19 ++++++++++++++----- gui/web/src/kelp-ops-api/autogenerate.js | 5 +++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 gui/model/bot.go create mode 100644 gui/web/src/kelp-ops-api/autogenerate.js diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index fa044e367..c88b3dac7 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -1,11 +1,13 @@ package backend import ( + "encoding/json" "fmt" "log" "net/http" "github.com/stellar/go/keypair" + "github.com/stellar/kelp/gui/model" "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/trader" ) @@ -63,5 +65,14 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { return } + bot := model.MakeAutogeneratedBot() + botJson, e := json.Marshal(*bot) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("unable to serialize bot: %s\n", e))) + return + } + w.WriteHeader(http.StatusOK) + w.Write(botJson) } diff --git a/gui/model/bot.go b/gui/model/bot.go new file mode 100644 index 000000000..c6f19e4b6 --- /dev/null +++ b/gui/model/bot.go @@ -0,0 +1,21 @@ +package model + +// Bot represents a kelp bot instance +type Bot struct { + Name string `json:"name"` + Running bool `json:"running"` + Test bool `json:"test"` + Warnings uint16 `json:"warnings"` + Errors uint16 `json:"errors"` +} + +// MakeAutogeneratedBot factory method +func MakeAutogeneratedBot() *Bot { + return &Bot{ + Name: "George the Auspicious Octopus", + Running: false, + Test: true, + Warnings: 0, + Errors: 0, + } +} diff --git a/gui/web/src/App.js b/gui/web/src/App.js index abd9a487a..5530ee42a 100644 --- a/gui/web/src/App.js +++ b/gui/web/src/App.js @@ -46,7 +46,9 @@ class App extends Component {
- + } + /> diff --git a/gui/web/src/components/screens/Bots/Bots.js b/gui/web/src/components/screens/Bots/Bots.js index 7280db715..b6d0bd8a1 100644 --- a/gui/web/src/components/screens/Bots/Bots.js +++ b/gui/web/src/components/screens/Bots/Bots.js @@ -5,6 +5,7 @@ import EmptyList from '../../molecules/EmptyList/EmptyList'; import ScreenHeader from '../../molecules/ScreenHeader/ScreenHeader'; import grid from '../../_styles/grid.module.scss'; +import autogenerate from '../../../kelp-ops-api/autogenerate'; const placeaholderBots = [ { @@ -42,6 +43,13 @@ class Bots extends Component { this.autogenerateBot = this.autogenerateBot.bind(this); this.createBot = this.createBot.bind(this); } + + componentWillUnmount() { + if (this._asyncRequest) { + this._asyncRequest.cancel(); + this._asyncRequest = null; + } + } gotoForm() { this.props.history.push('/new') @@ -52,11 +60,12 @@ class Bots extends Component { } autogenerateBot() { - let rand = Math.floor(Math.random() * placeaholderBots.length); - let newElement = placeaholderBots[rand]; - this.setState(prevState => ({ - bots: [...prevState.bots, newElement] - })) + this._asyncRequest = autogenerate(this.props.baseUrl).then(newBot => { + this._asyncRequest = null; + this.setState(prevState => ({ + bots: [...prevState.bots, newBot] + })) + }); } createBot() { diff --git a/gui/web/src/kelp-ops-api/autogenerate.js b/gui/web/src/kelp-ops-api/autogenerate.js new file mode 100644 index 000000000..8f45dfd42 --- /dev/null +++ b/gui/web/src/kelp-ops-api/autogenerate.js @@ -0,0 +1,5 @@ +export default (baseUrl) => { + return fetch(baseUrl + "/api/v1/autogenerate").then(resp => { + return resp.json(); + }); +}; \ No newline at end of file From 690d9cec6e49eff920ced7187472e1fb1b52b166 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Wed, 8 May 2019 16:48:01 -0700 Subject: [PATCH 04/14] 4 - config file name derived from bot name and fund autogenerated bot account --- gui/backend/autogenerate_bot.go | 88 ++++++++++++++++++++------------- gui/model/bot.go | 22 +++++++++ 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index c88b3dac7..05b680192 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -6,12 +6,21 @@ import ( "log" "net/http" + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" "github.com/stellar/kelp/gui/model" "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/trader" ) +const publicTestnetHorizon = "https://horizon-testnet.stellar.org" + +var centralizedPricePrecisionOverride = int8(6) +var centralizedVolumePrecisionOverride = int8(1) +var centralizedMinBaseVolumeOverride = float64(30.0) +var centralizedMinQuoteVolumeOverride = float64(10.0) + func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { kp, e := keypair.Random() if e != nil { @@ -19,35 +28,8 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { return } - centralizedPricePrecisionOverride := int8(6) - centralizedVolumePrecisionOverride := int8(1) - centralizedMinBaseVolumeOverride := float64(30.0) - centralizedMinQuoteVolumeOverride := float64(10.0) - sampleTrader := trader.MakeBotConfig( - "", - kp.Seed(), - "XLM", - "", - "COUPON", - "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI", - 300, - 0, - 5, - "both", - 0, - 0, - "https://horizon-testnet.stellar.org", - nil, - &trader.FeeConfig{ - CapacityTrigger: 0.8, - Percentile: 90, - MaxOpFeeStroops: 5000, - }, - ¢ralizedPricePrecisionOverride, - ¢ralizedVolumePrecisionOverride, - ¢ralizedMinBaseVolumeOverride, - ¢ralizedMinQuoteVolumeOverride, - ) + bot := model.MakeAutogeneratedBot() + go fundBot(kp.Address(), bot.Name) _, e = runBashCommand("mkdir -p " + s.configsPath) if e != nil { @@ -55,17 +37,17 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { return } - filePath := s.configsPath + "/autogenerated_test_bot.cfg" - log.Printf("writing autogenerated bot config to file: %s\n", filePath) - - e = toml.WriteFile(filePath, sampleTrader) + sampleTrader := makeSampleTrader(kp.Seed()) + filenamePair := bot.Filenames() + traderFilePath := fmt.Sprintf("%s/%s", s.configsPath, filenamePair.Trader) + log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath) + e = toml.WriteFile(traderFilePath, sampleTrader) if e != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("error writing toml file: %s\n", e))) return } - bot := model.MakeAutogeneratedBot() botJson, e := json.Marshal(*bot) if e != nil { w.WriteHeader(http.StatusInternalServerError) @@ -76,3 +58,41 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write(botJson) } + +func fundBot(address string, botName string) { + client := horizonclient.DefaultTestNetClient + txSuccess, e := client.Fund(address) + if e != nil { + log.Printf("error funding address %s for bot '%s': %s\n", address, botName, e) + } else { + log.Printf("successfully funded account %s for bot '%s': %s\n", address, botName, txSuccess.TransactionSuccessToString()) + } +} + +func makeSampleTrader(seed string) *trader.BotConfig { + return trader.MakeBotConfig( + "", + seed, + "XLM", + "", + "COUPON", + "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI", + 300, + 0, + 5, + "both", + 0, + 0, + publicTestnetHorizon, + nil, + &trader.FeeConfig{ + CapacityTrigger: 0.8, + Percentile: 90, + MaxOpFeeStroops: 5000, + }, + ¢ralizedPricePrecisionOverride, + ¢ralizedVolumePrecisionOverride, + ¢ralizedMinBaseVolumeOverride, + ¢ralizedMinQuoteVolumeOverride, + ) +} diff --git a/gui/model/bot.go b/gui/model/bot.go index c6f19e4b6..3747fba72 100644 --- a/gui/model/bot.go +++ b/gui/model/bot.go @@ -1,8 +1,14 @@ package model +import ( + "fmt" + "strings" +) + // Bot represents a kelp bot instance type Bot struct { Name string `json:"name"` + Strategy string `json:"strategy"` Running bool `json:"running"` Test bool `json:"test"` Warnings uint16 `json:"warnings"` @@ -13,9 +19,25 @@ type Bot struct { func MakeAutogeneratedBot() *Bot { return &Bot{ Name: "George the Auspicious Octopus", + Strategy: "buysell", Running: false, Test: true, Warnings: 0, Errors: 0, } } + +// FilenamePair represents the two config filenames associated with a bot +type FilenamePair struct { + Trader string + Strategy string +} + +// Filename where we should save bot file +func (b *Bot) Filenames() *FilenamePair { + converted := strings.ToLower(strings.ReplaceAll(b.Name, " ", "_")) + return &FilenamePair{ + Trader: fmt.Sprintf("%s__trader.%s", converted, "cfg"), + Strategy: fmt.Sprintf("%s__strategy_%s.%s", converted, b.Strategy, "cfg"), + } +} From e088025699a929adcbcff2669b4aadd7e3d64105 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 12:38:36 -0700 Subject: [PATCH 05/14] 5 - create trustline for autogenerated bot account --- gui/backend/autogenerate_bot.go | 39 ++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index 05b680192..bee1d60f0 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -6,8 +6,9 @@ import ( "log" "net/http" + "github.com/stellar/go/build" + "github.com/stellar/go/clients/horizon" "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" "github.com/stellar/kelp/gui/model" "github.com/stellar/kelp/support/toml" @@ -29,7 +30,7 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { } bot := model.MakeAutogeneratedBot() - go fundBot(kp.Address(), bot.Name) + go setupAccount(kp.Address(), kp.Seed(), bot.Name) _, e = runBashCommand("mkdir -p " + s.configsPath) if e != nil { @@ -59,14 +60,42 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { w.Write(botJson) } -func fundBot(address string, botName string) { - client := horizonclient.DefaultTestNetClient - txSuccess, e := client.Fund(address) +func setupAccount(address string, signer string, botName string) { + hclient := horizonclient.DefaultTestNetClient + txSuccess, e := hclient.Fund(address) if e != nil { log.Printf("error funding address %s for bot '%s': %s\n", address, botName, e) } else { log.Printf("successfully funded account %s for bot '%s': %s\n", address, botName, txSuccess.TransactionSuccessToString()) } + + client := horizon.DefaultTestNetClient + txn, e := build.Transaction( + build.SourceAccount{address}, + build.AutoSequence{client}, + build.TestNetwork, + build.Trust("COUPON", "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI"), + ) + if e != nil { + log.Printf("cannot create trustline transaction for account %s for bot '%s': %s\n", address, botName, e) + return + } + txnS, e := txn.Sign(signer) + if e != nil { + log.Printf("cannot sign trustline transaction for account %s for bot '%s': %s\n", address, botName, e) + return + } + txn64, e := txnS.Base64() + if e != nil { + log.Printf("cannot convert trustline transaction to base64 for account %s for bot '%s': %s\n", address, botName, e) + return + } + resp, e := client.SubmitTransaction(txn64) + if e != nil { + log.Printf("error submitting change trust transaction for address %s for bot '%s': %s\n", address, botName, e) + return + } + log.Printf("successfully added trustline for address %s for bot '%s': %s\n", address, botName, resp) } func makeSampleTrader(seed string) *trader.BotConfig { From e4f516bdb35294bb37bc190498b242cd1e89f307 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 12:41:18 -0700 Subject: [PATCH 06/14] 6 - fields for struct object --- gui/backend/autogenerate_bot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index bee1d60f0..237e2bbab 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -71,8 +71,8 @@ func setupAccount(address string, signer string, botName string) { client := horizon.DefaultTestNetClient txn, e := build.Transaction( - build.SourceAccount{address}, - build.AutoSequence{client}, + build.SourceAccount{AddressOrSeed: address}, + build.AutoSequence{SequenceProvider: client}, build.TestNetwork, build.Trust("COUPON", "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI"), ) From e8a4c06e68e960ffe5cd8b883682738267b1da84 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 12:41:54 -0700 Subject: [PATCH 07/14] 7 - format for printing trustline success --- gui/backend/autogenerate_bot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index 237e2bbab..e38cb723d 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -95,7 +95,7 @@ func setupAccount(address string, signer string, botName string) { log.Printf("error submitting change trust transaction for address %s for bot '%s': %s\n", address, botName, e) return } - log.Printf("successfully added trustline for address %s for bot '%s': %s\n", address, botName, resp) + log.Printf("successfully added trustline for address %s for bot '%s': %v\n", address, botName, resp) } func makeSampleTrader(seed string) *trader.BotConfig { From 1a07d87f3ae9395a9e5113ab1016826e9253e701 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 14:25:24 -0700 Subject: [PATCH 08/14] 8 - generate buysell strategy file --- gui/backend/autogenerate_bot.go | 45 ++++++++++++++++++++++++++-- plugins/buysellStrategy.go | 39 ++++++++++++++++++++---- plugins/factory.go | 2 +- plugins/sellStrategy.go | 2 +- plugins/staticSpreadLevelProvider.go | 8 ++--- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index e38cb723d..ca80cdfb8 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -11,6 +11,7 @@ import ( "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" "github.com/stellar/kelp/gui/model" + "github.com/stellar/kelp/plugins" "github.com/stellar/kelp/support/toml" "github.com/stellar/kelp/trader" ) @@ -38,14 +39,24 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { return } - sampleTrader := makeSampleTrader(kp.Seed()) filenamePair := bot.Filenames() + sampleTrader := makeSampleTrader(kp.Seed()) traderFilePath := fmt.Sprintf("%s/%s", s.configsPath, filenamePair.Trader) log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath) e = toml.WriteFile(traderFilePath, sampleTrader) if e != nil { w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(fmt.Sprintf("error writing toml file: %s\n", e))) + w.Write([]byte(fmt.Sprintf("error writing trader toml file: %s\n", e))) + return + } + + sampleBuysell := makeSampleBuysell() + strategyFilePath := fmt.Sprintf("%s/%s", s.configsPath, filenamePair.Strategy) + log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath) + e = toml.WriteFile(strategyFilePath, sampleBuysell) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error writing strategy toml file: %s\n", e))) return } @@ -125,3 +136,33 @@ func makeSampleTrader(seed string) *trader.BotConfig { ¢ralizedMinQuoteVolumeOverride, ) } + +func makeSampleBuysell() *plugins.BuySellConfig { + return plugins.MakeBuysellConfig( + 0.001, + 0.001, + 0.0, + 0.0, + true, + 10.0, + "exchange", + "kraken/XXLM/ZUSD", + "fixed", + "1.0", + []plugins.StaticLevel{ + plugins.StaticLevel{ + SPREAD: 0.0010, + AMOUNT: 100.0, + }, plugins.StaticLevel{ + SPREAD: 0.0015, + AMOUNT: 100.0, + }, plugins.StaticLevel{ + SPREAD: 0.0020, + AMOUNT: 100.0, + }, plugins.StaticLevel{ + SPREAD: 0.0025, + AMOUNT: 100.0, + }, + }, + ) +} diff --git a/plugins/buysellStrategy.go b/plugins/buysellStrategy.go index 88e86b1ce..c56ee37b0 100644 --- a/plugins/buysellStrategy.go +++ b/plugins/buysellStrategy.go @@ -9,8 +9,8 @@ import ( "github.com/stellar/kelp/support/utils" ) -// buySellConfig contains the configuration params for this strategy -type buySellConfig struct { +// BuySellConfig contains the configuration params for this strategy +type BuySellConfig struct { PriceTolerance float64 `valid:"-" toml:"PRICE_TOLERANCE"` AmountTolerance float64 `valid:"-" toml:"AMOUNT_TOLERANCE"` RateOffsetPercent float64 `valid:"-" toml:"RATE_OFFSET_PERCENT"` @@ -21,11 +21,40 @@ type buySellConfig struct { DataFeedAURL string `valid:"-" toml:"DATA_FEED_A_URL"` DataTypeB string `valid:"-" toml:"DATA_TYPE_B"` DataFeedBURL string `valid:"-" toml:"DATA_FEED_B_URL"` - Levels []staticLevel `valid:"-" toml:"LEVELS"` + Levels []StaticLevel `valid:"-" toml:"LEVELS"` +} + +// MakeBuysellConfig factory method +func MakeBuysellConfig( + priceTolerance float64, + amountTolerance float64, + rateOffsetPercent float64, + rateOffset float64, + rateOffsetPercentFirst bool, + amountOfABase float64, + dataTypeA string, + dataFeedAURL string, + dataTypeB string, + dataFeedBURL string, + levels []StaticLevel, +) *BuySellConfig { + return &BuySellConfig{ + PriceTolerance: priceTolerance, + AmountTolerance: amountTolerance, + RateOffsetPercent: rateOffsetPercent, + RateOffset: rateOffset, + RateOffsetPercentFirst: rateOffsetPercentFirst, + AmountOfABase: amountOfABase, + DataTypeA: dataTypeA, + DataFeedAURL: dataFeedAURL, + DataTypeB: dataTypeB, + DataFeedBURL: dataFeedBURL, + Levels: levels, + } } // String impl. -func (c buySellConfig) String() string { +func (c BuySellConfig) String() string { return utils.StructString(c, nil) } @@ -36,7 +65,7 @@ func makeBuySellStrategy( ieif *IEIF, assetBase *horizon.Asset, assetQuote *horizon.Asset, - config *buySellConfig, + config *BuySellConfig, ) (api.Strategy, error) { offsetSell := rateOffset{ percent: config.RateOffsetPercent, diff --git a/plugins/factory.go b/plugins/factory.go index a8c66359c..d22b2994d 100644 --- a/plugins/factory.go +++ b/plugins/factory.go @@ -40,7 +40,7 @@ var strategies = map[string]StrategyContainer{ NeedsConfig: true, Complexity: "Beginner", makeFn: func(strategyFactoryData strategyFactoryData) (api.Strategy, error) { - var cfg buySellConfig + var cfg BuySellConfig err := config.Read(strategyFactoryData.stratConfigPath, &cfg) utils.CheckConfigError(cfg, err, strategyFactoryData.stratConfigPath) utils.LogConfig(cfg) diff --git a/plugins/sellStrategy.go b/plugins/sellStrategy.go index e815aa0f4..b97ce021f 100644 --- a/plugins/sellStrategy.go +++ b/plugins/sellStrategy.go @@ -22,7 +22,7 @@ type sellConfig struct { RateOffsetPercent float64 `valid:"-" toml:"RATE_OFFSET_PERCENT"` RateOffset float64 `valid:"-" toml:"RATE_OFFSET"` RateOffsetPercentFirst bool `valid:"-" toml:"RATE_OFFSET_PERCENT_FIRST"` - Levels []staticLevel `valid:"-" toml:"LEVELS"` + Levels []StaticLevel `valid:"-" toml:"LEVELS"` } // String impl. diff --git a/plugins/staticSpreadLevelProvider.go b/plugins/staticSpreadLevelProvider.go index d9602bc84..5082bf20f 100644 --- a/plugins/staticSpreadLevelProvider.go +++ b/plugins/staticSpreadLevelProvider.go @@ -7,9 +7,9 @@ import ( "github.com/stellar/kelp/model" ) -// staticLevel represents a layer in the orderbook defined statically +// StaticLevel represents a layer in the orderbook defined statically // extracted here because it's shared by strategy and sideStrategy where strategy depeneds on sideStrategy -type staticLevel struct { +type StaticLevel struct { SPREAD float64 `valid:"-"` AMOUNT float64 `valid:"-"` } @@ -34,7 +34,7 @@ type rateOffset struct { // staticSpreadLevelProvider provides a fixed number of levels using a static percentage spread type staticSpreadLevelProvider struct { - staticLevels []staticLevel + staticLevels []StaticLevel amountOfBase float64 offset rateOffset pf *api.FeedPair @@ -45,7 +45,7 @@ type staticSpreadLevelProvider struct { var _ api.LevelProvider = &staticSpreadLevelProvider{} // makeStaticSpreadLevelProvider is a factory method -func makeStaticSpreadLevelProvider(staticLevels []staticLevel, amountOfBase float64, offset rateOffset, pf *api.FeedPair, orderConstraints *model.OrderConstraints) api.LevelProvider { +func makeStaticSpreadLevelProvider(staticLevels []StaticLevel, amountOfBase float64, offset rateOffset, pf *api.FeedPair, orderConstraints *model.OrderConstraints) api.LevelProvider { return &staticSpreadLevelProvider{ staticLevels: staticLevels, amountOfBase: amountOfBase, From fdfe9d4971c5bafb69159a5bf084c05f1db70b11 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 16:17:44 -0700 Subject: [PATCH 09/14] 9 - /start endpoint --- cmd/server.go | 27 +++---------------------- gui/backend/api_server.go | 28 ++++++++++++++++--------- gui/backend/autogenerate_bot.go | 18 +++++++++++++---- gui/backend/routes.go | 1 + gui/backend/start_bot.go | 36 +++++++++++++++++++++++++++++++++ gui/backend/version.go | 4 ++-- gui/model/bot.go | 17 +++++++++++++--- support/utils/os.go | 30 +++++++++++++++++++++++++++ 8 files changed, 119 insertions(+), 42 deletions(-) create mode 100644 gui/backend/start_bot.go create mode 100644 support/utils/os.go diff --git a/cmd/server.go b/cmd/server.go index 0fd244346..9d2271f27 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,7 +1,6 @@ package cmd import ( - "bufio" "fmt" "log" "net/http" @@ -16,6 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/stellar/kelp/gui" "github.com/stellar/kelp/gui/backend" + "github.com/stellar/kelp/support/utils" ) var serverCmd = &cobra.Command{ @@ -112,7 +112,7 @@ func runWithYarn(options serverInputs) { os.Setenv("PORT", fmt.Sprintf("%d", *options.port)) log.Printf("Serving frontend via yarn on HTTP port: %d\n", *options.port) - e := runCommandStreamOutput(exec.Command("yarn", "--cwd", "gui/web", "start")) + e := utils.RunCommandStreamOutput(exec.Command("yarn", "--cwd", "gui/web", "start")) if e != nil { panic(e) } @@ -121,7 +121,7 @@ func runWithYarn(options serverInputs) { func generateStaticFiles() { log.Printf("generating contents of gui/web/build ...\n") - e := runCommandStreamOutput(exec.Command("yarn", "--cwd", "gui/web", "build")) + e := utils.RunCommandStreamOutput(exec.Command("yarn", "--cwd", "gui/web", "build")) if e != nil { panic(e) } @@ -129,24 +129,3 @@ func generateStaticFiles() { log.Printf("... finished generating contents of gui/web/build\n") log.Println() } - -func runCommandStreamOutput(command *exec.Cmd) error { - stdout, e := command.StdoutPipe() - if e != nil { - return fmt.Errorf("error while creating Stdout pipe: %s", e) - } - command.Start() - - scanner := bufio.NewScanner(stdout) - scanner.Split(bufio.ScanLines) - for scanner.Scan() { - line := scanner.Text() - log.Printf("\t%s\n", line) - } - - e = command.Wait() - if e != nil { - return fmt.Errorf("could not execute command: %s", e) - } - return nil -} diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index 3d34ee5cf..80cc9ec03 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -5,13 +5,16 @@ import ( "os" "os/exec" "path/filepath" + + "github.com/stellar/kelp/support/utils" ) // APIServer is an instance of the API service type APIServer struct { - dirPath string - binPath string - configsPath string + dirPath string + binPath string + configsDir string + logsDir string } // MakeAPIServer is a factory method @@ -22,12 +25,14 @@ func MakeAPIServer() (*APIServer, error) { } dirPath := filepath.Dir(binPath) - configsPath := dirPath + "/ops/configs" + configsDir := dirPath + "/ops/configs" + logsDir := dirPath + "/ops/logs" return &APIServer{ - dirPath: dirPath, - binPath: binPath, - configsPath: configsPath, + dirPath: dirPath, + binPath: binPath, + configsDir: configsDir, + logsDir: logsDir, }, nil } @@ -36,10 +41,15 @@ func (s *APIServer) runKelpCommand(cmd string) ([]byte, error) { return runBashCommand(cmdString) } +func (s *APIServer) runKelpCommandStreaming(cmd string) error { + cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) + return utils.RunCommandStreamOutput(exec.Command("bash", "-c", cmdString)) +} + func runBashCommand(cmd string) ([]byte, error) { - bytes, e := exec.Command("bash", "-c", cmd).Output() + resultBytes, e := exec.Command("bash", "-c", cmd).Output() if e != nil { return nil, fmt.Errorf("could not run bash command '%s': %s", cmd, e) } - return bytes, nil + return resultBytes, nil } diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index ca80cdfb8..05bd5f235 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -26,6 +26,7 @@ var centralizedMinQuoteVolumeOverride = float64(10.0) func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { kp, e := keypair.Random() if e != nil { + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("error generating keypair: %s\n", e))) return } @@ -33,15 +34,23 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { bot := model.MakeAutogeneratedBot() go setupAccount(kp.Address(), kp.Seed(), bot.Name) - _, e = runBashCommand("mkdir -p " + s.configsPath) + _, e = runBashCommand("mkdir -p " + s.configsDir) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error running mkdir command for configsDir: %s\n", e))) + return + } + + _, e = runBashCommand("mkdir -p " + s.logsDir) if e != nil { - w.Write([]byte(fmt.Sprintf("error running mkdir command: %s\n", e))) + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error running mkdir command for logsDir: %s\n", e))) return } filenamePair := bot.Filenames() sampleTrader := makeSampleTrader(kp.Seed()) - traderFilePath := fmt.Sprintf("%s/%s", s.configsPath, filenamePair.Trader) + traderFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Trader) log.Printf("writing autogenerated bot config to file: %s\n", traderFilePath) e = toml.WriteFile(traderFilePath, sampleTrader) if e != nil { @@ -51,7 +60,7 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { } sampleBuysell := makeSampleBuysell() - strategyFilePath := fmt.Sprintf("%s/%s", s.configsPath, filenamePair.Strategy) + strategyFilePath := fmt.Sprintf("%s/%s", s.configsDir, filenamePair.Strategy) log.Printf("writing autogenerated strategy config to file: %s\n", strategyFilePath) e = toml.WriteFile(strategyFilePath, sampleBuysell) if e != nil { @@ -76,6 +85,7 @@ func setupAccount(address string, signer string, botName string) { txSuccess, e := hclient.Fund(address) if e != nil { log.Printf("error funding address %s for bot '%s': %s\n", address, botName, e) + return } else { log.Printf("successfully funded account %s for bot '%s': %s\n", address, botName, txSuccess.TransactionSuccessToString()) } diff --git a/gui/backend/routes.go b/gui/backend/routes.go index db99f0675..9c6f1ddfa 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -11,5 +11,6 @@ func SetRoutes(r *chi.Mux, s *APIServer) { r.Route("/api/v1", func(r chi.Router) { r.Get("/version", http.HandlerFunc(s.version)) r.Get("/autogenerate", http.HandlerFunc(s.autogenerateBot)) + r.Post("/start", http.HandlerFunc(s.startBot)) }) } diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go new file mode 100644 index 000000000..cdbc68241 --- /dev/null +++ b/gui/backend/start_bot.go @@ -0,0 +1,36 @@ +package backend + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + + "github.com/stellar/kelp/gui/model" +) + +func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { + botNameBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error when reading request input: %s\n", e))) + return + } + botName := string(botNameBytes) + + strategy := "buysell" + filenamePair := model.GetBotFilenames(botName, strategy) + logPrefix := model.GetLogPrefix(botName, strategy) + command := fmt.Sprintf("trade -c %s/%s -s %s -f %s/%s -l %s/%s", s.configsDir, filenamePair.Trader, strategy, s.configsDir, filenamePair.Strategy, s.logsDir, logPrefix) + log.Printf("run command for bot '%s': %s\n", botName, command) + + go func(kelpCmdString string, name string) { + output, e := s.runKelpCommand(kelpCmdString) + if e != nil { + fmt.Printf("error when starting bot '%s': %s", name, e) + } + fmt.Printf("finished start bot command with result: %s\n", string(output)) + }(command, botName) + + w.WriteHeader(http.StatusOK) +} diff --git a/gui/backend/version.go b/gui/backend/version.go index 10fd5121a..e5219a738 100644 --- a/gui/backend/version.go +++ b/gui/backend/version.go @@ -6,10 +6,10 @@ import ( ) func (s *APIServer) version(w http.ResponseWriter, r *http.Request) { - bytes, e := s.runKelpCommand("version | grep version | cut -d':' -f3") + resultBytes, e := s.runKelpCommand("version | grep version | cut -d':' -f3") if e != nil { w.Write([]byte(fmt.Sprintf("error in version command: %s\n", e))) return } - w.Write(bytes) + w.Write(resultBytes) } diff --git a/gui/model/bot.go b/gui/model/bot.go index 3747fba72..d448623f4 100644 --- a/gui/model/bot.go +++ b/gui/model/bot.go @@ -33,11 +33,22 @@ type FilenamePair struct { Strategy string } -// Filename where we should save bot file +// Filenames where we should save bot config file func (b *Bot) Filenames() *FilenamePair { - converted := strings.ToLower(strings.ReplaceAll(b.Name, " ", "_")) + return GetBotFilenames(b.Name, b.Strategy) +} + +// GetBotFilenames from botName +func GetBotFilenames(botName string, strategy string) *FilenamePair { + converted := strings.ToLower(strings.ReplaceAll(botName, " ", "_")) return &FilenamePair{ Trader: fmt.Sprintf("%s__trader.%s", converted, "cfg"), - Strategy: fmt.Sprintf("%s__strategy_%s.%s", converted, b.Strategy, "cfg"), + Strategy: fmt.Sprintf("%s__strategy_%s.%s", converted, strategy, "cfg"), } } + +// GetLogPrefix from botName +func GetLogPrefix(botName string, strategy string) string { + converted := strings.ToLower(strings.ReplaceAll(botName, " ", "_")) + return fmt.Sprintf("%s__%s_", converted, strategy) +} diff --git a/support/utils/os.go b/support/utils/os.go new file mode 100644 index 000000000..1fad458c8 --- /dev/null +++ b/support/utils/os.go @@ -0,0 +1,30 @@ +package utils + +import ( + "bufio" + "fmt" + "log" + "os/exec" +) + +// RunCommandStreamOutput runs the provided command in a streaming fashion +func RunCommandStreamOutput(command *exec.Cmd) error { + stdout, e := command.StdoutPipe() + if e != nil { + return fmt.Errorf("error while creating Stdout pipe: %s", e) + } + command.Start() + + scanner := bufio.NewScanner(stdout) + scanner.Split(bufio.ScanLines) + for scanner.Scan() { + line := scanner.Text() + log.Printf("\t%s\n", line) + } + + e = command.Wait() + if e != nil { + return fmt.Errorf("could not execute command: %s", e) + } + return nil +} From d901f8d85f33a4517ec0f1bdae00c53f1359921a Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 16:33:13 -0700 Subject: [PATCH 10/14] 10 - hook up /start endpoint --- .../components/molecules/BotCard/BotCard.js | 35 ++++++++++++------- gui/web/src/components/screens/Bots/Bots.js | 1 + gui/web/src/kelp-ops-api/start.js | 8 +++++ 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 gui/web/src/kelp-ops-api/start.js diff --git a/gui/web/src/components/molecules/BotCard/BotCard.js b/gui/web/src/components/molecules/BotCard/BotCard.js index 70ec21e24..c19ee7c2f 100644 --- a/gui/web/src/components/molecules/BotCard/BotCard.js +++ b/gui/web/src/components/molecules/BotCard/BotCard.js @@ -11,6 +11,7 @@ import BotAssetsInfo from '../../atoms/BotAssetsInfo/BotAssetsInfo'; import BotBidAskInfo from '../../atoms/BotBidAskInfo/BotBidAskInfo'; import Button from '../../atoms/Button/Button'; +import start from '../../../kelp-ops-api/start'; class BotCard extends Component { constructor(props) { @@ -36,28 +37,38 @@ class BotCard extends Component { warnings: PropTypes.number, errors: PropTypes.number, showDetailsFn: PropTypes.func, + baseUrl: PropTypes.string, }; + componentWillUnmount() { + if (this._asyncRequest) { + this._asyncRequest.cancel(); + this._asyncRequest = null; + } + } + toggleBot() { if(this.state.isRunning){ this.stopBot(); - } - else { + } else { this.startBot(); } } startBot(){ - this.setState({ - timeStarted: new Date(), - isRunning: true, - }, () => { - this.tick(); - - this.timer = setInterval( - () => this.tick(), - 1000 - ); + var _this = this; + start(this.props.baseUrl, this.props.name).then(resp => { + _this.setState({ + timeStarted: new Date(), + isRunning: true, + }, () => { + _this.tick(); + + _this.timer = setInterval( + () => _this.tick(), + 1000 + ); + }); }); } diff --git a/gui/web/src/components/screens/Bots/Bots.js b/gui/web/src/components/screens/Bots/Bots.js index b6d0bd8a1..0770a1c94 100644 --- a/gui/web/src/components/screens/Bots/Bots.js +++ b/gui/web/src/components/screens/Bots/Bots.js @@ -105,6 +105,7 @@ class Bots extends Component { warnings={bot.warnings} errors={bot.errors} showDetailsFn={this.gotoDetails} + baseUrl={this.props.baseUrl} /> )); diff --git a/gui/web/src/kelp-ops-api/start.js b/gui/web/src/kelp-ops-api/start.js new file mode 100644 index 000000000..a47dd1a63 --- /dev/null +++ b/gui/web/src/kelp-ops-api/start.js @@ -0,0 +1,8 @@ +export default (baseUrl, botName) => { + return fetch(baseUrl + "/api/v1/start", { + method: "POST", + body: botName, + }).then(resp => { + return resp.text(); + }); +}; \ No newline at end of file From 3ca68635d01667c48527542537a1a5d8a5103cbc Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 17:06:56 -0700 Subject: [PATCH 11/14] 11 - /stop endopoint and hook up to frontend --- gui/backend/routes.go | 1 + gui/backend/start_bot.go | 23 +++++++--- gui/backend/stop_bot.go | 44 +++++++++++++++++++ .../components/molecules/BotCard/BotCard.js | 14 +++--- gui/web/src/kelp-ops-api/stop.js | 8 ++++ 5 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 gui/backend/stop_bot.go create mode 100644 gui/web/src/kelp-ops-api/stop.js diff --git a/gui/backend/routes.go b/gui/backend/routes.go index 9c6f1ddfa..c560094d3 100644 --- a/gui/backend/routes.go +++ b/gui/backend/routes.go @@ -12,5 +12,6 @@ func SetRoutes(r *chi.Mux, s *APIServer) { r.Get("/version", http.HandlerFunc(s.version)) r.Get("/autogenerate", http.HandlerFunc(s.autogenerateBot)) r.Post("/start", http.HandlerFunc(s.startBot)) + r.Post("/stop", http.HandlerFunc(s.stopBot)) }) } diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go index cdbc68241..74d256eb2 100644 --- a/gui/backend/start_bot.go +++ b/gui/backend/start_bot.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "log" "net/http" + "strings" "github.com/stellar/kelp/gui/model" ) @@ -17,20 +18,30 @@ func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { return } botName := string(botNameBytes) + s.doStartBot(botName, "buysell", nil) + w.WriteHeader(http.StatusOK) +} - strategy := "buysell" +func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint8) { filenamePair := model.GetBotFilenames(botName, strategy) logPrefix := model.GetLogPrefix(botName, strategy) command := fmt.Sprintf("trade -c %s/%s -s %s -f %s/%s -l %s/%s", s.configsDir, filenamePair.Trader, strategy, s.configsDir, filenamePair.Strategy, s.logsDir, logPrefix) + if iterations != nil { + command = fmt.Sprintf("%s --iter %d", command, *iterations) + } log.Printf("run command for bot '%s': %s\n", botName, command) go func(kelpCmdString string, name string) { - output, e := s.runKelpCommand(kelpCmdString) + // runKelpCommand is blocking + _, e := s.runKelpCommand(kelpCmdString) if e != nil { - fmt.Printf("error when starting bot '%s': %s", name, e) + if strings.Contains(e.Error(), "signal: terminated") { + fmt.Printf("terminated start bot command for bot '%s' with strategy '%s'\n", name, strategy) + return + } + fmt.Printf("error when starting bot '%s' with strategy '%s': %s\n", name, strategy, e) + return } - fmt.Printf("finished start bot command with result: %s\n", string(output)) + fmt.Printf("finished start bot command for bot '%s' with strategy '%s'\n", name, strategy) }(command, botName) - - w.WriteHeader(http.StatusOK) } diff --git a/gui/backend/stop_bot.go b/gui/backend/stop_bot.go new file mode 100644 index 000000000..fb2afbfda --- /dev/null +++ b/gui/backend/stop_bot.go @@ -0,0 +1,44 @@ +package backend + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/stellar/kelp/gui/model" +) + +func (s *APIServer) stopBot(w http.ResponseWriter, r *http.Request) { + botNameBytes, e := ioutil.ReadAll(r.Body) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error when reading request input: %s\n", e))) + return + } + botName := string(botNameBytes) + strategy := "buysell" + + filenamePair := model.GetBotFilenames(botName, strategy) + command := fmt.Sprintf("ps aux | grep %s | col | cut -d' ' -f2 | xargs kill", filenamePair.Trader) + log.Printf("stop command for bot '%s': %s\n", botName, command) + + go func(cmdString string, name string) { + // runKelpCommand is blocking + _, e := runBashCommand(cmdString) + if e != nil { + if strings.Contains(e.Error(), "signal: terminated") { + fmt.Printf("stopped bot '%s'\n", name) + + var numIterations uint8 = 1 + s.doStartBot(botName, "delete", &numIterations) + return + } + fmt.Printf("error when stopping bot '%s': %s\n", name, e) + return + } + }(command, botName) + + w.WriteHeader(http.StatusOK) +} diff --git a/gui/web/src/components/molecules/BotCard/BotCard.js b/gui/web/src/components/molecules/BotCard/BotCard.js index c19ee7c2f..457d3db82 100644 --- a/gui/web/src/components/molecules/BotCard/BotCard.js +++ b/gui/web/src/components/molecules/BotCard/BotCard.js @@ -12,6 +12,7 @@ import BotBidAskInfo from '../../atoms/BotBidAskInfo/BotBidAskInfo'; import Button from '../../atoms/Button/Button'; import start from '../../../kelp-ops-api/start'; +import stop from '../../../kelp-ops-api/stop'; class BotCard extends Component { constructor(props) { @@ -73,12 +74,15 @@ class BotCard extends Component { } stopBot() { - this.setState({ - timeStarted: null, - timeElapsed: null, - isRunning: false, + var _this = this; + stop(this.props.baseUrl, this.props.name).then(resp => { + _this.setState({ + timeStarted: null, + timeElapsed: null, + isRunning: false, + }); + clearTimeout(_this.timer); }); - clearTimeout(this.timer); } tick() { diff --git a/gui/web/src/kelp-ops-api/stop.js b/gui/web/src/kelp-ops-api/stop.js new file mode 100644 index 000000000..352909304 --- /dev/null +++ b/gui/web/src/kelp-ops-api/stop.js @@ -0,0 +1,8 @@ +export default (baseUrl, botName) => { + return fetch(baseUrl + "/api/v1/stop", { + method: "POST", + body: botName, + }).then(resp => { + return resp.text(); + }); +}; \ No newline at end of file From 4030d78178305211141bdcfcbed17f35b27f5ad7 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Thu, 9 May 2019 17:49:30 -0700 Subject: [PATCH 12/14] 12 - issue COUPON currency to autogenerated accounts --- gui/backend/autogenerate_bot.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index 05bd5f235..a539dcb6c 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -17,6 +17,7 @@ import ( ) const publicTestnetHorizon = "https://horizon-testnet.stellar.org" +const issuerSeed = "SANPCJHHXCPRN6IIZRBEQXS5M3L2LY7EYQLAVTYD56KL3V7ABO4I3ISZ" var centralizedPricePrecisionOverride = int8(6) var centralizedVolumePrecisionOverride = int8(1) @@ -96,12 +97,17 @@ func setupAccount(address string, signer string, botName string) { build.AutoSequence{SequenceProvider: client}, build.TestNetwork, build.Trust("COUPON", "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI"), + build.Payment( + build.Destination{AddressOrSeed: address}, + build.CreditAmount{Code: "COUPON", Issuer: "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI", Amount: "1000.0"}, + build.SourceAccount{AddressOrSeed: "GBMMZMK2DC4FFP4CAI6KCVNCQ7WLO5A7DQU7EC7WGHRDQBZB763X4OQI"}, + ), ) if e != nil { log.Printf("cannot create trustline transaction for account %s for bot '%s': %s\n", address, botName, e) return } - txnS, e := txn.Sign(signer) + txnS, e := txn.Sign(signer, issuerSeed) if e != nil { log.Printf("cannot sign trustline transaction for account %s for bot '%s': %s\n", address, botName, e) return From 37a91ad3e6c216c6f390be19a0de9ee93d77c7d7 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 20 May 2019 22:46:16 -0700 Subject: [PATCH 13/14] 13 - register commands with APIServer to easily stop bots --- gui/backend/api_server.go | 124 ++++++++++++++++++++++++++++---- gui/backend/autogenerate_bot.go | 4 +- gui/backend/start_bot.go | 41 +++++++---- gui/backend/stop_bot.go | 38 ++++------ gui/backend/version.go | 2 +- 5 files changed, 157 insertions(+), 52 deletions(-) diff --git a/gui/backend/api_server.go b/gui/backend/api_server.go index 80cc9ec03..0d0d5b0da 100644 --- a/gui/backend/api_server.go +++ b/gui/backend/api_server.go @@ -1,20 +1,27 @@ package backend import ( + "bufio" + "bytes" "fmt" + "io" + "log" "os" "os/exec" "path/filepath" + "sync" "github.com/stellar/kelp/support/utils" ) // APIServer is an instance of the API service type APIServer struct { - dirPath string - binPath string - configsDir string - logsDir string + dirPath string + binPath string + configsDir string + logsDir string + processes map[string]*exec.Cmd + processLock *sync.Mutex } // MakeAPIServer is a factory method @@ -29,16 +36,73 @@ func MakeAPIServer() (*APIServer, error) { logsDir := dirPath + "/ops/logs" return &APIServer{ - dirPath: dirPath, - binPath: binPath, - configsDir: configsDir, - logsDir: logsDir, + dirPath: dirPath, + binPath: binPath, + configsDir: configsDir, + logsDir: logsDir, + processes: map[string]*exec.Cmd{}, + processLock: &sync.Mutex{}, }, nil } -func (s *APIServer) runKelpCommand(cmd string) ([]byte, error) { +func (s *APIServer) registerCommand(namespace string, c *exec.Cmd) error { + s.processLock.Lock() + defer s.processLock.Unlock() + + if _, exists := s.processes[namespace]; exists { + return fmt.Errorf("process with namespace already exists: %s", namespace) + } + + s.processes[namespace] = c + log.Printf("registered command under namespace '%s' with PID: %d", namespace, c.Process.Pid) + return nil +} + +func (s *APIServer) unregisterCommand(namespace string) error { + s.processLock.Lock() + defer s.processLock.Unlock() + + if c, exists := s.processes[namespace]; exists { + delete(s.processes, namespace) + log.Printf("unregistered command under namespace '%s' with PID: %d", namespace, c.Process.Pid) + return nil + } + return fmt.Errorf("process with namespace does not exist: %s", namespace) +} + +func (s *APIServer) getCommand(namespace string) (*exec.Cmd, bool) { + s.processLock.Lock() + defer s.processLock.Unlock() + + c, exists := s.processes[namespace] + return c, exists +} + +func (s *APIServer) safeUnregisterCommand(namespace string) { + s.unregisterCommand(namespace) +} + +func (s *APIServer) stopCommand(namespace string) error { + if c, exists := s.getCommand(namespace); exists { + e := s.unregisterCommand(namespace) + if e != nil { + return fmt.Errorf("could not stop command because of an error when unregistering command for namespace '%s': %s", namespace, e) + } + + log.Printf("killing process %d\n", c.Process.Pid) + return c.Process.Kill() + } + return fmt.Errorf("process with namespace does not exist: %s", namespace) +} + +func (s *APIServer) runKelpCommandBlocking(namespace string, cmd string) ([]byte, error) { + cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) + return s.runBashCommandBlocking(namespace, cmdString) +} + +func (s *APIServer) runKelpCommandBackground(namespace string, cmd string) (*exec.Cmd, error) { cmdString := fmt.Sprintf("%s %s", s.binPath, cmd) - return runBashCommand(cmdString) + return s.runBashCommandBackground(namespace, cmdString, nil) } func (s *APIServer) runKelpCommandStreaming(cmd string) error { @@ -46,10 +110,42 @@ func (s *APIServer) runKelpCommandStreaming(cmd string) error { return utils.RunCommandStreamOutput(exec.Command("bash", "-c", cmdString)) } -func runBashCommand(cmd string) ([]byte, error) { - resultBytes, e := exec.Command("bash", "-c", cmd).Output() +func (s *APIServer) runBashCommandBlocking(namespace string, cmd string) ([]byte, error) { + var buf bytes.Buffer + writer := bufio.NewWriter(&buf) + c, e := s.runBashCommandBackground(namespace, cmd, writer) + if e != nil { + return nil, fmt.Errorf("could not run bash command in background '%s': %s", cmd, e) + } + + e = c.Wait() if e != nil { - return nil, fmt.Errorf("could not run bash command '%s': %s", cmd, e) + return nil, fmt.Errorf("error waiting for bash command '%s': %s", cmd, e) } - return resultBytes, nil + + e = s.unregisterCommand(namespace) + if e != nil { + return nil, fmt.Errorf("error unregistering bash command '%s': %s", cmd, e) + } + + return buf.Bytes(), nil +} + +func (s *APIServer) runBashCommandBackground(namespace string, cmd string, writer io.Writer) (*exec.Cmd, error) { + c := exec.Command("bash", "-c", cmd) + if writer != nil { + c.Stdout = writer + } + + e := c.Start() + if e != nil { + return c, fmt.Errorf("could not start bash command '%s': %s", cmd, e) + } + + e = s.registerCommand(namespace, c) + if e != nil { + return nil, fmt.Errorf("error registering bash command '%s': %s", cmd, e) + } + + return c, nil } diff --git a/gui/backend/autogenerate_bot.go b/gui/backend/autogenerate_bot.go index a539dcb6c..23d221e8d 100644 --- a/gui/backend/autogenerate_bot.go +++ b/gui/backend/autogenerate_bot.go @@ -35,14 +35,14 @@ func (s *APIServer) autogenerateBot(w http.ResponseWriter, r *http.Request) { bot := model.MakeAutogeneratedBot() go setupAccount(kp.Address(), kp.Seed(), bot.Name) - _, e = runBashCommand("mkdir -p " + s.configsDir) + _, e = s.runBashCommandBlocking("mkdir", "mkdir -p "+s.configsDir) if e != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("error running mkdir command for configsDir: %s\n", e))) return } - _, e = runBashCommand("mkdir -p " + s.logsDir) + _, e = s.runBashCommandBlocking("mkdir", "mkdir -p "+s.logsDir) if e != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("error running mkdir command for logsDir: %s\n", e))) diff --git a/gui/backend/start_bot.go b/gui/backend/start_bot.go index 74d256eb2..411a9f475 100644 --- a/gui/backend/start_bot.go +++ b/gui/backend/start_bot.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "log" "net/http" + "os/exec" "strings" "github.com/stellar/kelp/gui/model" @@ -17,12 +18,18 @@ func (s *APIServer) startBot(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("error when reading request input: %s\n", e))) return } + botName := string(botNameBytes) - s.doStartBot(botName, "buysell", nil) + e = s.doStartBot(botName, "buysell", nil) + if e != nil { + log.Printf("error starting bot: %s", e) + w.WriteHeader(http.StatusInternalServerError) + } + w.WriteHeader(http.StatusOK) } -func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint8) { +func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint8) error { filenamePair := model.GetBotFilenames(botName, strategy) logPrefix := model.GetLogPrefix(botName, strategy) command := fmt.Sprintf("trade -c %s/%s -s %s -f %s/%s -l %s/%s", s.configsDir, filenamePair.Trader, strategy, s.configsDir, filenamePair.Strategy, s.logsDir, logPrefix) @@ -31,17 +38,27 @@ func (s *APIServer) doStartBot(botName string, strategy string, iterations *uint } log.Printf("run command for bot '%s': %s\n", botName, command) - go func(kelpCmdString string, name string) { - // runKelpCommand is blocking - _, e := s.runKelpCommand(kelpCmdString) - if e != nil { - if strings.Contains(e.Error(), "signal: terminated") { - fmt.Printf("terminated start bot command for bot '%s' with strategy '%s'\n", name, strategy) + c, e := s.runKelpCommandBackground(botName, command) + if e != nil { + return fmt.Errorf("could not start bot %s: %s", botName, e) + } + + go func(kelpCommand *exec.Cmd, name string) { + defer s.safeUnregisterCommand(name) + + if kelpCommand != nil { + e := kelpCommand.Wait() + if e != nil { + if strings.Contains(e.Error(), "signal: terminated") { + log.Printf("terminated start bot command for bot '%s' with strategy '%s'\n", name, strategy) + return + } + log.Printf("error when starting bot '%s' with strategy '%s': %s\n", name, strategy, e) return } - fmt.Printf("error when starting bot '%s' with strategy '%s': %s\n", name, strategy, e) - return } - fmt.Printf("finished start bot command for bot '%s' with strategy '%s'\n", name, strategy) - }(command, botName) + log.Printf("finished start bot command for bot '%s' with strategy '%s'\n", name, strategy) + }(c, botName) + + return nil } diff --git a/gui/backend/stop_bot.go b/gui/backend/stop_bot.go index fb2afbfda..aebd0ceb8 100644 --- a/gui/backend/stop_bot.go +++ b/gui/backend/stop_bot.go @@ -5,9 +5,6 @@ import ( "io/ioutil" "log" "net/http" - "strings" - - "github.com/stellar/kelp/gui/model" ) func (s *APIServer) stopBot(w http.ResponseWriter, r *http.Request) { @@ -17,28 +14,23 @@ func (s *APIServer) stopBot(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("error when reading request input: %s\n", e))) return } - botName := string(botNameBytes) - strategy := "buysell" - - filenamePair := model.GetBotFilenames(botName, strategy) - command := fmt.Sprintf("ps aux | grep %s | col | cut -d' ' -f2 | xargs kill", filenamePair.Trader) - log.Printf("stop command for bot '%s': %s\n", botName, command) - go func(cmdString string, name string) { - // runKelpCommand is blocking - _, e := runBashCommand(cmdString) - if e != nil { - if strings.Contains(e.Error(), "signal: terminated") { - fmt.Printf("stopped bot '%s'\n", name) + botName := string(botNameBytes) + e = s.stopCommand(botName) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error when killing bot %s: %s\n", botName, e))) + return + } + log.Printf("stopped bot '%s'\n", botName) - var numIterations uint8 = 1 - s.doStartBot(botName, "delete", &numIterations) - return - } - fmt.Printf("error when stopping bot '%s': %s\n", name, e) - return - } - }(command, botName) + var numIterations uint8 = 1 + e = s.doStartBot(botName, "delete", &numIterations) + if e != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(fmt.Sprintf("error when deleting bot ortders %s: %s\n", botName, e))) + return + } w.WriteHeader(http.StatusOK) } diff --git a/gui/backend/version.go b/gui/backend/version.go index e5219a738..b2fae6914 100644 --- a/gui/backend/version.go +++ b/gui/backend/version.go @@ -6,7 +6,7 @@ import ( ) func (s *APIServer) version(w http.ResponseWriter, r *http.Request) { - resultBytes, e := s.runKelpCommand("version | grep version | cut -d':' -f3") + resultBytes, e := s.runKelpCommandBlocking("version", "version | grep version | cut -d':' -f3") if e != nil { w.Write([]byte(fmt.Sprintf("error in version command: %s\n", e))) return From ae5de574f2fb998c4dd25433d8d9f99cb0dcc1d5 Mon Sep 17 00:00:00 2001 From: Nikhil Saraf <1028334+nikhilsaraf@users.noreply.github.com> Date: Mon, 20 May 2019 23:20:25 -0700 Subject: [PATCH 14/14] 14 - maintain golang compatibility pre v1.12 --- gui/model/bot.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/model/bot.go b/gui/model/bot.go index d448623f4..bd074b9de 100644 --- a/gui/model/bot.go +++ b/gui/model/bot.go @@ -40,7 +40,7 @@ func (b *Bot) Filenames() *FilenamePair { // GetBotFilenames from botName func GetBotFilenames(botName string, strategy string) *FilenamePair { - converted := strings.ToLower(strings.ReplaceAll(botName, " ", "_")) + converted := strings.ToLower(strings.Replace(botName, " ", "_", -1)) return &FilenamePair{ Trader: fmt.Sprintf("%s__trader.%s", converted, "cfg"), Strategy: fmt.Sprintf("%s__strategy_%s.%s", converted, strategy, "cfg"), @@ -49,6 +49,6 @@ func GetBotFilenames(botName string, strategy string) *FilenamePair { // GetLogPrefix from botName func GetLogPrefix(botName string, strategy string) string { - converted := strings.ToLower(strings.ReplaceAll(botName, " ", "_")) + converted := strings.ToLower(strings.Replace(botName, " ", "_", -1)) return fmt.Sprintf("%s__%s_", converted, strategy) }