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)
}