diff --git a/frontend/api/bbgo.js b/frontend/api/bbgo.js
index 486c0fde6..8ab737ecf 100644
--- a/frontend/api/bbgo.js
+++ b/frontend/api/bbgo.js
@@ -2,6 +2,16 @@ import axios from "axios";
const baseURL = process.env.NODE_ENV === "development" ? "http://localhost:8080" : ""
+export function testSessionConnection(data, cb) {
+ return axios.post(baseURL + '/api/sessions/test-connection', data, {
+ headers: {
+ 'Content-Type': 'application/json',
+ }
+ }).then(response => {
+ cb(response.data)
+ });
+}
+
export function querySessions(cb) {
axios.get(baseURL + '/api/sessions', {})
.then(response => {
@@ -10,14 +20,14 @@ export function querySessions(cb) {
}
export function queryTrades(params, cb) {
- axios.get(baseURL + '/api/trades', { params: params })
+ axios.get(baseURL + '/api/trades', {params: params})
.then(response => {
cb(response.data.trades)
});
}
export function queryClosedOrders(params, cb) {
- axios.get(baseURL + '/api/orders/closed', { params: params })
+ axios.get(baseURL + '/api/orders/closed', {params: params})
.then(response => {
cb(response.data.orders)
});
@@ -31,7 +41,7 @@ export function queryAssets(cb) {
}
export function queryTradingVolume(params, cb) {
- axios.get(baseURL + '/api/trading-volume', { params: params })
+ axios.get(baseURL + '/api/trading-volume', {params: params})
.then(response => {
cb(response.data.tradingVolumes)
});
diff --git a/frontend/components/ExchangeSessionForm.js b/frontend/components/ExchangeSessionForm.js
index fca9d2743..a3ec09606 100644
--- a/frontend/components/ExchangeSessionForm.js
+++ b/frontend/components/ExchangeSessionForm.js
@@ -1,8 +1,11 @@
import React from 'react';
import Grid from '@material-ui/core/Grid';
+import Box from '@material-ui/core/Box';
+import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import TextField from '@material-ui/core/TextField';
import FormControlLabel from '@material-ui/core/FormControlLabel';
+import FormHelperText from '@material-ui/core/FormHelperText';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
@@ -10,6 +13,10 @@ import Checkbox from '@material-ui/core/Checkbox';
import Select from '@material-ui/core/Select';
import MenuItem from '@material-ui/core/MenuItem';
+import Alert from '@material-ui/lab/Alert';
+
+import {testSessionConnection} from '../api/bbgo';
+
import {makeStyles} from '@material-ui/core/styles';
const useStyles = makeStyles((theme) => ({
@@ -18,28 +25,69 @@ const useStyles = makeStyles((theme) => ({
marginBottom: theme.spacing(1),
minWidth: 120,
},
+ buttons: {
+ display: 'flex',
+ justifyContent: 'flex-end',
+ marginTop: theme.spacing(2),
+ paddingTop: theme.spacing(2),
+ paddingBottom: theme.spacing(2),
+ '& > *': {
+ marginLeft: theme.spacing(1),
+ }
+ },
}));
export default function ExchangeSessionForm() {
-
const classes = useStyles();
const [exchangeType, setExchangeType] = React.useState('max');
+ const [customSessionName, setCustomSessionName] = React.useState(false);
+ const [sessionName, setSessionName] = React.useState(exchangeType);
+ const [testing, setTesting] = React.useState(false);
+ const [testResponse, setTestResponse] = React.useState(null);
+
+ const [apiKey, setApiKey] = React.useState('');
+ const [apiSecret, setApiSecret] = React.useState('');
const [isMargin, setIsMargin] = React.useState(false);
const [isIsolatedMargin, setIsIsolatedMargin] = React.useState(false);
const [isolatedMarginSymbol, setIsolatedMarginSymbol] = React.useState("");
+ const resetTestResponse = () => {
+ setTestResponse(null)
+ }
+
const handleExchangeTypeChange = (event) => {
setExchangeType(event.target.value);
+ setSessionName(event.target.value);
+ resetTestResponse()
};
- const handleIsMarginChange = (event) => {
- setIsMargin(event.target.checked);
- };
+ const createSessionConfig = () => {
+ return {
+ name: sessionName,
+ exchange: exchangeType,
+ key: apiKey,
+ secret: apiSecret,
+ margin: isMargin,
+ envVarPrefix: exchangeType.toUpperCase() + "_",
+ isolatedMargin: isIsolatedMargin,
+ isolatedMarginSymbol: isolatedMarginSymbol,
+ }
+ }
- const handleIsIsolatedMarginChange = (event) => {
- setIsIsolatedMargin(event.target.checked);
+ const handleTestConnection = (event) => {
+ const payload = createSessionConfig()
+ setTesting(true)
+ testSessionConnection(payload, (response) => {
+ console.log(response)
+ setTesting(false)
+ setTestResponse(response)
+ }).catch((reason) => {
+ console.error(reason)
+ setTesting(false)
+ setTestResponse(reason)
+ })
};
return (
@@ -64,71 +112,125 @@ export default function ExchangeSessionForm() {
-
+
{
+ setSessionName(event.target.value)
+ }}
+ value={sessionName}
/>
-
-
-
- {
+ setCustomSessionName(event.target.checked);
+ }} value="1"/>}
+ label="Custom exchange session name"
/>
+
+ By default, the session name will be the exchange type name,
+ e.g. binance
or max
.
+ If you're using multiple exchange sessions, you might need to custom the session name.
+ This is for advanced users.
+
-
-
+ {
+ setApiKey(event.target.value)
+ resetTestResponse()
+ }}
/>
- }
- label="Use margin trading. This is only available for Binance"
+ {
+ setApiSecret(event.target.value)
+ resetTestResponse()
+ }}
/>
-
- }
- label="Use isolated margin trading, if this is set, you can only trade one symbol with one session. This is only available for Binance"
- />
-
-
- {isIsolatedMargin ?
+ {exchangeType === "binance" ? (
- {
+ setIsMargin(event.target.checked);
+ resetTestResponse();
+ }} value="1"/>}
+ label="Use margin trading."
/>
+ This is only available for Binance. Please use the leverage at your own risk.
+
+ {
+ setIsIsolatedMargin(event.target.checked);
+ resetTestResponse()
+ }} value="1"/>}
+ label="Use isolated margin trading."
+ />
+ This is only available for Binance. If this is set, you can only trade one symbol with one session.
+
+ {isIsolatedMargin ?
+ {
+ setIsolatedMarginSymbol(event.target.value);
+ resetTestResponse()
+ }}
+ fullWidth
+ required
+ />
+ : null}
- : null}
-
-
+ ) : null}
+
+
+
+
+
+
+
+ {
+ testResponse ? testResponse.error ? (
+
+ {testResponse.error}
+
+ ) : testResponse.success ? (
+
+ Connection Test Succeeded
+
+ ) : null : null
+ }
+
+
);
}
diff --git a/frontend/package.json b/frontend/package.json
index 9acbf71d1..896384d81 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,7 @@
"@material-ui/core": "^4.11.2",
"@material-ui/data-grid": "^4.0.0-alpha.18",
"@material-ui/icons": "^4.11.2",
+ "@material-ui/lab": "^4.0.0-alpha.57",
"@nivo/bar": "^0.67.0",
"@nivo/core": "^0.67.0",
"@nivo/pie": "^0.67.0",
diff --git a/frontend/pages/setup/session.js b/frontend/pages/setup/index.js
similarity index 94%
rename from frontend/pages/setup/session.js
rename to frontend/pages/setup/index.js
index 6d1b9c62d..6015b30c9 100644
--- a/frontend/pages/setup/session.js
+++ b/frontend/pages/setup/index.js
@@ -20,7 +20,7 @@ const useStyles = makeStyles((theme) => ({
},
}));
-const steps = ['Add Exchange Session', 'Review Settings', 'Test Connection'];
+const steps = ['Add Exchange Session', 'Configure Strategy', 'Restart'];
function getStepContent(step) {
switch (step) {
@@ -35,7 +35,7 @@ function getStepContent(step) {
}
}
-export default function SetupSession() {
+export default function Setup() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index fb8257897..451828704 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -159,6 +159,17 @@
dependencies:
"@babel/runtime" "^7.4.4"
+"@material-ui/lab@^4.0.0-alpha.57":
+ version "4.0.0-alpha.57"
+ resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.57.tgz#e8961bcf6449e8a8dabe84f2700daacfcafbf83a"
+ integrity sha512-qo/IuIQOmEKtzmRD2E4Aa6DB4A87kmY6h0uYhjUmrrgmEAgbbw9etXpWPVXuRK6AGIQCjFzV6WO2i21m1R4FCw==
+ dependencies:
+ "@babel/runtime" "^7.4.4"
+ "@material-ui/utils" "^4.11.2"
+ clsx "^1.0.4"
+ prop-types "^15.7.2"
+ react-is "^16.8.0 || ^17.0.0"
+
"@material-ui/styles@^4.11.2":
version "4.11.2"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.2.tgz#e70558be3f41719e8c0d63c7a3c9ae163fdc84cb"
diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go
index cb6f703fb..57cac009f 100644
--- a/pkg/bbgo/config.go
+++ b/pkg/bbgo/config.go
@@ -53,10 +53,15 @@ type NotificationConfig struct {
}
type Session struct {
- ExchangeName string `json:"exchange" yaml:"exchange"`
- EnvVarPrefix string `json:"envVarPrefix" yaml:"envVarPrefix"`
+ Name string `json:"name,omitempty" yaml:"name,omitempty"`
+ ExchangeName string `json:"exchange" yaml:"exchange"`
+ EnvVarPrefix string `json:"envVarPrefix" yaml:"envVarPrefix"`
+
+ Key string `json:"key,omitempty" yaml:"key,omitempty"`
+ Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
+
PublicOnly bool `json:"publicOnly,omitempty" yaml:"publicOnly"`
- Margin bool `json:"margin,omitempty" yaml:"margin"`
+ Margin bool `json:"margin,omitempty" yaml:"margin,omitempty"`
IsolatedMargin bool `json:"isolatedMargin,omitempty" yaml:"isolatedMargin,omitempty"`
IsolatedMarginSymbol string `json:"isolatedMarginSymbol,omitempty" yaml:"isolatedMarginSymbol,omitempty"`
}
diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go
index 02ce5a4c0..c871e9574 100644
--- a/pkg/bbgo/environment.go
+++ b/pkg/bbgo/environment.go
@@ -142,36 +142,52 @@ func (environ *Environment) AddExchangesByViperKeys() error {
return nil
}
+func NewExchangeSessionFromConfig(name string, sessionConfig Session) (*ExchangeSession, error) {
+ exchangeName, err := types.ValidExchangeName(sessionConfig.ExchangeName)
+ if err != nil {
+ return nil, err
+ }
+
+ var exchange types.Exchange
+
+ if sessionConfig.Key != "" && sessionConfig.Secret != "" {
+ exchange, err = cmdutil.NewExchangeStandard(exchangeName, sessionConfig.Key, sessionConfig.Secret)
+ } else {
+ exchange, err = cmdutil.NewExchangeWithEnvVarPrefix(exchangeName, sessionConfig.EnvVarPrefix)
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ // configure exchange
+ if sessionConfig.Margin {
+ marginExchange, ok := exchange.(types.MarginExchange)
+ if !ok {
+ return nil, fmt.Errorf("exchange %s does not support margin", exchangeName)
+ }
+
+ if sessionConfig.IsolatedMargin {
+ marginExchange.UseIsolatedMargin(sessionConfig.IsolatedMarginSymbol)
+ } else {
+ marginExchange.UseMargin()
+ }
+ }
+
+ session := NewExchangeSession(name, exchange)
+ session.IsMargin = sessionConfig.Margin
+ session.IsIsolatedMargin = sessionConfig.IsolatedMargin
+ session.IsolatedMarginSymbol = sessionConfig.IsolatedMarginSymbol
+ return session, nil
+}
+
func (environ *Environment) AddExchangesFromSessionConfig(sessions map[string]Session) error {
for sessionName, sessionConfig := range sessions {
- exchangeName, err := types.ValidExchangeName(sessionConfig.ExchangeName)
+ session, err := NewExchangeSessionFromConfig(sessionName, sessionConfig)
if err != nil {
return err
}
- exchange, err := cmdutil.NewExchangeWithEnvVarPrefix(exchangeName, sessionConfig.EnvVarPrefix)
- if err != nil {
- return err
- }
-
- // configure exchange
- if sessionConfig.Margin {
- marginExchange, ok := exchange.(types.MarginExchange)
- if !ok {
- return fmt.Errorf("exchange %s does not support margin", exchangeName)
- }
-
- if sessionConfig.IsolatedMargin {
- marginExchange.UseIsolatedMargin(sessionConfig.IsolatedMarginSymbol)
- } else {
- marginExchange.UseMargin()
- }
- }
-
- session := NewExchangeSession(sessionName, exchange)
- session.IsMargin = sessionConfig.Margin
- session.IsIsolatedMargin = sessionConfig.IsolatedMargin
- session.IsolatedMarginSymbol = sessionConfig.IsolatedMarginSymbol
environ.AddExchangeSession(sessionName, session)
}
@@ -188,7 +204,6 @@ func (environ *Environment) Init(ctx context.Context) (err error) {
for n := range environ.sessions {
var session = environ.sessions[n]
-
if err := session.Init(ctx, environ); err != nil {
return err
}
diff --git a/pkg/bbgo/server.go b/pkg/bbgo/server.go
index 77efecc59..34430933c 100644
--- a/pkg/bbgo/server.go
+++ b/pkg/bbgo/server.go
@@ -16,12 +16,14 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
-func RunServer(ctx context.Context, userConfig *Config, environ *Environment) error {
+func RunServer(ctx context.Context, userConfig *Config, environ *Environment, trader *Trader) error {
r := gin.Default()
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"*"},
- AllowHeaders: []string{"Origin"},
+ AllowHeaders: []string{"Origin", "Content-Type"},
ExposeHeaders: []string{"Content-Length"},
+ AllowMethods: []string{"GET", "POST"},
+ AllowWebSockets: true,
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
@@ -134,6 +136,42 @@ func RunServer(ctx context.Context, userConfig *Config, environ *Environment) er
return
})
+ r.POST("/api/sessions/test-connection", func(c *gin.Context) {
+ var sessionConfig Session
+ if err := c.BindJSON(&sessionConfig); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ session, err := NewExchangeSessionFromConfig(sessionConfig.ExchangeName, sessionConfig)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "error": err.Error(),
+ })
+ return
+ }
+
+ var anyErr error
+ _, openOrdersErr := session.Exchange.QueryOpenOrders(ctx, "BTCUSDT")
+ if openOrdersErr != nil {
+ anyErr = openOrdersErr
+ }
+
+ _, balanceErr := session.Exchange.QueryAccountBalances(ctx)
+ if balanceErr != nil {
+ anyErr = balanceErr
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": anyErr == nil,
+ "error": anyErr,
+ "balance": balanceErr == nil,
+ "openOrders": openOrdersErr == nil,
+ })
+ })
+
r.GET("/api/sessions", func(c *gin.Context) {
var sessions []*ExchangeSession
for _, session := range environ.Sessions() {
diff --git a/pkg/cmd/cmdutil/exchange.go b/pkg/cmd/cmdutil/exchange.go
index cadb68ec3..21b23f4ae 100644
--- a/pkg/cmd/cmdutil/exchange.go
+++ b/pkg/cmd/cmdutil/exchange.go
@@ -11,29 +11,17 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
-func NewExchangeWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) {
- if len(varPrefix) == 0 {
- varPrefix = n.String()
+func NewExchangeStandard(n types.ExchangeName, key, secret string) (types.Exchange, error) {
+ if len(key) == 0 || len(secret) == 0 {
+ return nil, errors.New("binance: empty key or secret")
}
switch n {
case types.ExchangeBinance:
- key := viper.GetString(varPrefix + "-api-key")
- secret := viper.GetString(varPrefix + "-api-secret")
- if len(key) == 0 || len(secret) == 0 {
- return nil, errors.New("binance: empty key or secret")
- }
-
return binance.New(key, secret), nil
case types.ExchangeMax:
- key := viper.GetString(varPrefix + "-api-key")
- secret := viper.GetString(varPrefix + "-api-secret")
- if len(key) == 0 || len(secret) == 0 {
- return nil, errors.New("max: empty key or secret")
- }
-
return max.New(key, secret), nil
default:
@@ -42,6 +30,20 @@ func NewExchangeWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.
}
}
+func NewExchangeWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange, error) {
+ if len(varPrefix) == 0 {
+ varPrefix = n.String()
+ }
+
+ key := viper.GetString(varPrefix + "-api-key")
+ secret := viper.GetString(varPrefix + "-api-secret")
+ if len(key) == 0 || len(secret) == 0 {
+ return nil, errors.New("max: empty key or secret")
+ }
+
+ return NewExchangeStandard(n, key, secret)
+}
+
// NewExchange constructor exchange object from viper config.
func NewExchange(n types.ExchangeName) (types.Exchange, error) {
return NewExchangeWithEnvVarPrefix(n, "")
diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go
index 0a4f30150..021d0dcf3 100644
--- a/pkg/cmd/run.go
+++ b/pkg/cmd/run.go
@@ -55,6 +55,33 @@ var RunCmd = &cobra.Command{
RunE: run,
}
+func runSetup(basectx context.Context, userConfig *bbgo.Config, enableApiServer bool) error {
+ ctx, cancelTrading := context.WithCancel(basectx)
+ defer cancelTrading()
+
+ environ := bbgo.NewEnvironment()
+
+ trader := bbgo.NewTrader(environ)
+
+ if enableApiServer {
+ go func() {
+ if err := bbgo.RunServer(ctx, userConfig, environ, trader); err != nil {
+ log.WithError(err).Errorf("server error")
+ }
+ }()
+ }
+
+ cmdutil.WaitForSignal(ctx, syscall.SIGINT, syscall.SIGTERM)
+ cancelTrading()
+
+ shutdownCtx, cancelShutdown := context.WithDeadline(ctx, time.Now().Add(30*time.Second))
+
+ log.Infof("shutting down...")
+ trader.Graceful.Shutdown(shutdownCtx)
+ cancelShutdown()
+ return nil
+}
+
func runConfig(basectx context.Context, userConfig *bbgo.Config, enableApiServer bool) error {
ctx, cancelTrading := context.WithCancel(basectx)
defer cancelTrading()
@@ -227,7 +254,7 @@ func runConfig(basectx context.Context, userConfig *bbgo.Config, enableApiServer
if enableApiServer {
go func() {
- if err := bbgo.RunServer(ctx, userConfig, environ); err != nil {
+ if err := bbgo.RunServer(ctx, userConfig, environ, trader); err != nil {
log.WithError(err).Errorf("server error")
}
}()
@@ -262,24 +289,7 @@ func run(cmd *cobra.Command, args []string) error {
}
}
- configFile, err := cmd.Flags().GetString("config")
- if err != nil {
- return err
- }
-
- if len(configFile) == 0 {
- return errors.New("--config option is required")
- }
-
- noCompile, err := cmd.Flags().GetBool("no-compile")
- if err != nil {
- return err
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- defer cancel()
-
- userConfig, err := bbgo.Load(configFile, false)
+ setup, err := cmd.Flags().GetBool("setup")
if err != nil {
return err
}
@@ -289,17 +299,56 @@ func run(cmd *cobra.Command, args []string) error {
return err
}
- // for wrapper binary, we can just run the strategies
- if bbgo.IsWrapperBinary || (userConfig.Build != nil && len(userConfig.Build.Imports) == 0) || noCompile {
- userConfig, err = bbgo.Load(configFile, true)
+ noCompile, err := cmd.Flags().GetBool("no-compile")
+ if err != nil {
+ return err
+ }
+
+ configFile, err := cmd.Flags().GetString("config")
+ if err != nil {
+ return err
+ }
+
+ var userConfig *bbgo.Config
+
+ if setup {
+ log.Infof("running in setup mode, skip reading config file")
+ enableApiServer = true
+ userConfig = &bbgo.Config{
+ Notifications: nil,
+ Persistence: nil,
+ Sessions: nil,
+ ExchangeStrategies: nil,
+ }
+ } else {
+ if len(configFile) == 0 {
+ return errors.New("--config option is required")
+ }
+
+ userConfig, err = bbgo.Load(configFile, false)
if err != nil {
return err
}
+ }
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // for wrapper binary, we can just run the strategies
+ if bbgo.IsWrapperBinary || (userConfig.Build != nil && len(userConfig.Build.Imports) == 0) || noCompile {
if bbgo.IsWrapperBinary {
log.Infof("running wrapper binary...")
}
+ if setup {
+ return runSetup(ctx, userConfig, enableApiServer)
+ }
+
+ userConfig, err = bbgo.Load(configFile, true)
+ if err != nil {
+ return err
+ }
+
return runConfig(ctx, userConfig, enableApiServer)
}