mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
integrate strateg adding api
This commit is contained in:
parent
f94a45de40
commit
8aa96c4546
|
@ -20,6 +20,12 @@ export function addSession(session, cb) {
|
|||
});
|
||||
}
|
||||
|
||||
export function attachStrategyOn(session, strategyID, strategy, cb) {
|
||||
return axios.post(baseURL + `/api/setup/strategy/single/${strategyID}/session/${session}`, strategy).then(response => {
|
||||
cb(response.data)
|
||||
});
|
||||
}
|
||||
|
||||
export function testSessionConnection(session, cb) {
|
||||
return axios.post(baseURL + '/api/sessions/test', session).then(response => {
|
||||
cb(response.data)
|
||||
|
|
|
@ -6,15 +6,16 @@ import Button from '@material-ui/core/Button';
|
|||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import {querySessions, querySessionSymbols} from "../api/bbgo";
|
||||
import {attachStrategyOn, querySessions, querySessionSymbols} from "../api/bbgo";
|
||||
|
||||
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';
|
||||
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
import FormLabel from '@material-ui/core/FormLabel';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
|
@ -23,8 +24,54 @@ import Box from "@material-ui/core/Box";
|
|||
|
||||
import NumberFormat from 'react-number-format';
|
||||
|
||||
function NumberFormatCustom(props) {
|
||||
const { inputRef, onChange, ...other } = props;
|
||||
function parseFloatValid(s) {
|
||||
if (s) {
|
||||
const f = parseFloat(s)
|
||||
if (!isNaN(f)) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function parseFloatCall(s, cb) {
|
||||
if (s) {
|
||||
const f = parseFloat(s)
|
||||
if (!isNaN(f)) {
|
||||
cb(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function StandardNumberFormat(props) {
|
||||
const {inputRef, onChange, ...other} = props;
|
||||
return (
|
||||
<NumberFormat
|
||||
{...other}
|
||||
getInputRef={inputRef}
|
||||
onValueChange={(values) => {
|
||||
onChange({
|
||||
target: {
|
||||
name: props.name,
|
||||
value: values.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
thousandSeparator
|
||||
isNumericString
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
StandardNumberFormat.propTypes = {
|
||||
inputRef: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
function PriceNumberFormat(props) {
|
||||
const {inputRef, onChange, ...other} = props;
|
||||
|
||||
return (
|
||||
<NumberFormat
|
||||
|
@ -45,7 +92,7 @@ function NumberFormatCustom(props) {
|
|||
);
|
||||
}
|
||||
|
||||
NumberFormatCustom.propTypes = {
|
||||
PriceNumberFormat.propTypes = {
|
||||
inputRef: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
@ -70,7 +117,7 @@ const useStyles = makeStyles((theme) => ({
|
|||
}));
|
||||
|
||||
|
||||
export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
||||
export default function ConfigureGridStrategyForm({onBack, onAdded}) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [sessions, setSessions] = React.useState([]);
|
||||
|
@ -81,9 +128,16 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
|
||||
const [selectedSymbol, setSelectedSymbol] = React.useState('');
|
||||
|
||||
const [quantityBy, setQuantityBy] = React.useState('fixedAmount');
|
||||
|
||||
const [upperPrice, setUpperPrice] = React.useState(30000.0);
|
||||
const [lowerPrice, setLowerPrice] = React.useState(10000.0);
|
||||
|
||||
const [fixedAmount, setFixedAmount] = React.useState(100.0);
|
||||
const [fixedQuantity, setFixedQuantity] = React.useState(1.234);
|
||||
const [gridNumber, setGridNumber] = React.useState(20);
|
||||
const [profitSpread, setProfitSpread] = React.useState(100.0);
|
||||
|
||||
const [response, setResponse] = React.useState({});
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -93,7 +147,39 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
}, [])
|
||||
|
||||
const handleAdd = (event) => {
|
||||
const payload = {
|
||||
symbol: selectedSymbol,
|
||||
gridNumber: parseFloatValid(gridNumber),
|
||||
profitSpread: parseFloatValid(profitSpread),
|
||||
upperPrice: parseFloatValid(upperPrice),
|
||||
lowerPrice: parseFloatValid(lowerPrice),
|
||||
}
|
||||
switch (quantityBy) {
|
||||
case "fixedQuantity":
|
||||
payload.quantity = parseFloatValid(fixedQuantity);
|
||||
break;
|
||||
|
||||
case "fixedAmount":
|
||||
payload.orderAmount = parseFloatValid(fixedAmount);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
console.log(payload)
|
||||
attachStrategyOn(selectedSessionName, "grid", payload, (response) => {
|
||||
console.log(response)
|
||||
setResponse(response)
|
||||
if (onAdded) {
|
||||
setTimeout(onAdded, 3000)
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
setResponse(err.response.data)
|
||||
})
|
||||
};
|
||||
|
||||
const handleQuantityBy = (event) => {
|
||||
setQuantityBy(event.target.value);
|
||||
};
|
||||
|
||||
const handleSessionChange = (event) => {
|
||||
|
@ -143,6 +229,7 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
id="session-select"
|
||||
value={selectedSessionName ? selectedSessionName : ''}
|
||||
onChange={handleSessionChange}
|
||||
required
|
||||
>
|
||||
{sessionMenuItems}
|
||||
</Select>
|
||||
|
@ -156,6 +243,7 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
<FormControl className={classes.formControl}>
|
||||
<InputLabel id="symbol-select-label">Market</InputLabel>
|
||||
<Select
|
||||
required
|
||||
labelId="symbol-select-label"
|
||||
id="symbol-select"
|
||||
value={selectedSymbol ? selectedSymbol : ''}
|
||||
|
@ -178,16 +266,11 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
if (event.target.value) {
|
||||
const v = parseFloat(event.target.value)
|
||||
if (!isNaN(v)) {
|
||||
setUpperPrice(v);
|
||||
}
|
||||
}
|
||||
parseFloatCall(event.target.value, setUpperPrice)
|
||||
}}
|
||||
value={upperPrice}
|
||||
InputProps={{
|
||||
inputComponent: NumberFormatCustom,
|
||||
inputComponent: PriceNumberFormat,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
@ -200,51 +283,93 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
if (event.target.value) {
|
||||
const v = parseFloat(event.target.value)
|
||||
if (!isNaN(v)) {
|
||||
setLowerPrice(v);
|
||||
}
|
||||
}
|
||||
parseFloatCall(event.target.value, setLowerPrice)
|
||||
}}
|
||||
value={lowerPrice}
|
||||
InputProps={{
|
||||
inputComponent: NumberFormatCustom,
|
||||
inputComponent: PriceNumberFormat,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControlLabel
|
||||
control={<Checkbox color="secondary" name="custom_session_name"
|
||||
onChange={(event) => {
|
||||
|
||||
}} 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}>
|
||||
<TextField id="key" name="api_key" label="API Key"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
}}
|
||||
<TextField
|
||||
id="profitSpread"
|
||||
name="profit_spread"
|
||||
label="Profit Spread"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
parseFloatCall(event.target.value, setProfitSpread)
|
||||
}}
|
||||
value={profitSpread}
|
||||
InputProps={{
|
||||
inputComponent: StandardNumberFormat,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
|
||||
<Grid item xs={12} sm={3}>
|
||||
<FormControl component="fieldset">
|
||||
<FormLabel component="legend">Order Quantity By</FormLabel>
|
||||
<RadioGroup name="quantityBy" value={quantityBy} onChange={handleQuantityBy}>
|
||||
<FormControlLabel value="fixedAmount" control={<Radio/>} label="Fixed Amount"/>
|
||||
<FormControlLabel value="fixedQuantity" control={<Radio/>} label="Fixed Quantity"/>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} sm={9}>
|
||||
{quantityBy === "fixedQuantity" ? (
|
||||
<TextField
|
||||
id="fixedQuantity"
|
||||
name="order_quantity"
|
||||
label="Fixed Quantity"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
parseFloatCall(event.target.value, setFixedQuantity)
|
||||
}}
|
||||
value={fixedQuantity}
|
||||
InputProps={{
|
||||
inputComponent: StandardNumberFormat,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{quantityBy === "fixedAmount" ? (
|
||||
<TextField
|
||||
id="orderAmount"
|
||||
name="order_amount"
|
||||
label="Fixed Amount"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
parseFloatCall(event.target.value, setFixedAmount)
|
||||
}}
|
||||
value={fixedAmount}
|
||||
InputProps={{
|
||||
inputComponent: PriceNumberFormat,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<TextField id="secret" name="api_secret" label="API Secret"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
}}
|
||||
<TextField
|
||||
id="gridNumber"
|
||||
name="grid_number"
|
||||
label="Number of Grid"
|
||||
fullWidth
|
||||
required
|
||||
onChange={(event) => {
|
||||
parseFloatCall(event.target.value, setGridNumber)
|
||||
}}
|
||||
value={gridNumber}
|
||||
InputProps={{
|
||||
inputComponent: StandardNumberFormat,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
@ -264,7 +389,7 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
color="primary"
|
||||
onClick={handleAdd}
|
||||
>
|
||||
Add
|
||||
Add Strategy
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
@ -275,7 +400,7 @@ export default function ConfigureGridStrategyForm({onBack, onConfigured}) {
|
|||
</Box>
|
||||
) : response.success ? (
|
||||
<Box m={2}>
|
||||
<Alert severity="success">Exchange Session Added</Alert>
|
||||
<Alert severity="success">Strategy Added</Alert>
|
||||
</Box>
|
||||
) : null : null
|
||||
}
|
||||
|
|
|
@ -43,9 +43,12 @@ function getStepContent(step, setActiveStep) {
|
|||
}}/>
|
||||
case 3:
|
||||
return (
|
||||
<ConfigureGridStrategyForm />
|
||||
);
|
||||
<ConfigureGridStrategyForm onBack={() => {
|
||||
setActiveStep(2)
|
||||
}} onAdded={() => {
|
||||
|
||||
}}/>
|
||||
);
|
||||
case 4:
|
||||
return;
|
||||
default:
|
||||
|
|
|
@ -22,9 +22,9 @@ type PnLReporterConfig struct {
|
|||
When datatype.StringSlice `json:"when" yaml:"when"`
|
||||
}
|
||||
|
||||
// ExchangeStrategyMount wraps the SingleExchangeStrategy with the Session name for mounting
|
||||
// ExchangeStrategyMount wraps the SingleExchangeStrategy with the ExchangeSession name for mounting
|
||||
type ExchangeStrategyMount struct {
|
||||
// Mounts contains the Session name to mount
|
||||
// Mounts contains the ExchangeSession name to mount
|
||||
Mounts []string
|
||||
|
||||
// Strategy is the strategy we loaded from config
|
||||
|
@ -57,8 +57,8 @@ type Session struct {
|
|||
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"`
|
||||
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,omitempty"`
|
||||
|
@ -152,11 +152,11 @@ func GetNativeBuildTargetConfig() BuildTargetConfig {
|
|||
}
|
||||
|
||||
type Config struct {
|
||||
Build *BuildConfig `json:"build" yaml:"build"`
|
||||
Build *BuildConfig `json:"build,omitempty" yaml:"build,omitempty"`
|
||||
|
||||
// Imports is deprecated
|
||||
// Deprecated: use BuildConfig instead
|
||||
Imports []string `json:"imports" yaml:"imports"`
|
||||
Imports []string `json:"imports,omitempty" yaml:"imports,omitempty"`
|
||||
|
||||
Backtest *Backtest `json:"backtest,omitempty" yaml:"backtest,omitempty"`
|
||||
|
||||
|
@ -168,8 +168,8 @@ type Config struct {
|
|||
|
||||
RiskControls *RiskControls `json:"riskControls,omitempty" yaml:"riskControls,omitempty"`
|
||||
|
||||
ExchangeStrategies []ExchangeStrategyMount
|
||||
CrossExchangeStrategies []CrossExchangeStrategy
|
||||
ExchangeStrategies []ExchangeStrategyMount `json:"exchangeStrategies,omitempty" yaml:"exchangeStrategies,omitempty"`
|
||||
CrossExchangeStrategies []CrossExchangeStrategy `json:"crossExchangeStrategies,omitempty" yaml:"crossExchangeStrategies,omitempty"`
|
||||
|
||||
PnLReporters []PnLReporterConfig `json:"reportPnL,omitempty" yaml:"reportPnL,omitempty"`
|
||||
}
|
||||
|
@ -294,6 +294,18 @@ func loadCrossExchangeStrategies(config *Config, stash Stash) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
func NewStrategyFromMap(id string, conf interface{}) (SingleExchangeStrategy, error) {
|
||||
if st, ok := LoadedExchangeStrategies[id]; ok {
|
||||
val, err := reUnmarshal(conf, st)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return val.(SingleExchangeStrategy), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("strategy %s not found", id)
|
||||
}
|
||||
|
||||
func loadExchangeStrategies(config *Config, stash Stash) (err error) {
|
||||
exchangeStrategiesConf, ok := stash["exchangeStrategies"]
|
||||
if !ok {
|
||||
|
@ -325,16 +337,17 @@ func loadExchangeStrategies(config *Config, stash Stash) (err error) {
|
|||
}
|
||||
|
||||
for id, conf := range configStash {
|
||||
|
||||
// look up the real struct type
|
||||
if st, ok := LoadedExchangeStrategies[id]; ok {
|
||||
val, err := reUnmarshal(conf, st)
|
||||
if _, ok := LoadedExchangeStrategies[id]; ok {
|
||||
st, err := NewStrategyFromMap(id, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config.ExchangeStrategies = append(config.ExchangeStrategies, ExchangeStrategyMount{
|
||||
Mounts: mounts,
|
||||
Strategy: val.(SingleExchangeStrategy),
|
||||
Strategy: st,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ func NewEnvironment() *Environment {
|
|||
// default trade scan time
|
||||
tradeScanTime: time.Now().AddDate(0, 0, -7), // sync from 7 days ago
|
||||
sessions: make(map[string]*ExchangeSession),
|
||||
startTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -171,6 +172,9 @@ func NewExchangeSessionFromConfig(name string, sessionConfig *ExchangeSession) (
|
|||
}
|
||||
|
||||
session := NewExchangeSession(name, exchange)
|
||||
session.ExchangeName = sessionConfig.ExchangeName
|
||||
session.EnvVarPrefix = sessionConfig.EnvVarPrefix
|
||||
session.PublicOnly = sessionConfig.PublicOnly
|
||||
session.Margin = sessionConfig.Margin
|
||||
session.IsolatedMargin = sessionConfig.IsolatedMargin
|
||||
session.IsolatedMarginSymbol = sessionConfig.IsolatedMarginSymbol
|
||||
|
@ -192,23 +196,19 @@ func (environ *Environment) AddExchangesFromSessionConfig(sessions map[string]*E
|
|||
|
||||
// Init prepares the data that will be used by the strategies
|
||||
func (environ *Environment) Init(ctx context.Context) (err error) {
|
||||
// feed klines into the market data store
|
||||
if environ.startTime == emptyTime {
|
||||
environ.startTime = time.Now()
|
||||
}
|
||||
|
||||
for n := range environ.sessions {
|
||||
var session = environ.sessions[n]
|
||||
|
||||
if err := session.Init(ctx, environ); err != nil {
|
||||
return err
|
||||
// we can skip initialized sessions
|
||||
if err != ErrSessionAlreadyInitialized {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.InitSymbols(ctx, environ); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.IsInitialized = true
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
6
pkg/bbgo/errors.go
Normal file
6
pkg/bbgo/errors.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package bbgo
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrSessionAlreadyInitialized = errors.New("session is already initialized")
|
||||
|
|
@ -1,428 +1,2 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/markbates/pkger"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/strategy/grid"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func RunServer(ctx context.Context, userConfig *Config, environ *Environment, trader *Trader, setup bool) error {
|
||||
r := gin.Default()
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowWebSockets: true,
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
|
||||
r.GET("/api/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
if setup {
|
||||
r.POST("/api/setup/test-db", func(c *gin.Context) {
|
||||
payload := struct {
|
||||
DSN string `json:"dsn"`
|
||||
}{}
|
||||
|
||||
if err := c.BindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
dsn := payload.DSN
|
||||
if len(dsn) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing dsn argument"})
|
||||
return
|
||||
}
|
||||
|
||||
db, err := ConnectMySQL(dsn)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Close() ; err != nil {
|
||||
log.WithError(err).Error("db connection close error")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
r.POST("/api/setup/configure-db", func(c *gin.Context) {
|
||||
payload := struct {
|
||||
DSN string `json:"dsn"`
|
||||
}{}
|
||||
|
||||
if err := c.BindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
dsn := payload.DSN
|
||||
if len(dsn) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing dsn argument"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := environ.ConfigureDatabase(ctx, dsn); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
r.POST("/api/setup/strategy/grid", func(c *gin.Context) {
|
||||
var strategy grid.Strategy
|
||||
|
||||
if err := c.BindJSON(&strategy); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
r.GET("/api/trades", func(c *gin.Context) {
|
||||
exchange := c.Query("exchange")
|
||||
symbol := c.Query("symbol")
|
||||
gidStr := c.DefaultQuery("gid", "0")
|
||||
lastGID, err := strconv.ParseInt(gidStr, 10, 64)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("last gid parse error")
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
trades, err := environ.TradeService.Query(service.QueryTradesOptions{
|
||||
Exchange: types.ExchangeName(exchange),
|
||||
Symbol: symbol,
|
||||
LastGID: lastGID,
|
||||
Ordering: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
log.WithError(err).Error("order query error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"trades": trades,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/orders/closed", func(c *gin.Context) {
|
||||
exchange := c.Query("exchange")
|
||||
symbol := c.Query("symbol")
|
||||
gidStr := c.DefaultQuery("gid", "0")
|
||||
|
||||
lastGID, err := strconv.ParseInt(gidStr, 10, 64)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("last gid parse error")
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orders, err := environ.OrderService.Query(service.QueryOrdersOptions{
|
||||
Exchange: types.ExchangeName(exchange),
|
||||
Symbol: symbol,
|
||||
LastGID: lastGID,
|
||||
Ordering: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
log.WithError(err).Error("order query error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"orders": orders,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/trading-volume", func(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "day")
|
||||
segment := c.DefaultQuery("segment", "exchange")
|
||||
startTimeStr := c.Query("start-time")
|
||||
|
||||
var startTime time.Time
|
||||
|
||||
if startTimeStr != "" {
|
||||
v, err := time.Parse(time.RFC3339, startTimeStr)
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
log.WithError(err).Error("start-time format incorrect")
|
||||
return
|
||||
}
|
||||
startTime = v
|
||||
|
||||
} else {
|
||||
switch period {
|
||||
case "day":
|
||||
startTime = time.Now().AddDate(0, 0, -30)
|
||||
|
||||
case "month":
|
||||
startTime = time.Now().AddDate(0, -6, 0)
|
||||
|
||||
case "year":
|
||||
startTime = time.Now().AddDate(-2, 0, 0)
|
||||
|
||||
default:
|
||||
startTime = time.Now().AddDate(0, 0, -7)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := environ.TradeService.QueryTradingVolume(startTime, service.TradingVolumeQueryOptions{
|
||||
SegmentBy: segment,
|
||||
GroupByPeriod: period,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("trading volume query error")
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"tradingVolumes": rows})
|
||||
return
|
||||
})
|
||||
|
||||
|
||||
r.POST("/api/sessions/test", func(c *gin.Context) {
|
||||
var sessionConfig ExchangeSession
|
||||
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() {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"sessions": sessions})
|
||||
})
|
||||
|
||||
r.POST("/api/sessions", func(c *gin.Context) {
|
||||
var sessionConfig ExchangeSession
|
||||
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
|
||||
}
|
||||
|
||||
environ.AddExchangeSession(sessionConfig.Name, session)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/assets", func(c *gin.Context) {
|
||||
totalAssets := types.AssetMap{}
|
||||
|
||||
for _, session := range environ.sessions {
|
||||
balances := session.Account.Balances()
|
||||
|
||||
if err := session.UpdatePrices(ctx); err != nil {
|
||||
log.WithError(err).Error("price update failed")
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
assets := balances.Assets(session.lastPrices)
|
||||
|
||||
for currency, asset := range assets {
|
||||
totalAssets[currency] = asset
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"assets": totalAssets})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"session": session})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/trades", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"trades": session.Trades})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/open-orders", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
marketOrders := make(map[string][]types.Order)
|
||||
for symbol, orderStore := range session.orderStores {
|
||||
marketOrders[symbol] = orderStore.Orders()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"orders": marketOrders})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/account", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"account": session.Account})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/account/balances", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
if session.Account == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("the account of session %s is nil", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"balances": session.Account.Balances()})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/symbols", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for symbol := range session.markets {
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"symbols": symbols})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/used-symbols", func(c *gin.Context) {
|
||||
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for s := range session.usedSymbols {
|
||||
symbols = append(symbols, s)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"symbols": symbols})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/pnl", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/open-orders", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/trades", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/pnl", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
fs := pkger.Dir("/frontend/out")
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
return r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/indicator"
|
||||
|
@ -98,7 +97,7 @@ func (set *StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA {
|
|||
type ExchangeSession struct {
|
||||
// exchange Session based notification system
|
||||
// we make it as a value field so that we can configure it separately
|
||||
Notifiability `json:"-"`
|
||||
Notifiability `json:"-" yaml:"-"`
|
||||
|
||||
// ---------------------------
|
||||
// Session config fields
|
||||
|
@ -121,16 +120,16 @@ type ExchangeSession struct {
|
|||
// ---------------------------
|
||||
|
||||
// The exchange account states
|
||||
Account *types.Account `json:"account"`
|
||||
Account *types.Account `json:"account" yaml:"-"`
|
||||
|
||||
IsInitialized bool `json:"isInitialized"`
|
||||
IsInitialized bool `json:"isInitialized" yaml:"-"`
|
||||
|
||||
// Stream is the connection stream of the exchange
|
||||
Stream types.Stream `json:"-"`
|
||||
Stream types.Stream `json:"-" yaml:"-"`
|
||||
|
||||
Subscriptions map[types.Subscription]types.Subscription `json:"-"`
|
||||
Subscriptions map[types.Subscription]types.Subscription `json:"-" yaml:"-"`
|
||||
|
||||
Exchange types.Exchange `json:"-"`
|
||||
Exchange types.Exchange `json:"-" yaml:"-"`
|
||||
|
||||
// markets defines market configuration of a symbol
|
||||
markets map[string]types.Market
|
||||
|
@ -143,7 +142,7 @@ type ExchangeSession struct {
|
|||
|
||||
// Trades collects the executed trades from the exchange
|
||||
// map: symbol -> []trade
|
||||
Trades map[string]*types.TradeSlice `json:"-"`
|
||||
Trades map[string]*types.TradeSlice `json:"-" yaml:"-"`
|
||||
|
||||
// marketDataStores contains the market data store of each market
|
||||
marketDataStores map[string]*MarketDataStore
|
||||
|
@ -193,11 +192,35 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
|||
|
||||
func (session *ExchangeSession) Init(ctx context.Context, environ *Environment) error {
|
||||
if session.IsInitialized {
|
||||
return errors.New("session is already initialized")
|
||||
return ErrSessionAlreadyInitialized
|
||||
}
|
||||
|
||||
var log = log.WithField("session", session.Name)
|
||||
|
||||
// load markets first
|
||||
var markets, err = LoadExchangeMarketsWithCache(ctx, session.Exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(markets) == 0 {
|
||||
return fmt.Errorf("market config should not be empty")
|
||||
}
|
||||
|
||||
session.markets = markets
|
||||
|
||||
// query and initialize the balances
|
||||
log.Infof("querying balances from session %s...", session.Name)
|
||||
balances, err := session.Exchange.QueryAccountBalances(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("%s account", session.Name)
|
||||
balances.Print()
|
||||
|
||||
session.Account.UpdateBalances(balances)
|
||||
|
||||
var orderExecutor = &ExchangeOrderExecutor{
|
||||
// copy the notification system so that we can route
|
||||
Notifiability: session.Notifiability,
|
||||
|
@ -209,32 +232,7 @@ func (session *ExchangeSession) Init(ctx context.Context, environ *Environment)
|
|||
session.Stream.OnOrderUpdate(orderExecutor.EmitOrderUpdate)
|
||||
session.orderExecutor = orderExecutor
|
||||
|
||||
var markets, err = LoadExchangeMarketsWithCache(ctx, session.Exchange)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(markets) == 0 {
|
||||
return fmt.Errorf("market config should not be empty")
|
||||
}
|
||||
|
||||
session.markets = markets
|
||||
|
||||
// initialize balance data
|
||||
log.Infof("querying balances from session %s...", session.Name)
|
||||
balances, err := session.Exchange.QueryAccountBalances(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("%s account", session.Name)
|
||||
balances.Print()
|
||||
|
||||
session.Account.UpdateBalances(balances)
|
||||
session.Account.BindStream(session.Stream)
|
||||
session.Stream.OnBalanceUpdate(func(balances types.BalanceMap) {
|
||||
log.Infof("balance update: %+v", balances)
|
||||
})
|
||||
|
||||
// insert trade into db right before everything
|
||||
if environ.TradeService != nil {
|
||||
|
@ -403,6 +401,10 @@ func (session *ExchangeSession) Position(symbol string) (pos *Position, ok bool)
|
|||
return pos, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) Positions() map[string]*Position {
|
||||
return session.positions
|
||||
}
|
||||
|
||||
// MarketDataStore returns the market data store of a symbol
|
||||
func (session *ExchangeSession) MarketDataStore(symbol string) (s *MarketDataStore, ok bool) {
|
||||
s, ok = session.marketDataStores[symbol]
|
||||
|
@ -419,16 +421,28 @@ func (session *ExchangeSession) LastPrice(symbol string) (price float64, ok bool
|
|||
return price, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) LastPrices() map[string]float64 {
|
||||
return session.lastPrices
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) Market(symbol string) (market types.Market, ok bool) {
|
||||
market, ok = session.markets[symbol]
|
||||
return market, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) Markets() map[string]types.Market {
|
||||
return session.markets
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) OrderStore(symbol string) (store *OrderStore, ok bool) {
|
||||
store, ok = session.orderStores[symbol]
|
||||
return store, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) OrderStores() map[string]*OrderStore {
|
||||
return session.orderStores
|
||||
}
|
||||
|
||||
// Subscribe save the subscription info, later it will be assigned to the stream
|
||||
func (session *ExchangeSession) Subscribe(channel types.Channel, symbol string, options types.SubscribeOptions) *ExchangeSession {
|
||||
sub := types.Subscription{
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
|
||||
"github.com/c9s/bbgo/pkg/notifier/slacknotifier"
|
||||
"github.com/c9s/bbgo/pkg/notifier/telegramnotifier"
|
||||
"github.com/c9s/bbgo/pkg/server"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/slack/slacklog"
|
||||
)
|
||||
|
@ -65,7 +66,7 @@ func runSetup(basectx context.Context, userConfig *bbgo.Config, enableApiServer
|
|||
|
||||
if enableApiServer {
|
||||
go func() {
|
||||
if err := bbgo.RunServer(ctx, userConfig, environ, trader, true); err != nil {
|
||||
if err := server.Run(ctx, userConfig, environ, trader, true); err != nil {
|
||||
log.WithError(err).Errorf("server error")
|
||||
}
|
||||
}()
|
||||
|
@ -257,7 +258,7 @@ func runConfig(basectx context.Context, userConfig *bbgo.Config, enableApiServer
|
|||
|
||||
if enableApiServer {
|
||||
go func() {
|
||||
if err := bbgo.RunServer(ctx, userConfig, environ, trader, false); err != nil {
|
||||
if err := server.Run(ctx, userConfig, environ, trader, false); err != nil {
|
||||
log.WithError(err).Errorf("server error")
|
||||
}
|
||||
}()
|
||||
|
|
445
pkg/server/routes.go
Normal file
445
pkg/server/routes.go
Normal file
|
@ -0,0 +1,445 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/markbates/pkger"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func Run(ctx context.Context, userConfig *bbgo.Config, environ *bbgo.Environment, trader *bbgo.Trader, setup bool) error {
|
||||
r := gin.Default()
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowMethods: []string{"GET", "POST"},
|
||||
AllowWebSockets: true,
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
|
||||
r.GET("/api/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
if setup {
|
||||
r.POST("/api/setup/test-db", func(c *gin.Context) {
|
||||
payload := struct {
|
||||
DSN string `json:"dsn"`
|
||||
}{}
|
||||
|
||||
if err := c.BindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
dsn := payload.DSN
|
||||
if len(dsn) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing dsn argument"})
|
||||
return
|
||||
}
|
||||
|
||||
db, err := bbgo.ConnectMySQL(dsn)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.Close(); err != nil {
|
||||
logrus.WithError(err).Error("db connection close error")
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
r.POST("/api/setup/configure-db", func(c *gin.Context) {
|
||||
payload := struct {
|
||||
DSN string `json:"dsn"`
|
||||
}{}
|
||||
|
||||
if err := c.BindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
dsn := payload.DSN
|
||||
if len(dsn) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing dsn argument"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := environ.ConfigureDatabase(ctx, dsn); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
r.POST("/api/setup/strategy/single/:id/session/:session", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
strategyID := c.Param("id")
|
||||
|
||||
_, ok := environ.Session(sessionName)
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, "session not found")
|
||||
return
|
||||
}
|
||||
|
||||
var conf map[string]interface{}
|
||||
|
||||
if err := c.BindJSON(&conf); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing arguments"})
|
||||
return
|
||||
}
|
||||
|
||||
strategy, err := bbgo.NewStrategyFromMap(strategyID, conf)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
mount := bbgo.ExchangeStrategyMount{
|
||||
Mounts: []string{sessionName},
|
||||
Strategy: strategy,
|
||||
}
|
||||
|
||||
userConfig.ExchangeStrategies = append(userConfig.ExchangeStrategies, mount)
|
||||
|
||||
out, _ := yaml.Marshal(userConfig)
|
||||
fmt.Println(string(out))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
r.GET("/api/trades", func(c *gin.Context) {
|
||||
exchange := c.Query("exchange")
|
||||
symbol := c.Query("symbol")
|
||||
gidStr := c.DefaultQuery("gid", "0")
|
||||
lastGID, err := strconv.ParseInt(gidStr, 10, 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("last gid parse error")
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
trades, err := environ.TradeService.Query(service.QueryTradesOptions{
|
||||
Exchange: types.ExchangeName(exchange),
|
||||
Symbol: symbol,
|
||||
LastGID: lastGID,
|
||||
Ordering: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
logrus.WithError(err).Error("order query error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"trades": trades,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/orders/closed", func(c *gin.Context) {
|
||||
exchange := c.Query("exchange")
|
||||
symbol := c.Query("symbol")
|
||||
gidStr := c.DefaultQuery("gid", "0")
|
||||
|
||||
lastGID, err := strconv.ParseInt(gidStr, 10, 64)
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("last gid parse error")
|
||||
c.Status(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orders, err := environ.OrderService.Query(service.QueryOrdersOptions{
|
||||
Exchange: types.ExchangeName(exchange),
|
||||
Symbol: symbol,
|
||||
LastGID: lastGID,
|
||||
Ordering: "DESC",
|
||||
})
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
logrus.WithError(err).Error("order query error")
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"orders": orders,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/trading-volume", func(c *gin.Context) {
|
||||
period := c.DefaultQuery("period", "day")
|
||||
segment := c.DefaultQuery("segment", "exchange")
|
||||
startTimeStr := c.Query("start-time")
|
||||
|
||||
var startTime time.Time
|
||||
|
||||
if startTimeStr != "" {
|
||||
v, err := time.Parse(time.RFC3339, startTimeStr)
|
||||
if err != nil {
|
||||
c.Status(http.StatusBadRequest)
|
||||
logrus.WithError(err).Error("start-time format incorrect")
|
||||
return
|
||||
}
|
||||
startTime = v
|
||||
|
||||
} else {
|
||||
switch period {
|
||||
case "day":
|
||||
startTime = time.Now().AddDate(0, 0, -30)
|
||||
|
||||
case "month":
|
||||
startTime = time.Now().AddDate(0, -6, 0)
|
||||
|
||||
case "year":
|
||||
startTime = time.Now().AddDate(-2, 0, 0)
|
||||
|
||||
default:
|
||||
startTime = time.Now().AddDate(0, 0, -7)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
rows, err := environ.TradeService.QueryTradingVolume(startTime, service.TradingVolumeQueryOptions{
|
||||
SegmentBy: segment,
|
||||
GroupByPeriod: period,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("trading volume query error")
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"tradingVolumes": rows})
|
||||
return
|
||||
})
|
||||
|
||||
r.POST("/api/sessions/test", func(c *gin.Context) {
|
||||
var sessionConfig bbgo.ExchangeSession
|
||||
if err := c.BindJSON(&sessionConfig); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
session, err := bbgo.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 = []*bbgo.ExchangeSession{}
|
||||
for _, session := range environ.Sessions() {
|
||||
sessions = append(sessions, session)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"sessions": sessions})
|
||||
})
|
||||
|
||||
r.POST("/api/sessions", func(c *gin.Context) {
|
||||
var sessionConfig bbgo.ExchangeSession
|
||||
if err := c.BindJSON(&sessionConfig); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
session, err := bbgo.NewExchangeSessionFromConfig(sessionConfig.ExchangeName, &sessionConfig)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if userConfig.Sessions == nil {
|
||||
userConfig.Sessions = make(map[string]*bbgo.ExchangeSession)
|
||||
}
|
||||
userConfig.Sessions[sessionConfig.Name] = session
|
||||
|
||||
environ.AddExchangeSession(sessionConfig.Name, session)
|
||||
|
||||
if err := session.Init(ctx, environ); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/api/assets", func(c *gin.Context) {
|
||||
totalAssets := types.AssetMap{}
|
||||
|
||||
for _, session := range environ.Sessions() {
|
||||
balances := session.Account.Balances()
|
||||
|
||||
if err := session.UpdatePrices(ctx); err != nil {
|
||||
logrus.WithError(err).Error("price update failed")
|
||||
c.Status(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
assets := balances.Assets(session.LastPrices())
|
||||
|
||||
for currency, asset := range assets {
|
||||
totalAssets[currency] = asset
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"assets": totalAssets})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"session": session})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/trades", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"trades": session.Trades})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/open-orders", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
marketOrders := make(map[string][]types.Order)
|
||||
for symbol, orderStore := range session.OrderStores() {
|
||||
marketOrders[symbol] = orderStore.Orders()
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"orders": marketOrders})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/account", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"account": session.Account})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/account/balances", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
if session.Account == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("the account of session %s is nil", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"balances": session.Account.Balances()})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/symbols", func(c *gin.Context) {
|
||||
sessionName := c.Param("session")
|
||||
session, ok := environ.Session(sessionName)
|
||||
|
||||
if !ok {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": fmt.Sprintf("session %s not found", sessionName)})
|
||||
return
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for symbol := range session.Markets() {
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"symbols": symbols})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/pnl", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/open-orders", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/trades", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
r.GET("/api/sessions/:session/market/:symbol/pnl", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
|
||||
fs := pkger.Dir("/frontend/out")
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
http.FileServer(fs).ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
|
||||
return r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
|
||||
}
|
|
@ -24,41 +24,41 @@ func init() {
|
|||
type Strategy struct {
|
||||
// The notification system will be injected into the strategy automatically.
|
||||
// This field will be injected automatically since it's a single exchange strategy.
|
||||
*bbgo.Notifiability
|
||||
*bbgo.Notifiability `json:"-" yaml:"-"`
|
||||
|
||||
*bbgo.Graceful
|
||||
*bbgo.Graceful `json:"-" yaml:"-"`
|
||||
|
||||
// OrderExecutor is an interface for submitting order.
|
||||
// This field will be injected automatically since it's a single exchange strategy.
|
||||
bbgo.OrderExecutor
|
||||
|
||||
orderStore *bbgo.OrderStore
|
||||
bbgo.OrderExecutor `json:"-" yaml:"-"`
|
||||
|
||||
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
|
||||
// This field will be injected automatically since we defined the Symbol field.
|
||||
types.Market
|
||||
types.Market `json:"-" yaml:"-"`
|
||||
|
||||
// These fields will be filled from the config file (it translates YAML to JSON)
|
||||
Symbol string `json:"symbol"`
|
||||
Symbol string `json:"symbol" yaml:"symbol"`
|
||||
|
||||
// ProfitSpread is the fixed profit spread you want to submit the sell order
|
||||
ProfitSpread fixedpoint.Value `json:"profitSpread"`
|
||||
ProfitSpread fixedpoint.Value `json:"profitSpread" yaml:"profitSpread"`
|
||||
|
||||
// GridNum is the grid number, how many orders you want to post on the orderbook.
|
||||
GridNum int `json:"gridNumber"`
|
||||
GridNum int `json:"gridNumber" yaml:"gridNumber"`
|
||||
|
||||
UpperPrice fixedpoint.Value `json:"upperPrice"`
|
||||
UpperPrice fixedpoint.Value `json:"upperPrice" yaml:"upperPrice"`
|
||||
|
||||
LowerPrice fixedpoint.Value `json:"lowerPrice"`
|
||||
LowerPrice fixedpoint.Value `json:"lowerPrice" yaml:"lowerPrice"`
|
||||
|
||||
// Quantity is the quantity you want to submit for each order.
|
||||
Quantity float64 `json:"quantity"`
|
||||
Quantity float64 `json:"quantity,omitempty"`
|
||||
|
||||
// OrderAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity.
|
||||
OrderAmount fixedpoint.Value `json:"orderAmount"`
|
||||
// FixedAmount is used for fixed amount (dynamic quantity) if you don't want to use fixed quantity.
|
||||
FixedAmount fixedpoint.Value `json:"amount,omitempty" yaml:"amount"`
|
||||
|
||||
// Long means you want to hold more base asset than the quote asset.
|
||||
Long bool `json:"long"`
|
||||
Long bool `json:"long,omitempty" yaml:"long,omitempty"`
|
||||
|
||||
orderStore *bbgo.OrderStore
|
||||
|
||||
// activeOrders is the locally maintained active order book of the maker orders.
|
||||
activeOrders *bbgo.LocalActiveOrderBook
|
||||
|
@ -157,7 +157,6 @@ func (s *Strategy) submitReverseOrder(order types.Order) {
|
|||
var price = order.Price
|
||||
var quantity = order.Quantity
|
||||
|
||||
|
||||
switch side {
|
||||
case types.SideTypeSell:
|
||||
price += s.ProfitSpread.Float64()
|
||||
|
@ -165,8 +164,8 @@ func (s *Strategy) submitReverseOrder(order types.Order) {
|
|||
price -= s.ProfitSpread.Float64()
|
||||
}
|
||||
|
||||
if s.OrderAmount > 0 {
|
||||
quantity = s.OrderAmount.Float64() / price
|
||||
if s.FixedAmount > 0 {
|
||||
quantity = s.FixedAmount.Float64() / price
|
||||
} else if s.Long {
|
||||
// long = use the same amount to buy more quantity back
|
||||
// the original amount
|
||||
|
|
|
@ -214,6 +214,10 @@ func (a *Account) UpdateBalances(balances BalanceMap) {
|
|||
func (a *Account) BindStream(stream Stream) {
|
||||
stream.OnBalanceUpdate(a.UpdateBalances)
|
||||
stream.OnBalanceSnapshot(a.UpdateBalances)
|
||||
stream.OnBalanceUpdate(func(balances BalanceMap) {
|
||||
logrus.Infof("balance update: %+v", balances)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (a *Account) Print() {
|
||||
|
|
Loading…
Reference in New Issue
Block a user