integrate strateg adding api

This commit is contained in:
c9s 2021-02-03 02:26:41 +08:00
parent f94a45de40
commit 8aa96c4546
12 changed files with 740 additions and 550 deletions

View File

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

View File

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

View File

@ -43,9 +43,12 @@ function getStepContent(step, setActiveStep) {
}}/>
case 3:
return (
<ConfigureGridStrategyForm />
);
<ConfigureGridStrategyForm onBack={() => {
setActiveStep(2)
}} onAdded={() => {
}}/>
);
case 4:
return;
default:

View File

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

View File

@ -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
View File

@ -0,0 +1,6 @@
package bbgo
import "errors"
var ErrSessionAlreadyInitialized = errors.New("session is already initialized")

View File

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

View File

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

View File

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

View File

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

View File

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