support exchange session test from the setup wizard

This commit is contained in:
c9s 2021-02-02 11:44:07 +08:00
parent ee3c76d3fb
commit 73762d9888
10 changed files with 354 additions and 121 deletions

View File

@ -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 => {

View File

@ -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() {
</FormControl>
</Grid>
<Grid item xs={12} sm={12}>
<Grid item xs={12} sm={6}>
<TextField
required
id="name"
name="name"
label="Session Name"
fullWidth
autoComplete="given-name"
required
disabled={!customSessionName}
onChange={(event) => {
setSessionName(event.target.value)
}}
value={sessionName}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField id="state" name="state" label="State/Province/Region" fullWidth/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
id="zip"
name="zip"
label="Zip / Postal code"
fullWidth
autoComplete="shipping postal-code"
<FormControlLabel
control={<Checkbox color="secondary" name="custom_session_name"
onChange={(event) => {
setCustomSessionName(event.target.checked);
}} value="1"/>}
label="Custom exchange session name"
/>
<FormHelperText id="session-name-helper-text">
By default, the session name will be the exchange type name,
e.g. <code>binance</code> or <code>max</code>.<br/>
If you're using multiple exchange sessions, you might need to custom the session name. <br/>
This is for advanced users.
</FormHelperText>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
required
id="country"
name="country"
label="Country"
<Grid item xs={12}>
<TextField id="key" name="api_key" label="API Key"
fullWidth
autoComplete="shipping country"
required
onChange={(event) => {
setApiKey(event.target.value)
resetTestResponse()
}}
/>
</Grid>
<Grid item xs={12}>
<TextField id="secret" name="api_secret" label="API Secret"
fullWidth
required
onChange={(event) => {
setApiSecret(event.target.value)
resetTestResponse()
}}
/>
</Grid>
{exchangeType === "binance" ? (
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox color="secondary" name="isMargin" onChange={handleIsMarginChange}
value="1"/>}
label="Use margin trading. This is only available for Binance"
control={<Checkbox color="secondary" name="isMargin" onChange={(event) => {
setIsMargin(event.target.checked);
resetTestResponse();
}} value="1"/>}
label="Use margin trading."
/>
</Grid>
<FormHelperText id="isMargin-helper-text">This is only available for Binance. Please use the leverage at your own risk.</FormHelperText>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox color="secondary" name="isIsolatedMargin"
onChange={handleIsIsolatedMarginChange} value="1"/>}
label="Use isolated margin trading, if this is set, you can only trade one symbol with one session. This is only available for Binance"
onChange={(event) => {
setIsIsolatedMargin(event.target.checked);
resetTestResponse()
}} value="1"/>}
label="Use isolated margin trading."
/>
</Grid>
<FormHelperText id="isIsolatedMargin-helper-text">This is only available for Binance. If this is set, you can only trade one symbol with one session.</FormHelperText>
{isIsolatedMargin ?
<Grid item xs={12}>
<TextField
required
id="isolatedMarginSymbol"
name="isolatedMarginSymbol"
label="Isolated Margin Symbol"
onChange={(event) => {
setIsolatedMarginSymbol(event.target.value);
resetTestResponse()
}}
fullWidth
required
/>
</Grid>
: null}
</Grid>
) : null}
</Grid>
<div className={classes.buttons}>
<Button
color="primary"
onClick={handleTestConnection}
disabled={testing}>
{ testing ? "Testing" : "Test Connection"}
</Button>
<Button
variant="contained"
color="primary">
Create
</Button>
</div>
{
testResponse ? testResponse.error ? (
<Box m={2}>
<Alert severity="error">{testResponse.error}</Alert>
</Box>
) : testResponse.success ? (
<Box m={2}>
<Alert severity="success">Connection Test Succeeded</Alert>
</Box>
) : null : null
}
</React.Fragment>
);
}

View File

@ -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",

View File

@ -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);

View File

@ -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"

View File

@ -53,10 +53,15 @@ type NotificationConfig struct {
}
type Session struct {
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"`
}

View File

@ -142,23 +142,29 @@ func (environ *Environment) AddExchangesByViperKeys() error {
return nil
}
func (environ *Environment) AddExchangesFromSessionConfig(sessions map[string]Session) error {
for sessionName, sessionConfig := range sessions {
func NewExchangeSessionFromConfig(name string, sessionConfig Session) (*ExchangeSession, error) {
exchangeName, err := types.ValidExchangeName(sessionConfig.ExchangeName)
if err != nil {
return err
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)
}
exchange, err := cmdutil.NewExchangeWithEnvVarPrefix(exchangeName, sessionConfig.EnvVarPrefix)
if err != nil {
return err
return nil, err
}
// configure exchange
if sessionConfig.Margin {
marginExchange, ok := exchange.(types.MarginExchange)
if !ok {
return fmt.Errorf("exchange %s does not support margin", exchangeName)
return nil, fmt.Errorf("exchange %s does not support margin", exchangeName)
}
if sessionConfig.IsolatedMargin {
@ -168,10 +174,20 @@ func (environ *Environment) AddExchangesFromSessionConfig(sessions map[string]Se
}
}
session := NewExchangeSession(sessionName, exchange)
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 {
session, err := NewExchangeSessionFromConfig(sessionName, sessionConfig)
if err != nil {
return err
}
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
}

View File

@ -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() {

View File

@ -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, "")

View File

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