diff --git a/config/bbgo.yaml b/config/bbgo.yaml index 8d48fe93a..400df0f73 100644 --- a/config/bbgo.yaml +++ b/config/bbgo.yaml @@ -2,6 +2,7 @@ imports: - github.com/c9s/bbgo/pkg/strategy/buyandhold - github.com/c9s/bbgo/pkg/strategy/xpuremaker + notifications: slack: defaultChannel: "bbgo" @@ -40,3 +41,11 @@ exchangeStrategies: interval: "1m" baseQuantity: 0.01 minDropPercentage: -0.02 +- on: max + xpuremaker: + symbol: MAXUSDT + numOrders: 2 + side: both + behindVolume: 1000.0 + priceTick: 0.01 + baseQuantity: 100.0 diff --git a/config/xpuremaker.yaml b/config/xpuremaker.yaml index cd8072f63..c75bc4873 100644 --- a/config/xpuremaker.yaml +++ b/config/xpuremaker.yaml @@ -10,6 +10,7 @@ reportTrades: "ethusdt": "bbgo-ethusdt" "bnbusdt": "bbgo-bnbusdt" "sxpusdt": "bbgo-sxpusdt" + "maxusdt": "max-maxusdt" reportPnL: - averageCostBySymbols: diff --git a/examples/max-eqmaker/main.go b/examples/max-eqmaker/main.go index b393b9693..c343839dc 100644 --- a/examples/max-eqmaker/main.go +++ b/examples/max-eqmaker/main.go @@ -216,7 +216,7 @@ func generateOrders(symbol, side string, price, priceTick, baseVolume fixedpoint orders = append(orders, maxapi.Order{ Side: side, - OrderType: string(maxapi.OrderTypeLimit), + OrderType: maxapi.OrderTypeLimit, Market: symbol, Price: util.FormatFloat(price.Float64(), 3), Volume: util.FormatFloat(volume, 2), diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index cb661165c..d61105a41 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -73,7 +73,7 @@ func (reporter *TradeReporter) Report(trade types.Trade) { var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade) if err := reporter.notifier.NotifyTo(channel, text, trade); err != nil { - log.WithError(err).Error("notifier error") + log.WithError(err).Errorf("notifier error, channel=%s", channel) } } diff --git a/pkg/bbgo/loader.go b/pkg/bbgo/loader.go deleted file mode 100644 index 920078f66..000000000 --- a/pkg/bbgo/loader.go +++ /dev/null @@ -1,2 +0,0 @@ -package bbgo - diff --git a/pkg/bbgo/loader_test.go b/pkg/bbgo/loader_test.go deleted file mode 100644 index 920078f66..000000000 --- a/pkg/bbgo/loader_test.go +++ /dev/null @@ -1,2 +0,0 @@ -package bbgo - diff --git a/pkg/bbgo/order_processor.go b/pkg/bbgo/order_processor.go index 753f2821a..8ae5d888a 100644 --- a/pkg/bbgo/order_processor.go +++ b/pkg/bbgo/order_processor.go @@ -126,7 +126,7 @@ func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) er order.QuantityString = market.FormatVolume(quantity) */ - return p.Exchange.SubmitOrder(ctx, order) + return p.Exchange.SubmitOrders(ctx, order) } func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 { diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 5a1e8ac21..2ebff9431 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -81,7 +81,7 @@ func (reporter *AverageCostPnLReporter) Of(sessions ...string) *AverageCostPnLRe } func (reporter *AverageCostPnLReporter) When(specs ...string) *AverageCostPnLReporter { - for _,spec := range specs { + for _, spec := range specs { _, err := reporter.cron.AddJob(spec, reporter) if err != nil { panic(err) @@ -152,15 +152,16 @@ func (trader *Trader) Run(ctx context.Context) error { // load and run session strategies for sessionName, strategies := range trader.exchangeStrategies { + session := trader.environment.sessions[sessionName] // we can move this to the exchange session, // that way we can mount the notification on the exchange with DSL orderExecutor := &ExchangeOrderExecutor{ Notifiability: trader.Notifiability, - Exchange: nil, + Session: session, } for _, strategy := range strategies { - err := strategy.Run(ctx, orderExecutor, trader.environment.sessions[sessionName]) + err := strategy.Run(ctx, orderExecutor, session) if err != nil { return err } @@ -325,30 +326,52 @@ type ExchangeOrderExecutionRouter struct { sessions map[string]*ExchangeSession } -func (e *ExchangeOrderExecutionRouter) SubmitOrderTo(ctx context.Context, session string, order types.SubmitOrder) error { +func (e *ExchangeOrderExecutionRouter) SubmitOrdersTo(ctx context.Context, session string, orders ...types.SubmitOrder) error { es, ok := e.sessions[session] if !ok { return errors.Errorf("exchange session %s not found", session) } - e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order) + for _, order := range orders { + market, ok := es.Market(order.Symbol) + if !ok { + return errors.Errorf("market is not defined: %s", order.Symbol) + } - order.PriceString = order.Market.FormatVolume(order.Price) - order.QuantityString = order.Market.FormatVolume(order.Quantity) - return es.Exchange.SubmitOrder(ctx, order) + order.PriceString = market.FormatPrice(order.Price) + order.QuantityString = market.FormatVolume(order.Quantity) + e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order) + + if err := es.Exchange.SubmitOrders(ctx, order); err != nil { + return err + } + } + + return nil } // ExchangeOrderExecutor is an order executor wrapper for single exchange instance. type ExchangeOrderExecutor struct { Notifiability - Exchange types.Exchange + Session *ExchangeSession } -func (e *ExchangeOrderExecutor) SubmitOrder(ctx context.Context, order types.SubmitOrder) error { - e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order) +func (e *ExchangeOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { + for _, order := range orders { + market, ok := e.Session.Market(order.Symbol) + if !ok { + return errors.Errorf("market is not defined: %s", order.Symbol) + } - order.PriceString = order.Market.FormatVolume(order.Price) - order.QuantityString = order.Market.FormatVolume(order.Quantity) - return e.Exchange.SubmitOrder(ctx, order) + order.Market = market + order.PriceString = market.FormatPrice(order.Price) + order.QuantityString = market.FormatVolume(order.Quantity) + + e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order) + + return e.Session.Exchange.SubmitOrders(ctx, order) + } + + return nil } diff --git a/pkg/exchange/binance/convert.go b/pkg/exchange/binance/convert.go new file mode 100644 index 000000000..afc4d7b4f --- /dev/null +++ b/pkg/exchange/binance/convert.go @@ -0,0 +1,150 @@ +package binance + +import ( + "fmt" + "strconv" + "time" + + "github.com/adshao/go-binance" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { + switch orderType { + case types.OrderTypeLimit: + return binance.OrderTypeLimit, nil + + case types.OrderTypeStopLimit: + return binance.OrderTypeStopLossLimit, nil + + case types.OrderTypeStopMarket: + return binance.OrderTypeStopLoss, nil + + case types.OrderTypeMarket: + return binance.OrderTypeMarket, nil + } + + return "", fmt.Errorf("order type %s not supported", orderType) +} + +func toGlobalOrder(binanceOrder *binance.Order) (*types.Order, error) { + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: binanceOrder.Symbol, + Side: toGlobalSideType(binanceOrder.Side), + Type: toGlobalOrderType(binanceOrder.Type), + Quantity: util.MustParseFloat(binanceOrder.OrigQuantity), + Price: util.MustParseFloat(binanceOrder.Price), + TimeInForce: string(binanceOrder.TimeInForce), + }, + OrderID: uint64(binanceOrder.OrderID), + Status: toGlobalOrderStatus(binanceOrder.Status), + ExecutedQuantity: util.MustParseFloat(binanceOrder.ExecutedQuantity), + }, nil +} + +func toGlobalTrade(t binance.TradeV3) (*types.Trade, error) { + // skip trade ID that is the same. however this should not happen + var side types.SideType + if t.IsBuyer { + side = types.SideTypeBuy + } else { + side = types.SideTypeSell + } + + // trade time + mts := time.Unix(0, t.Time*int64(time.Millisecond)) + + price, err := strconv.ParseFloat(t.Price, 64) + if err != nil { + return nil, err + } + + quantity, err := strconv.ParseFloat(t.Quantity, 64) + if err != nil { + return nil, err + } + + quoteQuantity, err := strconv.ParseFloat(t.QuoteQuantity, 64) + if err != nil { + return nil, err + } + + fee, err := strconv.ParseFloat(t.Commission, 64) + if err != nil { + return nil, err + } + + return &types.Trade{ + ID: t.ID, + Price: price, + Symbol: t.Symbol, + Exchange: "binance", + Quantity: quantity, + Side: side, + IsBuyer: t.IsBuyer, + IsMaker: t.IsMaker, + Fee: fee, + FeeCurrency: t.CommissionAsset, + QuoteQuantity: quoteQuantity, + Time: mts, + }, nil +} + +func toGlobalSideType(side binance.SideType) types.SideType { + switch side { + case binance.SideTypeBuy: + return types.SideTypeBuy + + case binance.SideTypeSell: + return types.SideTypeSell + + default: + log.Errorf("unknown side type: %v", side) + return "" + } +} + +func toGlobalOrderType(orderType binance.OrderType) types.OrderType { + switch orderType { + + case binance.OrderTypeLimit: + return types.OrderTypeLimit + + case binance.OrderTypeMarket: + return types.OrderTypeMarket + + case binance.OrderTypeStopLossLimit: + return types.OrderTypeStopLimit + + case binance.OrderTypeStopLoss: + return types.OrderTypeStopMarket + + default: + log.Errorf("unsupported order type: %v", orderType) + return "" + } +} + +func toGlobalOrderStatus(orderStatus binance.OrderStatusType) types.OrderStatus { + switch orderStatus { + case binance.OrderStatusTypeNew: + return types.OrderStatusNew + + case binance.OrderStatusTypeRejected: + return types.OrderStatusRejected + + case binance.OrderStatusTypeCanceled: + return types.OrderStatusCanceled + + case binance.OrderStatusTypePartiallyFilled: + return types.OrderStatusPartiallyFilled + + case binance.OrderStatusTypeFilled: + return types.OrderStatusFilled + } + + return types.OrderStatus(orderStatus) +} diff --git a/pkg/exchange/binance/exchange.go b/pkg/exchange/binance/exchange.go index 3d47b3d57..a1ca5589f 100644 --- a/pkg/exchange/binance/exchange.go +++ b/pkg/exchange/binance/exchange.go @@ -3,10 +3,10 @@ package binance import ( "context" "fmt" - "strconv" "time" "github.com/adshao/go-binance" + "github.com/google/uuid" "github.com/sirupsen/logrus" @@ -55,7 +55,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { BaseCurrency: symbol.BaseAsset, } - if f := symbol.MinNotionalFilter() ; f != nil { + if f := symbol.MinNotionalFilter(); f != nil { market.MinNotional = util.MustParseFloat(f.MinNotional) market.MinAmount = util.MustParseFloat(f.MinNotional) } @@ -65,14 +65,14 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { // minQty defines the minimum quantity/icebergQty allowed. // maxQty defines the maximum quantity/icebergQty allowed. // stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. - if f := symbol.LotSizeFilter() ; f != nil { + if f := symbol.LotSizeFilter(); f != nil { market.MinLot = util.MustParseFloat(f.MinQuantity) market.MinQuantity = util.MustParseFloat(f.MinQuantity) market.MaxQuantity = util.MustParseFloat(f.MaxQuantity) // market.StepSize = util.MustParseFloat(f.StepSize) } - if f := symbol.PriceFilter() ; f != nil { + if f := symbol.PriceFilter(); f != nil { market.MaxPrice = util.MustParseFloat(f.MaxPrice) market.MinPrice = util.MustParseFloat(f.MinPrice) market.TickSize = util.MustParseFloat(f.TickSize) @@ -268,7 +268,25 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { return a, nil } -func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) error { +func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { + remoteOrders, err := e.Client.NewListOpenOrdersService().Symbol(symbol).Do(ctx) + if err != nil { + return orders, err + } + + for _, binanceOrder := range remoteOrders { + order , err := toGlobalOrder(binanceOrder) + if err != nil { + return orders, err + } + + orders = append(orders, *order) + } + + return orders, err +} + +func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { /* limit order example @@ -281,40 +299,39 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) err Price(priceString). Do(ctx) */ + for _, order := range orders { + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return err + } - orderType, err := toLocalOrderType(order.Type) - if err != nil { - return err + clientOrderID := uuid.New().String() + req := e.Client.NewCreateOrderService(). + Symbol(order.Symbol). + Side(binance.SideType(order.Side)). + NewClientOrderID(clientOrderID). + Type(orderType) + + req.Quantity(order.QuantityString) + + if len(order.PriceString) > 0 { + req.Price(order.PriceString) + } + + if len(order.TimeInForce) > 0 { + // TODO: check the TimeInForce value + req.TimeInForce(binance.TimeInForceType(order.TimeInForce)) + } + + retOrder, err := req.Do(ctx) + if err != nil { + return err + } + + log.Infof("order created: %+v", retOrder) } - req := e.Client.NewCreateOrderService(). - Symbol(order.Symbol). - Side(binance.SideType(order.Side)). - Type(orderType). - Quantity(order.QuantityString) - - if len(order.PriceString) > 0 { - req.Price(order.PriceString) - } - if len(order.TimeInForce) > 0 { - req.TimeInForce(order.TimeInForce) - } - - retOrder, err := req.Do(ctx) - log.Infof("order created: %+v", retOrder) - return err -} - -func toLocalOrderType(orderType types.OrderType) (binance.OrderType, error) { - switch orderType { - case types.OrderTypeLimit: - return binance.OrderTypeLimit, nil - - case types.OrderTypeMarket: - return binance.OrderTypeMarket, nil - } - - return "", fmt.Errorf("order type %s not supported", orderType) + return nil } func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, options types.KLineQueryOptions) ([]types.KLine, error) { @@ -393,7 +410,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } for _, t := range remoteTrades { - localTrade, err := convertRemoteTrade(*t) + localTrade, err := toGlobalTrade(*t) if err != nil { log.WithError(err).Errorf("can not convert binance trade: %+v", t) continue @@ -406,54 +423,6 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type return trades, nil } -func convertRemoteTrade(t binance.TradeV3) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side string - if t.IsBuyer { - side = "BUY" - } else { - side = "SELL" - } - - // trade time - mts := time.Unix(0, t.Time*int64(time.Millisecond)) - - price, err := strconv.ParseFloat(t.Price, 64) - if err != nil { - return nil, err - } - - quantity, err := strconv.ParseFloat(t.Quantity, 64) - if err != nil { - return nil, err - } - - quoteQuantity, err := strconv.ParseFloat(t.QuoteQuantity, 64) - if err != nil { - return nil, err - } - - fee, err := strconv.ParseFloat(t.Commission, 64) - if err != nil { - return nil, err - } - - return &types.Trade{ - ID: t.ID, - Price: price, - Symbol: t.Symbol, - Exchange: "binance", - Quantity: quantity, - Side: side, - IsBuyer: t.IsBuyer, - IsMaker: t.IsMaker, - Fee: fee, - FeeCurrency: t.CommissionAsset, - QuoteQuantity: quoteQuantity, - Time: mts, - }, nil -} - func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol, interval string, startTime, endTime time.Time) ([]types.KLine, error) { var allKLines []types.KLine @@ -496,3 +465,4 @@ func (e *Exchange) BatchQueryKLineWindows(ctx context.Context, symbol string, in return klineWindows, nil } + diff --git a/pkg/exchange/binance/parse.go b/pkg/exchange/binance/parse.go index 9f4de6207..ed5e682a7 100644 --- a/pkg/exchange/binance/parse.go +++ b/pkg/exchange/binance/parse.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/adshao/go-binance" "github.com/valyala/fastjson" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -99,7 +100,7 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) { Price: util.MustParseFloat(e.LastExecutedPrice), Quantity: util.MustParseFloat(e.LastExecutedQuantity), QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity), - Side: e.Side, + Side: toGlobalSideType(binance.SideType(e.Side)), IsBuyer: e.Side == "BUY", IsMaker: e.IsMaker, Time: tt, diff --git a/pkg/exchange/max/convert.go b/pkg/exchange/max/convert.go new file mode 100644 index 000000000..f2da72479 --- /dev/null +++ b/pkg/exchange/max/convert.go @@ -0,0 +1,217 @@ +package max + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/c9s/bbgo/pkg/exchange/max/maxapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" +) + +func toGlobalCurrency(currency string) string { + return strings.ToUpper(currency) +} + +func toLocalCurrency(currency string) string { + return strings.ToLower(currency) +} + +func toLocalSymbol(symbol string) string { + return strings.ToLower(symbol) +} + +func toGlobalSymbol(symbol string) string { + return strings.ToUpper(symbol) +} + +func toLocalSideType(side types.SideType) string { + return strings.ToLower(string(side)) +} + +func toGlobalSideType(v string) types.SideType { + switch strings.ToLower(v) { + case "bid", "buy": + return types.SideTypeBuy + + case "ask", "sell": + return types.SideTypeSell + + case "self-trade": + return types.SideTypeSelf + + } + + return types.SideType(v) +} + +func toGlobalOrderStatus(orderStatus max.OrderState, executedVolume, remainingVolume fixedpoint.Value) types.OrderStatus { + + switch orderStatus { + + case max.OrderStateCancel: + return types.OrderStatusCanceled + + case max.OrderStateFinalizing, max.OrderStateDone: + if executedVolume > 0 && remainingVolume > 0 { + return types.OrderStatusPartiallyFilled + } else if remainingVolume == 0 { + return types.OrderStatusFilled + } + + return types.OrderStatusFilled + + case max.OrderStateWait: + if executedVolume > 0 && remainingVolume > 0 { + return types.OrderStatusPartiallyFilled + } + + return types.OrderStatusNew + + case max.OrderStateConvert: + if executedVolume > 0 && remainingVolume > 0 { + return types.OrderStatusPartiallyFilled + } + + return types.OrderStatusNew + + case max.OrderStateFailed: + return types.OrderStatusRejected + + } + + logger.Errorf("unknown order status: %v", orderStatus) + return types.OrderStatus(orderStatus) +} + +func toGlobalOrderType(orderType max.OrderType) types.OrderType { + switch orderType { + case max.OrderTypeLimit: + return types.OrderTypeLimit + + case max.OrderTypeMarket: + return types.OrderTypeMarket + + case max.OrderTypeStopLimit: + return types.OrderTypeStopLimit + + case max.OrderTypeStopMarket: + return types.OrderTypeStopMarket + + } + + logger.Errorf("unknown order type: %v", orderType) + return types.OrderType(orderType) +} + +func toLocalOrderType(orderType types.OrderType) (max.OrderType, error) { + switch orderType { + + case types.OrderTypeStopLimit: + return max.OrderTypeStopLimit, nil + + case types.OrderTypeStopMarket: + return max.OrderTypeStopMarket, nil + + case types.OrderTypeLimit: + return max.OrderTypeLimit, nil + + case types.OrderTypeMarket: + return max.OrderTypeMarket, nil + } + + return "", fmt.Errorf("order type %s not supported", orderType) +} + +func toGlobalOrder(maxOrder max.Order) (*types.Order, error) { + executedVolume, err := fixedpoint.NewFromString(maxOrder.ExecutedVolume) + if err != nil { + return nil, err + } + + remainingVolume, err := fixedpoint.NewFromString(maxOrder.RemainingVolume) + if err != nil { + return nil, err + } + + return &types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: toGlobalSymbol(maxOrder.Market), + Side: toGlobalSideType(maxOrder.Side), + Type: toGlobalOrderType(maxOrder.OrderType), + Quantity: util.MustParseFloat(maxOrder.Volume), + Price: util.MustParseFloat(maxOrder.Price), + TimeInForce: "GTC", // MAX only supports GTC + }, + OrderID: maxOrder.ID, + Status: toGlobalOrderStatus(maxOrder.State, executedVolume, remainingVolume), + ExecutedQuantity: executedVolume.Float64(), + }, nil +} + +func toGlobalTrade(t max.Trade) (*types.Trade, error) { + // skip trade ID that is the same. however this should not happen + var side = toGlobalSideType(t.Side) + + // trade time + mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond)) + + price, err := strconv.ParseFloat(t.Price, 64) + if err != nil { + return nil, err + } + + quantity, err := strconv.ParseFloat(t.Volume, 64) + if err != nil { + return nil, err + } + + quoteQuantity, err := strconv.ParseFloat(t.Funds, 64) + if err != nil { + return nil, err + } + + fee, err := strconv.ParseFloat(t.Fee, 64) + if err != nil { + return nil, err + } + + return &types.Trade{ + ID: int64(t.ID), + Price: price, + Symbol: toGlobalSymbol(t.Market), + Exchange: "max", + Quantity: quantity, + Side: side, + IsBuyer: t.IsBuyer(), + IsMaker: t.IsMaker(), + Fee: fee, + FeeCurrency: toGlobalCurrency(t.FeeCurrency), + QuoteQuantity: quoteQuantity, + Time: mts, + }, nil +} + +func toGlobalDepositStatus(a string) types.DepositStatus { + switch a { + case "submitting", "submitted", "checking": + return types.DepositPending + + case "accepted": + return types.DepositSuccess + + case "rejected": + return types.DepositRejected + + case "canceled": + return types.DepositCancelled + + case "suspect", "refunded": + + } + + return types.DepositStatus(a) +} diff --git a/pkg/exchange/max/exchange.go b/pkg/exchange/max/exchange.go index ba30f9ac1..9a3a061f4 100644 --- a/pkg/exchange/max/exchange.go +++ b/pkg/exchange/max/exchange.go @@ -2,11 +2,9 @@ package max import ( "context" - "fmt" - "strconv" - "strings" "time" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -46,8 +44,10 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { markets := types.MarketMap{} for _, m := range remoteMarkets { + symbol := toGlobalSymbol(m.ID) + market := types.Market{ - Symbol: toGlobalSymbol(m.ID), + Symbol: symbol, PricePrecision: m.QuoteUnitPrecision, VolumePrecision: m.BaseUnitPrecision, QuoteCurrency: toGlobalCurrency(m.QuoteUnit), @@ -62,7 +62,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { TickSize: 0.001, } - markets[m.ID] = market + markets[symbol] = market } return markets, nil @@ -72,26 +72,52 @@ func (e *Exchange) NewStream() types.Stream { return NewStream(e.key, e.secret) } -func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) error { - orderType, err := toLocalOrderType(order.Type) +func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { + maxOrders, err := e.client.OrderService.Open(toLocalSymbol(symbol), maxapi.QueryOrderOptions{}) if err != nil { - return err + return orders, err } - req := e.client.OrderService.NewCreateOrderRequest(). - Market(toLocalSymbol(order.Symbol)). - OrderType(string(orderType)). - Side(toLocalSideType(order.Side)). - Volume(order.QuantityString). - Price(order.PriceString) + for _, maxOrder := range maxOrders { + order, err := toGlobalOrder(maxOrder) + if err != nil { + return orders, err + } - retOrder, err := req.Do(ctx) - if err != nil { - return err + orders = append(orders, *order) } - logger.Infof("order created: %+v", retOrder) - return err + return orders, err +} + +func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error { + for _, order := range orders { + orderType, err := toLocalOrderType(order.Type) + if err != nil { + return err + } + + clientOrderID := uuid.New().String() + req := e.client.OrderService.NewCreateOrderRequest(). + Market(toLocalSymbol(order.Symbol)). + OrderType(string(orderType)). + Side(toLocalSideType(order.Side)). + ClientOrderID(clientOrderID). + Volume(order.QuantityString) + + if len(order.PriceString) > 0 { + req.Price(order.PriceString) + } + + retOrder, err := req.Do(ctx) + if err != nil { + return err + } + + logger.Infof("order created: %+v", retOrder) + } + + return nil } // PlatformFeeCurrency @@ -230,7 +256,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, Address: "", // not supported AddressTag: "", // not supported TransactionID: d.TxID, - Status: convertDepositState(d.State), + Status: toGlobalDepositStatus(d.State), }) } @@ -240,27 +266,6 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since, return allDeposits, err } -func convertDepositState(a string) types.DepositStatus { - switch a { - case "submitting", "submitted", "checking": - return types.DepositPending - - case "accepted": - return types.DepositSuccess - - case "rejected": - return types.DepositRejected - - case "canceled": - return types.DepositCancelled - - case "suspect", "refunded": - - } - - return types.DepositStatus(a) -} - func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { accounts, err := e.client.AccountService.Accounts() if err != nil { @@ -301,7 +306,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type } for _, t := range remoteTrades { - localTrade, err := convertRemoteTrade(t) + localTrade, err := toGlobalTrade(t) if err != nil { logger.WithError(err).Errorf("can not convert trade: %+v", t) continue @@ -356,94 +361,3 @@ func (e *Exchange) QueryAveragePrice(ctx context.Context, symbol string) (float6 return (util.MustParseFloat(ticker.Sell) + util.MustParseFloat(ticker.Buy)) / 2, nil } - -func toGlobalCurrency(currency string) string { - return strings.ToUpper(currency) -} - -func toLocalCurrency(currency string) string { - return strings.ToLower(currency) -} - -func toLocalSymbol(symbol string) string { - return strings.ToLower(symbol) -} - -func toGlobalSymbol(symbol string) string { - return strings.ToUpper(symbol) -} - -func toLocalSideType(side types.SideType) string { - return strings.ToLower(string(side)) -} - -func toGlobalSideType(v string) string { - switch strings.ToLower(v) { - case "bid": - return "BUY" - - case "ask": - return "SELL" - - case "self-trade": - return "SELF" - - } - - return strings.ToUpper(v) -} - -func toLocalOrderType(orderType types.OrderType) (maxapi.OrderType, error) { - switch orderType { - case types.OrderTypeLimit: - return maxapi.OrderTypeLimit, nil - - case types.OrderTypeMarket: - return maxapi.OrderTypeMarket, nil - } - - return "", fmt.Errorf("order type %s not supported", orderType) -} - -func convertRemoteTrade(t maxapi.Trade) (*types.Trade, error) { - // skip trade ID that is the same. however this should not happen - var side = toGlobalSideType(t.Side) - - // trade time - mts := time.Unix(0, t.CreatedAtMilliSeconds*int64(time.Millisecond)) - - price, err := strconv.ParseFloat(t.Price, 64) - if err != nil { - return nil, err - } - - quantity, err := strconv.ParseFloat(t.Volume, 64) - if err != nil { - return nil, err - } - - quoteQuantity, err := strconv.ParseFloat(t.Funds, 64) - if err != nil { - return nil, err - } - - fee, err := strconv.ParseFloat(t.Fee, 64) - if err != nil { - return nil, err - } - - return &types.Trade{ - ID: int64(t.ID), - Price: price, - Symbol: toGlobalSymbol(t.Market), - Exchange: "max", - Quantity: quantity, - Side: side, - IsBuyer: t.IsBuyer(), - IsMaker: t.IsMaker(), - Fee: fee, - FeeCurrency: toGlobalCurrency(t.FeeCurrency), - QuoteQuantity: quoteQuantity, - Time: mts, - }, nil -} diff --git a/pkg/exchange/max/maxapi/order.go b/pkg/exchange/max/maxapi/order.go index d672c126a..bf8666af6 100644 --- a/pkg/exchange/max/maxapi/order.go +++ b/pkg/exchange/max/maxapi/order.go @@ -19,20 +19,28 @@ const ( type OrderState string const ( - OrderStateDone = OrderState("done") - OrderStateCancel = OrderState("cancel") - OrderStateWait = OrderState("wait") - OrderStateConvert = OrderState("convert") + OrderStateDone = OrderState("done") + OrderStateCancel = OrderState("cancel") + OrderStateWait = OrderState("wait") + OrderStateConvert = OrderState("convert") + OrderStateFinalizing = OrderState("finalizing") + OrderStateFailed = OrderState("failed") ) type OrderType string // Order types that the API can return. const ( - OrderTypeMarket = OrderType("market") - OrderTypeLimit = OrderType("limit") + OrderTypeMarket = OrderType("market") + OrderTypeLimit = OrderType("limit") + OrderTypeStopLimit = OrderType("stop_limit") + OrderTypeStopMarket = OrderType("stop_market") ) +type QueryOrderOptions struct { + GroupID int +} + // OrderService manages the Order endpoint. type OrderService struct { client *RestClient @@ -40,22 +48,52 @@ type OrderService struct { // Order represents one returned order (POST order/GET order/GET orders) on the max platform. type Order struct { - ID uint64 `json:"id,omitempty" db:"exchange_id"` - Side string `json:"side" db:"side"` - OrderType string `json:"ord_type,omitempty" db:"order_type"` - Price string `json:"price" db:"price"` - AveragePrice string `json:"avg_price,omitempty" db:"average_price"` - State string `json:"state,omitempty" db:"state"` - Market string `json:"market,omitempty" db:"market"` - Volume string `json:"volume" db:"volume"` - RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"` - ExecutedVolume string `json:"executed_volume,omitempty" db:"executed_volume"` - TradesCount int64 `json:"trades_count,omitempty" db:"trades_count"` - GroupID int64 `json:"group_id,omitempty" db:"group_id"` - ClientOID string `json:"client_oid,omitempty" db:"client_oid"` - CreatedAt time.Time `json:"-" db:"created_at"` - CreatedAtMs int64 `json:"created_at_in_ms,omitempty"` - InsertedAt time.Time `json:"-" db:"inserted_at"` + ID uint64 `json:"id,omitempty" db:"exchange_id"` + Side string `json:"side" db:"side"` + OrderType OrderType `json:"ord_type,omitempty" db:"order_type"` + Price string `json:"price" db:"price"` + AveragePrice string `json:"avg_price,omitempty" db:"average_price"` + State OrderState `json:"state,omitempty" db:"state"` + Market string `json:"market,omitempty" db:"market"` + Volume string `json:"volume" db:"volume"` + RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"` + ExecutedVolume string `json:"executed_volume,omitempty" db:"executed_volume"` + TradesCount int64 `json:"trades_count,omitempty" db:"trades_count"` + GroupID int64 `json:"group_id,omitempty" db:"group_id"` + ClientOID string `json:"client_oid,omitempty" db:"client_oid"` + CreatedAt time.Time `json:"-" db:"created_at"` + CreatedAtMs int64 `json:"created_at_in_ms,omitempty"` + InsertedAt time.Time `json:"-" db:"inserted_at"` +} + +// Open returns open orders +func (s *OrderService) Open(market string, options QueryOrderOptions) ([]Order, error) { + payload := map[string]interface{}{ + "market": market, + // "state": []OrderState{OrderStateWait, OrderStateConvert}, + "order_by": "desc", + } + + if options.GroupID > 0 { + payload["group_id"] = options.GroupID + } + + req, err := s.client.newAuthenticatedRequest("GET", "v2/orders", payload) + if err != nil { + return nil, err + } + + response, err := s.client.sendRequest(req) + if err != nil { + return nil, err + } + + var orders []Order + if err := response.DecodeJSON(&orders); err != nil { + return nil, err + } + + return orders, nil } // All returns all orders for the authenticated account. @@ -281,12 +319,12 @@ func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest { } type CreateOrderRequestParams struct { - PrivateRequestParams + *PrivateRequestParams Market string `json:"market"` Volume string `json:"volume"` - Price string `json:"price"` - StopPrice string `json:"stop_price"` + Price string `json:"price,omitempty"` + StopPrice string `json:"stop_price,omitempty"` Side string `json:"side"` OrderType string `json:"ord_type"` ClientOrderID string `json:"client_oid,omitempty"` @@ -335,7 +373,7 @@ func (r *CreateOrderRequest) ClientOrderID(clientOrderID string) *CreateOrderReq } func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) { - req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", r.params) + req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &r.params) if err != nil { return order, errors.Wrapf(err, "order create error") } diff --git a/pkg/exchange/max/maxapi/public_parser.go b/pkg/exchange/max/maxapi/public_parser.go index 1a487654a..b78d6ad2d 100644 --- a/pkg/exchange/max/maxapi/public_parser.go +++ b/pkg/exchange/max/maxapi/public_parser.go @@ -174,6 +174,8 @@ func (e *BookEvent) Time() time.Time { } func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) { + snapshot.Symbol = strings.ToUpper(e.Market) + for _, bid := range e.Bids { pv, err := bid.PriceVolumePair() if err != nil { diff --git a/pkg/exchange/max/maxapi/restapi.go b/pkg/exchange/max/maxapi/restapi.go index e275d5ac5..27d14e3f8 100644 --- a/pkg/exchange/max/maxapi/restapi.go +++ b/pkg/exchange/max/maxapi/restapi.go @@ -250,7 +250,6 @@ func getPrivateRequestParamsObject(v interface{}) (*PrivateRequestParams, error) vt = vt.Elem() } - if vt.Kind() != reflect.Struct { return nil, errors.New("reflect error: given object is not a struct" + vt.Kind().String()) } diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 1f02067a1..5dbfcc07a 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -49,6 +49,8 @@ func NewStream(key, secret string) *Stream { return } + newbook.Symbol = toGlobalSymbol(e.Market) + switch e.Event { case "snapshot": stream.EmitBookSnapshot(newbook) @@ -89,7 +91,7 @@ func NewStream(key, secret string) *Stream { } func (s *Stream) Subscribe(channel types.Channel, symbol string, options types.SubscribeOptions) { - s.websocketService.Subscribe(string(channel), symbol) + s.websocketService.Subscribe(string(channel), toLocalSymbol(symbol)) } func (s *Stream) Connect(ctx context.Context) error { diff --git a/pkg/strategy/buyandhold/strategy.go b/pkg/strategy/buyandhold/strategy.go index a26d35783..396f034e5 100644 --- a/pkg/strategy/buyandhold/strategy.go +++ b/pkg/strategy/buyandhold/strategy.go @@ -63,7 +63,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s } } - err := orderExecutor.SubmitOrder(ctx, types.SubmitOrder{ + err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index 5e7893fde..cd50038b7 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -37,7 +37,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s } _ = quoteBalance - err := orderExecutor.SubmitOrder(ctx, types.SubmitOrder{ + err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ Symbol: kline.Symbol, Side: types.SideTypeBuy, Type: types.OrderTypeMarket, diff --git a/pkg/strategy/xpuremaker/strategy.go b/pkg/strategy/xpuremaker/strategy.go index a928cbd99..3fa0a0a57 100644 --- a/pkg/strategy/xpuremaker/strategy.go +++ b/pkg/strategy/xpuremaker/strategy.go @@ -45,6 +45,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() + s.update(orderExecutor) + for { select { case <-ctx.Done(): @@ -52,10 +54,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s case <-s.book.C: s.book.C.Drain(2*time.Second, 5*time.Second) - s.update() + s.update(orderExecutor) case <-ticker.C: - s.update() + s.update(orderExecutor) } } }() @@ -89,19 +91,22 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s return nil } -func (s *Strategy) update() { +func (s *Strategy) update(orderExecutor types.OrderExecutor) { switch s.Side { case "buy": - s.updateOrders(types.SideTypeBuy) + s.updateOrders(orderExecutor, types.SideTypeBuy) case "sell": - s.updateOrders(types.SideTypeSell) + s.updateOrders(orderExecutor, types.SideTypeSell) case "both": - s.updateOrders(types.SideTypeBuy) - s.updateOrders(types.SideTypeSell) + s.updateOrders(orderExecutor, types.SideTypeBuy) + s.updateOrders(orderExecutor, types.SideTypeSell) + + default: + log.Panicf("undefined side: %s", s.Side) } } -func (s *Strategy) updateOrders(side types.SideType) { +func (s *Strategy) updateOrders(orderExecutor types.OrderExecutor, side types.SideType) { book := s.book.Copy() var pvs types.PriceVolumeSlice @@ -118,6 +123,8 @@ func (s *Strategy) updateOrders(side types.SideType) { return } + log.Infof("placing order behind volume: %f", s.BehindVolume.Float64()) + index := pvs.IndexByVolumeDepth(s.BehindVolume) if index == -1 { // do not place orders @@ -132,6 +139,10 @@ func (s *Strategy) updateOrders(side types.SideType) { return } log.Infof("submitting %d orders", len(orders)) + if err := orderExecutor.SubmitOrders(context.Background(), orders...); err != nil { + log.WithError(err).Errorf("order submit error") + return + } } func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseVolume fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) { @@ -166,7 +177,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri Quantity: volume, }) - log.Infof("%s order: %.2f @ %.3f", side, volume, price.Float64()) + log.Infof("%s order: %.2f @ %f", side, volume, price.Float64()) if len(orders) >= numOrders { break @@ -175,7 +186,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri price = price + priceTick declog := math.Log10(math.Abs(priceTick.Float64())) expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * math.Abs(priceTick.Float64())) - log.Infof("expBase: %f", expBase.Float64()) + // log.Infof("expBase: %f", expBase.Float64()) } return orders diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index b85e409c6..3acb030dc 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -54,7 +54,9 @@ type Exchange interface { QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error) - SubmitOrder(ctx context.Context, order SubmitOrder) error + SubmitOrders(ctx context.Context, orders ...SubmitOrder) error + + QueryOpenOrders(ctx context.Context, symbol string) (orders []Order, err error) } type TradeQueryOptions struct { diff --git a/pkg/types/market.go b/pkg/types/market.go index 4af98d6ec..437a07235 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -26,8 +26,7 @@ type Market struct { TickSize float64 } -func (m Market) FormatPrice(val float64) string { - +func (m Market) FormatPriceCurrency(val float64) string { switch m.QuoteCurrency { case "USD", "USDT": @@ -41,10 +40,19 @@ func (m Market) FormatPrice(val float64) string { } + return m.FormatPrice(val) +} + +func (m Market) FormatPrice(val float64) string { + + p := math.Pow10(m.PricePrecision) + val = math.Trunc(val*p) / p return strconv.FormatFloat(val, 'f', m.PricePrecision, 64) } func (m Market) FormatVolume(val float64) string { + p := math.Pow10(m.PricePrecision) + val = math.Trunc(val*p) / p return strconv.FormatFloat(val, 'f', m.VolumePrecision, 64) } diff --git a/pkg/types/order.go b/pkg/types/order.go index 4b6033a7e..996d353f1 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -1,7 +1,6 @@ package types import ( - "github.com/adshao/go-binance" "github.com/slack-go/slack" ) @@ -9,14 +8,35 @@ import ( type OrderType string const ( - OrderTypeLimit OrderType = "LIMIT" - OrderTypeMarket OrderType = "MARKET" + OrderTypeLimit OrderType = "LIMIT" + OrderTypeMarket OrderType = "MARKET" + OrderTypeStopLimit OrderType = "STOP_LIMIT" + OrderTypeStopMarket OrderType = "STOP_MARKET" ) +type OrderStatus string + +const ( + OrderStatusNew OrderStatus = "NEW" + OrderStatusFilled OrderStatus = "FILLED" + OrderStatusPartiallyFilled OrderStatus = "PARTIALLY_FILLED" + OrderStatusCanceled OrderStatus = "CANCELED" + OrderStatusRejected OrderStatus = "REJECTED" +) + +type Order struct { + SubmitOrder + + OrderID uint64 `json:"orderID"` // order id + Status OrderStatus `json:"status"` + ExecutedQuantity float64 `json:"executedQuantity"` +} + type SubmitOrder struct { - Symbol string - Side SideType - Type OrderType + Symbol string + Side SideType + Type OrderType + Quantity float64 Price float64 @@ -25,7 +45,7 @@ type SubmitOrder struct { PriceString string QuantityString string - TimeInForce binance.TimeInForceType + TimeInForce string `json:"timeInForce"` // GTC, IOC, FOK } func (o *SubmitOrder) SlackAttachment() slack.Attachment { diff --git a/pkg/types/side.go b/pkg/types/side.go index 7fd24785b..8d75f4893 100644 --- a/pkg/types/side.go +++ b/pkg/types/side.go @@ -6,12 +6,14 @@ type SideType string const ( SideTypeBuy = SideType("BUY") SideTypeSell = SideType("SELL") + SideTypeSelf = SideType("SELF") ) func (side SideType) Color() string { if side == SideTypeBuy { return Green } + if side == SideTypeSell { return Red } diff --git a/pkg/types/trade.go b/pkg/types/trade.go index 7637dbccf..f49a216d6 100644 --- a/pkg/types/trade.go +++ b/pkg/types/trade.go @@ -21,7 +21,7 @@ type Trade struct { QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"` Symbol string `json:"symbol" db:"symbol"` - Side string `json:"side" db:"side"` + Side SideType `json:"side" db:"side"` IsBuyer bool `json:"isBuyer" db:"is_buyer"` IsMaker bool `json:"isMaker" db:"is_maker"` Time time.Time `json:"tradedAt" db:"traded_at"` diff --git a/pkg/types/trader.go b/pkg/types/trader.go index 933941db1..d89199bc3 100644 --- a/pkg/types/trader.go +++ b/pkg/types/trader.go @@ -3,10 +3,10 @@ package types import "context" type OrderExecutor interface { - SubmitOrder(ctx context.Context, order SubmitOrder) error + SubmitOrders(ctx context.Context, orders ...SubmitOrder) error } type OrderExecutionRouter interface { // SubmitOrderTo submit order to a specific exchange session - SubmitOrderTo(ctx context.Context, session string, order SubmitOrder) error + SubmitOrdersTo(ctx context.Context, session string, orders ...SubmitOrder) error }