mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
Fix trade sync for self trades
MAX uses one single trade for presenting self trade. BINANCE uses two trade records for presenting self trade. the trade creation time are the same.
This commit is contained in:
parent
c3dbb1b204
commit
0ba595bd55
|
@ -4,28 +4,26 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/pkg/errors"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
|
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/binance"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
rootCmd.PersistentFlags().String("exchange", "binance", "exchange name")
|
||||||
rootCmd.PersistentFlags().String("symbol", "SANDUSDT", "symbol")
|
rootCmd.PersistentFlags().String("symbol", "SANDUSDT", "symbol")
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "binance-create-self-trade",
|
Use: "create-self-trade",
|
||||||
Short: "this program creates the self trade by getting the market ticker",
|
Short: "this program creates the self trade by getting the market ticker",
|
||||||
|
|
||||||
// SilenceUsage is an option to silence usage when an error occurs.
|
// SilenceUsage is an option to silence usage when an error occurs.
|
||||||
|
@ -35,21 +33,29 @@ var rootCmd = &cobra.Command{
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := godotenv.Load(".env.local") ; err != nil {
|
if err := godotenv.Load(".env.local"); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
key, secret := os.Getenv("BINANCE_API_KEY"), os.Getenv("BINANCE_API_SECRET")
|
|
||||||
if len(key) == 0 || len(secret) == 0 {
|
|
||||||
return errors.New("empty key or secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
symbol, err := cmd.Flags().GetString("symbol")
|
symbol, err := cmd.Flags().GetString("symbol")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var exchange = binance.New(key, secret)
|
exchangeNameStr, err := cmd.Flags().GetString("exchange")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exchangeName, err := types.ValidExchangeName(exchangeNameStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange, err := cmdutil.NewExchange(exchangeName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
markets, err := exchange.QueryMarkets(ctx)
|
markets, err := exchange.QueryMarkets(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -80,32 +86,32 @@ var rootCmd = &cobra.Command{
|
||||||
|
|
||||||
price := ticker.Buy + market.TickSize
|
price := ticker.Buy + market.TickSize
|
||||||
|
|
||||||
if int64(ticker.Sell * 1e8) == int64(price * 1e8) {
|
if int64(ticker.Sell*1e8) == int64(price*1e8) {
|
||||||
log.Fatal("zero spread, can not continue")
|
log.Fatal("zero spread, can not continue")
|
||||||
}
|
}
|
||||||
|
|
||||||
quantity := math.Max(market.MinNotional / price, market.MinQuantity) * 1.1
|
quantity := math.Max(market.MinNotional/price, market.MinQuantity) * 1.1
|
||||||
|
|
||||||
log.Infof("submiting order using quantity %f at price %f", quantity, price)
|
log.Infof("submiting order using quantity %f at price %f", quantity, price)
|
||||||
|
|
||||||
createdOrders, err := exchange.SubmitOrders(ctx, []types.SubmitOrder{
|
createdOrders, err := exchange.SubmitOrders(ctx, []types.SubmitOrder{
|
||||||
{
|
{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Market: market,
|
Market: market,
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: price,
|
Price: price,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
TimeInForce: "GTC",
|
TimeInForce: "GTC",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Market: market,
|
Market: market,
|
||||||
Side: types.SideTypeSell,
|
Side: types.SideTypeSell,
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: price,
|
Price: price,
|
||||||
Quantity: quantity,
|
Quantity: quantity,
|
||||||
TimeInForce: "GTC",
|
TimeInForce: "GTC",
|
||||||
},
|
},
|
||||||
}...)
|
}...)
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrUnimplemented = errors.New("unimplemented method")
|
||||||
|
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
sourceName types.ExchangeName
|
sourceName types.ExchangeName
|
||||||
publicExchange types.Exchange
|
publicExchange types.Exchange
|
||||||
|
@ -208,9 +210,14 @@ func (e Exchange) QueryTrades(ctx context.Context, symbol string, options *types
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
|
||||||
|
// Not using Tickers in back test (yet)
|
||||||
|
return nil, ErrUnimplemented
|
||||||
|
}
|
||||||
|
|
||||||
func (e Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
func (e Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||||
// Not using Tickers in back test (yet)
|
// Not using Tickers in back test (yet)
|
||||||
return nil, nil
|
return nil, ErrUnimplemented
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Exchange) Name() types.ExchangeName {
|
func (e Exchange) Name() types.ExchangeName {
|
||||||
|
|
188
pkg/exchange/batch/batch.go
Normal file
188
pkg/exchange/batch/batch.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package batch
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/exchange/binance"
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExchangeBatchProcessor struct {
|
||||||
|
types.Exchange
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ExchangeBatchProcessor) BatchQueryClosedOrders(ctx context.Context, symbol string, startTime, endTime time.Time, lastOrderID uint64) (c chan types.Order, errC chan error) {
|
||||||
|
c = make(chan types.Order, 500)
|
||||||
|
errC = make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
||||||
|
|
||||||
|
defer close(c)
|
||||||
|
defer close(errC)
|
||||||
|
|
||||||
|
orderIDs := make(map[uint64]struct{}, 500)
|
||||||
|
if lastOrderID > 0 {
|
||||||
|
orderIDs[lastOrderID] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for startTime.Before(endTime) {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("rate limit error")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("batch querying %s closed orders %s <=> %s", symbol, startTime, endTime)
|
||||||
|
|
||||||
|
orders, err := e.QueryClosedOrders(ctx, symbol, startTime, endTime, lastOrderID)
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(orders) == 0 || (len(orders) == 1 && orders[0].OrderID == lastOrderID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range orders {
|
||||||
|
if _, ok := orderIDs[o.OrderID]; ok {
|
||||||
|
logrus.Infof("skipping duplicated order id: %d", o.OrderID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c <- o
|
||||||
|
startTime = o.CreationTime.Time()
|
||||||
|
lastOrderID = o.OrderID
|
||||||
|
orderIDs[o.OrderID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c, errC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ExchangeBatchProcessor) BatchQueryKLines(ctx context.Context, symbol string, interval types.Interval, startTime, endTime time.Time) (c chan types.KLine, errC chan error) {
|
||||||
|
c = make(chan types.KLine, 1000)
|
||||||
|
errC = make(chan error, 1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
||||||
|
|
||||||
|
defer close(c)
|
||||||
|
defer close(errC)
|
||||||
|
|
||||||
|
for startTime.Before(endTime) {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("rate limit error")
|
||||||
|
}
|
||||||
|
|
||||||
|
kLines, err := e.QueryKLines(ctx, symbol, interval, types.KLineQueryOptions{
|
||||||
|
StartTime: &startTime,
|
||||||
|
Limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(kLines) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, kline := range kLines {
|
||||||
|
// ignore any kline before the given start time
|
||||||
|
if kline.StartTime.Before(startTime) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if kline.EndTime.After(endTime) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c <- kline
|
||||||
|
startTime = kline.EndTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c, errC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ExchangeBatchProcessor) BatchQueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (c chan types.Trade, errC chan error) {
|
||||||
|
c = make(chan types.Trade, 500)
|
||||||
|
errC = make(chan error, 1)
|
||||||
|
|
||||||
|
var lastTradeID = options.LastTradeID
|
||||||
|
|
||||||
|
// last 7 days
|
||||||
|
var startTime = time.Now().Add(-7 * 24 * time.Hour)
|
||||||
|
if options.StartTime != nil {
|
||||||
|
startTime = *options.StartTime
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
||||||
|
|
||||||
|
defer close(c)
|
||||||
|
defer close(errC)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := limiter.Wait(ctx); err != nil {
|
||||||
|
logrus.WithError(err).Error("rate limit error")
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("querying %s trades from %s, limit=%d", symbol, startTime, options.Limit)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var trades []types.Trade
|
||||||
|
|
||||||
|
switch ex := e.Exchange.(type) {
|
||||||
|
|
||||||
|
case *binance.Exchange:
|
||||||
|
trades, err = ex.QueryTrades(ctx, symbol, &types.TradeQueryOptions{
|
||||||
|
StartTime: &startTime,
|
||||||
|
Limit: options.Limit,
|
||||||
|
|
||||||
|
// only for MAX right now. binance is not using it, since we need to handle self-trade for binance
|
||||||
|
LastTradeID: lastTradeID,
|
||||||
|
})
|
||||||
|
|
||||||
|
default:
|
||||||
|
trades, err = ex.QueryTrades(ctx, symbol, &types.TradeQueryOptions{
|
||||||
|
StartTime: &startTime,
|
||||||
|
Limit: options.Limit,
|
||||||
|
|
||||||
|
// only for MAX right now. binance is not using it, since we need to handle self-trade for binance
|
||||||
|
LastTradeID: lastTradeID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errC <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(trades) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("returned %d trades", len(trades))
|
||||||
|
|
||||||
|
// increase the window to the next time frame by adding 1 millisecond
|
||||||
|
startTime = time.Time(trades[len(trades)-1].Time)
|
||||||
|
for _, t := range trades {
|
||||||
|
lastTradeID = t.ID
|
||||||
|
|
||||||
|
// ignore the first trade if last TradeID is given
|
||||||
|
c <- t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c, errC
|
||||||
|
}
|
|
@ -62,14 +62,20 @@ func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticke
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||||
var ret = make(map[string]types.Ticker)
|
var tickers = make(map[string]types.Ticker)
|
||||||
listPriceChangeStatsService := e.Client.NewListPriceChangeStatsService()
|
|
||||||
|
|
||||||
if len(symbol) == 1 {
|
if len(symbol) == 1 {
|
||||||
listPriceChangeStatsService.Symbol(symbol[0])
|
ticker, err := e.QueryTicker(ctx, symbol[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tickers[strings.ToUpper(symbol[0])] = *ticker
|
||||||
|
return tickers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
changeStats, err := listPriceChangeStatsService.Do(ctx)
|
var req = e.Client.NewListPriceChangeStatsService()
|
||||||
|
changeStats, err := req.Do(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -97,10 +103,10 @@ func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[stri
|
||||||
Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)),
|
Time: time.Unix(0, stats.CloseTime*int64(time.Millisecond)),
|
||||||
}
|
}
|
||||||
|
|
||||||
ret[stats.Symbol] = tick
|
tickers[stats.Symbol] = tick
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret, nil
|
return tickers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
||||||
|
@ -564,7 +570,6 @@ func (e *Exchange) submitSpotOrder(ctx context.Context, order types.SubmitOrder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
switch order.Type {
|
switch order.Type {
|
||||||
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
||||||
if len(order.StopPriceString) == 0 {
|
if len(order.StopPriceString) == 0 {
|
||||||
|
@ -695,17 +700,21 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
|
|
||||||
if options.Limit > 0 {
|
if options.Limit > 0 {
|
||||||
req.Limit(int(options.Limit))
|
req.Limit(int(options.Limit))
|
||||||
|
} else {
|
||||||
|
req.Limit(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.StartTime != nil {
|
if options.StartTime != nil {
|
||||||
req.StartTime(options.StartTime.UnixNano() / int64(time.Millisecond))
|
req.StartTime(options.StartTime.UnixNano() / int64(time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.EndTime != nil {
|
if options.EndTime != nil {
|
||||||
req.EndTime(options.EndTime.UnixNano() / int64(time.Millisecond))
|
req.EndTime(options.EndTime.UnixNano() / int64(time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BINANCE uses inclusive last trade ID, so we need to add by 1
|
||||||
if options.LastTradeID > 0 {
|
if options.LastTradeID > 0 {
|
||||||
req.FromID(options.LastTradeID)
|
req.FromID(options.LastTradeID + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteTrades, err = req.Do(ctx)
|
remoteTrades, err = req.Do(ctx)
|
||||||
|
@ -714,11 +723,12 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
req := e.Client.NewListTradesService().
|
req := e.Client.NewListTradesService().
|
||||||
Limit(1000).
|
|
||||||
Symbol(symbol)
|
Symbol(symbol)
|
||||||
|
|
||||||
if options.Limit > 0 {
|
if options.Limit > 0 {
|
||||||
req.Limit(int(options.Limit))
|
req.Limit(int(options.Limit))
|
||||||
|
} else {
|
||||||
|
req.Limit(1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.StartTime != nil {
|
if options.StartTime != nil {
|
||||||
|
@ -727,8 +737,10 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
if options.EndTime != nil {
|
if options.EndTime != nil {
|
||||||
req.EndTime(options.EndTime.UnixNano() / int64(time.Millisecond))
|
req.EndTime(options.EndTime.UnixNano() / int64(time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BINANCE uses inclusive last trade ID, so we need to add by 1
|
||||||
if options.LastTradeID > 0 {
|
if options.LastTradeID > 0 {
|
||||||
req.FromID(options.LastTradeID)
|
req.FromID(options.LastTradeID + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteTrades, err = req.Do(ctx)
|
remoteTrades, err = req.Do(ctx)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -43,24 +44,34 @@ func (e *Exchange) Name() types.ExchangeName {
|
||||||
return types.ExchangeMax
|
return types.ExchangeMax
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) {
|
||||||
|
ticker, err := e.client.PublicService.Ticker(toLocalSymbol(symbol))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.Ticker{
|
||||||
|
Time: ticker.Time,
|
||||||
|
Volume: util.MustParseFloat(ticker.Volume),
|
||||||
|
Last: util.MustParseFloat(ticker.Last),
|
||||||
|
Open: util.MustParseFloat(ticker.Open),
|
||||||
|
High: util.MustParseFloat(ticker.High),
|
||||||
|
Low: util.MustParseFloat(ticker.Low),
|
||||||
|
Buy: util.MustParseFloat(ticker.Buy),
|
||||||
|
Sell: util.MustParseFloat(ticker.Sell),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) {
|
||||||
var ret = make(map[string]types.Ticker)
|
var ret = make(map[string]types.Ticker)
|
||||||
|
|
||||||
if len(symbol) == 1 {
|
if len(symbol) == 1 {
|
||||||
ticker, err := e.client.PublicService.Ticker(toLocalSymbol(symbol[0]))
|
ticker, err := e.QueryTicker(ctx, symbol[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ret[toGlobalSymbol(symbol[0])] = types.Ticker{
|
|
||||||
Time: ticker.Time,
|
ret[toGlobalSymbol(symbol[0])] = *ticker
|
||||||
Volume: util.MustParseFloat(ticker.Volume),
|
|
||||||
Last: util.MustParseFloat(ticker.Last),
|
|
||||||
Open: util.MustParseFloat(ticker.Open),
|
|
||||||
High: util.MustParseFloat(ticker.High),
|
|
||||||
Low: util.MustParseFloat(ticker.Low),
|
|
||||||
Buy: util.MustParseFloat(ticker.Buy),
|
|
||||||
Sell: util.MustParseFloat(ticker.Sell),
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tickers, err := e.client.PublicService.Tickers()
|
tickers, err := e.client.PublicService.Tickers()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -285,8 +296,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
||||||
req := e.client.OrderService.NewCreateOrderRequest().
|
req := e.client.OrderService.NewCreateOrderRequest().
|
||||||
Market(toLocalSymbol(order.Symbol)).
|
Market(toLocalSymbol(order.Symbol)).
|
||||||
OrderType(string(orderType)).
|
OrderType(string(orderType)).
|
||||||
Side(toLocalSideType(order.Side)).
|
Side(toLocalSideType(order.Side))
|
||||||
Volume(order.QuantityString)
|
|
||||||
|
|
||||||
if len(order.ClientOrderID) > 0 {
|
if len(order.ClientOrderID) > 0 {
|
||||||
req.ClientOrderID(order.ClientOrderID)
|
req.ClientOrderID(order.ClientOrderID)
|
||||||
|
@ -295,6 +305,25 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
||||||
req.ClientOrderID(clientOrderID)
|
req.ClientOrderID(clientOrderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(order.QuantityString) > 0 {
|
||||||
|
req.Volume(order.QuantityString)
|
||||||
|
} else if order.Market.Symbol != "" {
|
||||||
|
req.Volume(order.Market.FormatQuantity(order.Quantity))
|
||||||
|
} else {
|
||||||
|
req.Volume(strconv.FormatFloat(order.Quantity, 'f', 8, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
// set price field for limit orders
|
||||||
|
switch order.Type {
|
||||||
|
case types.OrderTypeStopLimit, types.OrderTypeLimit:
|
||||||
|
if len(order.PriceString) > 0 {
|
||||||
|
req.Price(order.PriceString)
|
||||||
|
} else if order.Market.Symbol != "" {
|
||||||
|
req.Price(order.Market.FormatPrice(order.Price))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set stop price field for limit orders
|
||||||
switch order.Type {
|
switch order.Type {
|
||||||
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
||||||
if len(order.StopPriceString) == 0 {
|
if len(order.StopPriceString) == 0 {
|
||||||
|
@ -502,6 +531,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
req.Limit(500)
|
req.Limit(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MAX uses exclusive last trade ID
|
||||||
if options.LastTradeID > 0 {
|
if options.LastTradeID > 0 {
|
||||||
req.From(options.LastTradeID)
|
req.From(options.LastTradeID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
batch2 "github.com/c9s/bbgo/pkg/exchange/batch"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +32,7 @@ func (s *BacktestService) Sync(ctx context.Context, exchange types.Exchange, sym
|
||||||
startTime = lastKLine.StartTime.Add(time.Minute)
|
startTime = lastKLine.StartTime.Add(time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
batch := &types.ExchangeBatchProcessor{Exchange: exchange}
|
batch := &batch2.ExchangeBatchProcessor{Exchange: exchange}
|
||||||
|
|
||||||
// should use channel here
|
// should use channel here
|
||||||
klineC, errC := batch.BatchQueryKLines(ctx, symbol, interval, startTime, now)
|
klineC, errC := batch.BatchQueryKLines(ctx, symbol, interval, startTime, now)
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
batch2 "github.com/c9s/bbgo/pkg/exchange/batch"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -39,7 +40,7 @@ func (s *SyncService) SyncOrders(ctx context.Context, exchange types.Exchange, s
|
||||||
logrus.Infof("found last order, start from lastID = %d since %s", lastID, startTime)
|
logrus.Infof("found last order, start from lastID = %d since %s", lastID, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
batch := &types.ExchangeBatchProcessor{Exchange: exchange}
|
batch := &batch2.ExchangeBatchProcessor{Exchange: exchange}
|
||||||
ordersC, errC := batch.BatchQueryClosedOrders(ctx, symbol, startTime, time.Now(), lastID)
|
ordersC, errC := batch.BatchQueryClosedOrders(ctx, symbol, startTime, time.Now(), lastID)
|
||||||
for order := range ordersC {
|
for order := range ordersC {
|
||||||
select {
|
select {
|
||||||
|
@ -81,14 +82,17 @@ func (s *SyncService) SyncTrades(ctx context.Context, exchange types.Exchange, s
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastTradeID int64 = 0
|
||||||
if lastTrade != nil {
|
if lastTrade != nil {
|
||||||
startTime = time.Time(lastTrade.Time).Add(time.Millisecond)
|
startTime = time.Time(lastTrade.Time).Add(time.Millisecond)
|
||||||
|
lastTradeID = lastTrade.ID
|
||||||
logrus.Infof("found last trade, start from lastID = %d since %s", lastTrade.ID, startTime)
|
logrus.Infof("found last trade, start from lastID = %d since %s", lastTrade.ID, startTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
batch := &types.ExchangeBatchProcessor{Exchange: exchange}
|
batch := &batch2.ExchangeBatchProcessor{Exchange: exchange}
|
||||||
tradeC, errC := batch.BatchQueryTrades(ctx, symbol, &types.TradeQueryOptions{
|
tradeC, errC := batch.BatchQueryTrades(ctx, symbol, &types.TradeQueryOptions{
|
||||||
StartTime: &startTime,
|
StartTime: &startTime,
|
||||||
|
LastTradeID: lastTradeID,
|
||||||
})
|
})
|
||||||
|
|
||||||
for trade := range tradeC {
|
for trade := range tradeC {
|
||||||
|
@ -107,7 +111,6 @@ func (s *SyncService) SyncTrades(ctx context.Context, exchange types.Exchange, s
|
||||||
if err := s.TradeService.Insert(trade); err != nil {
|
if err := s.TradeService.Insert(trade); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return <-errC
|
return <-errC
|
||||||
|
|
|
@ -1,162 +1,2 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/time/rate"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ExchangeBatchProcessor struct {
|
|
||||||
Exchange
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExchangeBatchProcessor) BatchQueryClosedOrders(ctx context.Context, symbol string, startTime, endTime time.Time, lastOrderID uint64) (c chan Order, errC chan error) {
|
|
||||||
c = make(chan Order, 500)
|
|
||||||
errC = make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
|
||||||
|
|
||||||
defer close(c)
|
|
||||||
defer close(errC)
|
|
||||||
|
|
||||||
orderIDs := make(map[uint64]struct{}, 500)
|
|
||||||
if lastOrderID > 0 {
|
|
||||||
orderIDs[lastOrderID] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
for startTime.Before(endTime) {
|
|
||||||
if err := limiter.Wait(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Error("rate limit error")
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("batch querying %s closed orders %s <=> %s", symbol, startTime, endTime)
|
|
||||||
|
|
||||||
orders, err := e.QueryClosedOrders(ctx, symbol, startTime, endTime, lastOrderID)
|
|
||||||
if err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(orders) == 0 || (len(orders) == 1 && orders[0].OrderID == lastOrderID) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, o := range orders {
|
|
||||||
if _, ok := orderIDs[o.OrderID]; ok {
|
|
||||||
logrus.Infof("skipping duplicated order id: %d", o.OrderID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
c <- o
|
|
||||||
startTime = o.CreationTime.Time()
|
|
||||||
lastOrderID = o.OrderID
|
|
||||||
orderIDs[o.OrderID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c, errC
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExchangeBatchProcessor) BatchQueryKLines(ctx context.Context, symbol string, interval Interval, startTime, endTime time.Time) (c chan KLine, errC chan error) {
|
|
||||||
c = make(chan KLine, 1000)
|
|
||||||
errC = make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
|
||||||
|
|
||||||
defer close(c)
|
|
||||||
defer close(errC)
|
|
||||||
|
|
||||||
for startTime.Before(endTime) {
|
|
||||||
if err := limiter.Wait(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Error("rate limit error")
|
|
||||||
}
|
|
||||||
|
|
||||||
kLines, err := e.QueryKLines(ctx, symbol, interval, KLineQueryOptions{
|
|
||||||
StartTime: &startTime,
|
|
||||||
Limit: 1000,
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(kLines) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, kline := range kLines {
|
|
||||||
// ignore any kline before the given start time
|
|
||||||
if kline.StartTime.Before(startTime) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if kline.EndTime.After(endTime) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c <- kline
|
|
||||||
startTime = kline.EndTime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c, errC
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ExchangeBatchProcessor) BatchQueryTrades(ctx context.Context, symbol string, options *TradeQueryOptions) (c chan Trade, errC chan error) {
|
|
||||||
c = make(chan Trade, 500)
|
|
||||||
errC = make(chan error, 1)
|
|
||||||
|
|
||||||
// last 7 days
|
|
||||||
var startTime = time.Now().Add(-7 * 24 * time.Hour)
|
|
||||||
if options.StartTime != nil {
|
|
||||||
startTime = *options.StartTime
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
limiter := rate.NewLimiter(rate.Every(5*time.Second), 2) // from binance (original 1200, use 1000 for safety)
|
|
||||||
|
|
||||||
defer close(c)
|
|
||||||
defer close(errC)
|
|
||||||
|
|
||||||
for {
|
|
||||||
if err := limiter.Wait(ctx); err != nil {
|
|
||||||
logrus.WithError(err).Error("rate limit error")
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("querying %s trades from %s, limit=%d", symbol, startTime, options.Limit)
|
|
||||||
|
|
||||||
trades, err := e.QueryTrades(ctx, symbol, &TradeQueryOptions{
|
|
||||||
StartTime: &startTime,
|
|
||||||
Limit: options.Limit,
|
|
||||||
// LastTradeID: lastTradeID,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
errC <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(trades) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof("returned %d trades", len(trades))
|
|
||||||
|
|
||||||
// increase the window to the next time frame by adding 1 millisecond
|
|
||||||
startTime = time.Time(trades[len(trades)-1].Time).Add(time.Millisecond)
|
|
||||||
for _, t := range trades {
|
|
||||||
// ignore the first trade if last TradeID is given
|
|
||||||
c <- t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return c, errC
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -12,6 +14,22 @@ const DateFormat = "2006-01-02"
|
||||||
|
|
||||||
type ExchangeName string
|
type ExchangeName string
|
||||||
|
|
||||||
|
func (n *ExchangeName) UnmarshalJSON(data []byte) error {
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(data, &s) ; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case "max", "binance", "ftx":
|
||||||
|
*n = ExchangeName(s)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("unknown or unsupported exchange name: %s, valid names are: max, binance, ftx", s)
|
||||||
|
}
|
||||||
|
|
||||||
func (n ExchangeName) String() string {
|
func (n ExchangeName) String() string {
|
||||||
return string(n)
|
return string(n)
|
||||||
}
|
}
|
||||||
|
@ -46,6 +64,8 @@ type Exchange interface {
|
||||||
|
|
||||||
QueryAccountBalances(ctx context.Context) (BalanceMap, error)
|
QueryAccountBalances(ctx context.Context) (BalanceMap, error)
|
||||||
|
|
||||||
|
QueryTicker(ctx context.Context, symbol string) (*Ticker, error)
|
||||||
|
|
||||||
QueryTickers(ctx context.Context, symbol ...string) (map[string]Ticker, error)
|
QueryTickers(ctx context.Context, symbol ...string) (map[string]Ticker, error)
|
||||||
|
|
||||||
QueryKLines(ctx context.Context, symbol string, interval Interval, options KLineQueryOptions) ([]KLine, error)
|
QueryKLines(ctx context.Context, symbol string, interval Interval, options KLineQueryOptions) ([]KLine, error)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user