Add more exchange order features

- use uuid for client order id
- add stop limit and stop market order types
- add order convert functions
- improve submit orders
This commit is contained in:
c9s 2020-10-25 18:26:10 +08:00
parent 0d570dd4c8
commit 308427416a
26 changed files with 658 additions and 293 deletions

View File

@ -2,6 +2,7 @@
imports: imports:
- github.com/c9s/bbgo/pkg/strategy/buyandhold - github.com/c9s/bbgo/pkg/strategy/buyandhold
- github.com/c9s/bbgo/pkg/strategy/xpuremaker - github.com/c9s/bbgo/pkg/strategy/xpuremaker
notifications: notifications:
slack: slack:
defaultChannel: "bbgo" defaultChannel: "bbgo"
@ -40,3 +41,11 @@ exchangeStrategies:
interval: "1m" interval: "1m"
baseQuantity: 0.01 baseQuantity: 0.01
minDropPercentage: -0.02 minDropPercentage: -0.02
- on: max
xpuremaker:
symbol: MAXUSDT
numOrders: 2
side: both
behindVolume: 1000.0
priceTick: 0.01
baseQuantity: 100.0

View File

@ -10,6 +10,7 @@ reportTrades:
"ethusdt": "bbgo-ethusdt" "ethusdt": "bbgo-ethusdt"
"bnbusdt": "bbgo-bnbusdt" "bnbusdt": "bbgo-bnbusdt"
"sxpusdt": "bbgo-sxpusdt" "sxpusdt": "bbgo-sxpusdt"
"maxusdt": "max-maxusdt"
reportPnL: reportPnL:
- averageCostBySymbols: - averageCostBySymbols:

View File

@ -216,7 +216,7 @@ func generateOrders(symbol, side string, price, priceTick, baseVolume fixedpoint
orders = append(orders, maxapi.Order{ orders = append(orders, maxapi.Order{
Side: side, Side: side,
OrderType: string(maxapi.OrderTypeLimit), OrderType: maxapi.OrderTypeLimit,
Market: symbol, Market: symbol,
Price: util.FormatFloat(price.Float64(), 3), Price: util.FormatFloat(price.Float64(), 3),
Volume: util.FormatFloat(volume, 2), Volume: util.FormatFloat(volume, 2),

View File

@ -73,7 +73,7 @@ func (reporter *TradeReporter) Report(trade types.Trade) {
var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade) var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade)
if err := reporter.notifier.NotifyTo(channel, text, trade); err != nil { 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)
} }
} }

View File

@ -1,2 +0,0 @@
package bbgo

View File

@ -1,2 +0,0 @@
package bbgo

View File

@ -126,7 +126,7 @@ func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) er
order.QuantityString = market.FormatVolume(quantity) 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 { func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 {

View File

@ -81,7 +81,7 @@ func (reporter *AverageCostPnLReporter) Of(sessions ...string) *AverageCostPnLRe
} }
func (reporter *AverageCostPnLReporter) When(specs ...string) *AverageCostPnLReporter { func (reporter *AverageCostPnLReporter) When(specs ...string) *AverageCostPnLReporter {
for _,spec := range specs { for _, spec := range specs {
_, err := reporter.cron.AddJob(spec, reporter) _, err := reporter.cron.AddJob(spec, reporter)
if err != nil { if err != nil {
panic(err) panic(err)
@ -152,15 +152,16 @@ func (trader *Trader) Run(ctx context.Context) error {
// load and run session strategies // load and run session strategies
for sessionName, strategies := range trader.exchangeStrategies { for sessionName, strategies := range trader.exchangeStrategies {
session := trader.environment.sessions[sessionName]
// we can move this to the exchange session, // we can move this to the exchange session,
// that way we can mount the notification on the exchange with DSL // that way we can mount the notification on the exchange with DSL
orderExecutor := &ExchangeOrderExecutor{ orderExecutor := &ExchangeOrderExecutor{
Notifiability: trader.Notifiability, Notifiability: trader.Notifiability,
Exchange: nil, Session: session,
} }
for _, strategy := range strategies { for _, strategy := range strategies {
err := strategy.Run(ctx, orderExecutor, trader.environment.sessions[sessionName]) err := strategy.Run(ctx, orderExecutor, session)
if err != nil { if err != nil {
return err return err
} }
@ -325,30 +326,52 @@ type ExchangeOrderExecutionRouter struct {
sessions map[string]*ExchangeSession 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] es, ok := e.sessions[session]
if !ok { if !ok {
return errors.Errorf("exchange session %s not found", session) return errors.Errorf("exchange session %s not found", session)
} }
for _, order := range orders {
market, ok := es.Market(order.Symbol)
if !ok {
return errors.Errorf("market is not defined: %s", order.Symbol)
}
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) e.Notify(":memo: Submitting order to %s %s %s %s with quantity: %s", session, order.Symbol, order.Type, order.Side, order.QuantityString, order)
order.PriceString = order.Market.FormatVolume(order.Price) if err := es.Exchange.SubmitOrders(ctx, order); err != nil {
order.QuantityString = order.Market.FormatVolume(order.Quantity) return err
return es.Exchange.SubmitOrder(ctx, order) }
}
return nil
} }
// ExchangeOrderExecutor is an order executor wrapper for single exchange instance. // ExchangeOrderExecutor is an order executor wrapper for single exchange instance.
type ExchangeOrderExecutor struct { type ExchangeOrderExecutor struct {
Notifiability Notifiability
Exchange types.Exchange Session *ExchangeSession
} }
func (e *ExchangeOrderExecutor) SubmitOrder(ctx context.Context, order types.SubmitOrder) error { 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.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) e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
order.PriceString = order.Market.FormatVolume(order.Price) return e.Session.Exchange.SubmitOrders(ctx, order)
order.QuantityString = order.Market.FormatVolume(order.Quantity) }
return e.Exchange.SubmitOrder(ctx, order)
return nil
} }

View File

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

View File

@ -3,10 +3,10 @@ package binance
import ( import (
"context" "context"
"fmt" "fmt"
"strconv"
"time" "time"
"github.com/adshao/go-binance" "github.com/adshao/go-binance"
"github.com/google/uuid"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -55,7 +55,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
BaseCurrency: symbol.BaseAsset, BaseCurrency: symbol.BaseAsset,
} }
if f := symbol.MinNotionalFilter() ; f != nil { if f := symbol.MinNotionalFilter(); f != nil {
market.MinNotional = util.MustParseFloat(f.MinNotional) market.MinNotional = util.MustParseFloat(f.MinNotional)
market.MinAmount = 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. // minQty defines the minimum quantity/icebergQty allowed.
// maxQty defines the maximum quantity/icebergQty allowed. // maxQty defines the maximum quantity/icebergQty allowed.
// stepSize defines the intervals that a quantity/icebergQty can be increased/decreased by. // 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.MinLot = util.MustParseFloat(f.MinQuantity)
market.MinQuantity = util.MustParseFloat(f.MinQuantity) market.MinQuantity = util.MustParseFloat(f.MinQuantity)
market.MaxQuantity = util.MustParseFloat(f.MaxQuantity) market.MaxQuantity = util.MustParseFloat(f.MaxQuantity)
// market.StepSize = util.MustParseFloat(f.StepSize) // 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.MaxPrice = util.MustParseFloat(f.MaxPrice)
market.MinPrice = util.MustParseFloat(f.MinPrice) market.MinPrice = util.MustParseFloat(f.MinPrice)
market.TickSize = util.MustParseFloat(f.TickSize) market.TickSize = util.MustParseFloat(f.TickSize)
@ -268,7 +268,25 @@ func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
return a, nil 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 limit order example
@ -281,40 +299,39 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) err
Price(priceString). Price(priceString).
Do(ctx) Do(ctx)
*/ */
for _, order := range orders {
orderType, err := toLocalOrderType(order.Type) orderType, err := toLocalOrderType(order.Type)
if err != nil { if err != nil {
return err return err
} }
clientOrderID := uuid.New().String()
req := e.Client.NewCreateOrderService(). req := e.Client.NewCreateOrderService().
Symbol(order.Symbol). Symbol(order.Symbol).
Side(binance.SideType(order.Side)). Side(binance.SideType(order.Side)).
Type(orderType). NewClientOrderID(clientOrderID).
Quantity(order.QuantityString) Type(orderType)
req.Quantity(order.QuantityString)
if len(order.PriceString) > 0 { if len(order.PriceString) > 0 {
req.Price(order.PriceString) req.Price(order.PriceString)
} }
if len(order.TimeInForce) > 0 { if len(order.TimeInForce) > 0 {
req.TimeInForce(order.TimeInForce) // TODO: check the TimeInForce value
req.TimeInForce(binance.TimeInForceType(order.TimeInForce))
} }
retOrder, err := req.Do(ctx) retOrder, err := req.Do(ctx)
log.Infof("order created: %+v", retOrder) if err != nil {
return err 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) log.Infof("order created: %+v", retOrder)
}
return nil
} }
func (e *Exchange) QueryKLines(ctx context.Context, symbol, interval string, options types.KLineQueryOptions) ([]types.KLine, error) { 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 { for _, t := range remoteTrades {
localTrade, err := convertRemoteTrade(*t) localTrade, err := toGlobalTrade(*t)
if err != nil { if err != nil {
log.WithError(err).Errorf("can not convert binance trade: %+v", t) log.WithError(err).Errorf("can not convert binance trade: %+v", t)
continue continue
@ -406,54 +423,6 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
return trades, nil 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) { func (e *Exchange) BatchQueryKLines(ctx context.Context, symbol, interval string, startTime, endTime time.Time) ([]types.KLine, error) {
var allKLines []types.KLine var allKLines []types.KLine
@ -496,3 +465,4 @@ func (e *Exchange) BatchQueryKLineWindows(ctx context.Context, symbol string, in
return klineWindows, nil return klineWindows, nil
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/adshao/go-binance"
"github.com/valyala/fastjson" "github.com/valyala/fastjson"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
@ -99,7 +100,7 @@ func (e *ExecutionReportEvent) Trade() (*types.Trade, error) {
Price: util.MustParseFloat(e.LastExecutedPrice), Price: util.MustParseFloat(e.LastExecutedPrice),
Quantity: util.MustParseFloat(e.LastExecutedQuantity), Quantity: util.MustParseFloat(e.LastExecutedQuantity),
QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity), QuoteQuantity: util.MustParseFloat(e.LastQuoteAssetTransactedQuantity),
Side: e.Side, Side: toGlobalSideType(binance.SideType(e.Side)),
IsBuyer: e.Side == "BUY", IsBuyer: e.Side == "BUY",
IsMaker: e.IsMaker, IsMaker: e.IsMaker,
Time: tt, Time: tt,

217
pkg/exchange/max/convert.go Normal file
View File

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

View File

@ -2,11 +2,9 @@ package max
import ( import (
"context" "context"
"fmt"
"strconv"
"strings"
"time" "time"
"github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -46,8 +44,10 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
markets := types.MarketMap{} markets := types.MarketMap{}
for _, m := range remoteMarkets { for _, m := range remoteMarkets {
symbol := toGlobalSymbol(m.ID)
market := types.Market{ market := types.Market{
Symbol: toGlobalSymbol(m.ID), Symbol: symbol,
PricePrecision: m.QuoteUnitPrecision, PricePrecision: m.QuoteUnitPrecision,
VolumePrecision: m.BaseUnitPrecision, VolumePrecision: m.BaseUnitPrecision,
QuoteCurrency: toGlobalCurrency(m.QuoteUnit), QuoteCurrency: toGlobalCurrency(m.QuoteUnit),
@ -62,7 +62,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
TickSize: 0.001, TickSize: 0.001,
} }
markets[m.ID] = market markets[symbol] = market
} }
return markets, nil return markets, nil
@ -72,18 +72,42 @@ func (e *Exchange) NewStream() types.Stream {
return NewStream(e.key, e.secret) return NewStream(e.key, e.secret)
} }
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) {
maxOrders, err := e.client.OrderService.Open(toLocalSymbol(symbol), maxapi.QueryOrderOptions{})
if err != nil {
return orders, err
}
for _, maxOrder := range maxOrders {
order, err := toGlobalOrder(maxOrder)
if err != nil {
return orders, err
}
orders = append(orders, *order)
}
return orders, err
}
func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) error {
for _, order := range orders {
orderType, err := toLocalOrderType(order.Type) orderType, err := toLocalOrderType(order.Type)
if err != nil { if err != nil {
return err return err
} }
clientOrderID := uuid.New().String()
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). ClientOrderID(clientOrderID).
Price(order.PriceString) Volume(order.QuantityString)
if len(order.PriceString) > 0 {
req.Price(order.PriceString)
}
retOrder, err := req.Do(ctx) retOrder, err := req.Do(ctx)
if err != nil { if err != nil {
@ -91,7 +115,9 @@ func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) err
} }
logger.Infof("order created: %+v", retOrder) logger.Infof("order created: %+v", retOrder)
return err }
return nil
} }
// PlatformFeeCurrency // PlatformFeeCurrency
@ -230,7 +256,7 @@ func (e *Exchange) QueryDepositHistory(ctx context.Context, asset string, since,
Address: "", // not supported Address: "", // not supported
AddressTag: "", // not supported AddressTag: "", // not supported
TransactionID: d.TxID, 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 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) { func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) {
accounts, err := e.client.AccountService.Accounts() accounts, err := e.client.AccountService.Accounts()
if err != nil { if err != nil {
@ -301,7 +306,7 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
} }
for _, t := range remoteTrades { for _, t := range remoteTrades {
localTrade, err := convertRemoteTrade(t) localTrade, err := toGlobalTrade(t)
if err != nil { if err != nil {
logger.WithError(err).Errorf("can not convert trade: %+v", t) logger.WithError(err).Errorf("can not convert trade: %+v", t)
continue 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 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
}

View File

@ -23,6 +23,8 @@ const (
OrderStateCancel = OrderState("cancel") OrderStateCancel = OrderState("cancel")
OrderStateWait = OrderState("wait") OrderStateWait = OrderState("wait")
OrderStateConvert = OrderState("convert") OrderStateConvert = OrderState("convert")
OrderStateFinalizing = OrderState("finalizing")
OrderStateFailed = OrderState("failed")
) )
type OrderType string type OrderType string
@ -31,8 +33,14 @@ type OrderType string
const ( const (
OrderTypeMarket = OrderType("market") OrderTypeMarket = OrderType("market")
OrderTypeLimit = OrderType("limit") OrderTypeLimit = OrderType("limit")
OrderTypeStopLimit = OrderType("stop_limit")
OrderTypeStopMarket = OrderType("stop_market")
) )
type QueryOrderOptions struct {
GroupID int
}
// OrderService manages the Order endpoint. // OrderService manages the Order endpoint.
type OrderService struct { type OrderService struct {
client *RestClient client *RestClient
@ -42,10 +50,10 @@ type OrderService struct {
type Order struct { type Order struct {
ID uint64 `json:"id,omitempty" db:"exchange_id"` ID uint64 `json:"id,omitempty" db:"exchange_id"`
Side string `json:"side" db:"side"` Side string `json:"side" db:"side"`
OrderType string `json:"ord_type,omitempty" db:"order_type"` OrderType OrderType `json:"ord_type,omitempty" db:"order_type"`
Price string `json:"price" db:"price"` Price string `json:"price" db:"price"`
AveragePrice string `json:"avg_price,omitempty" db:"average_price"` AveragePrice string `json:"avg_price,omitempty" db:"average_price"`
State string `json:"state,omitempty" db:"state"` State OrderState `json:"state,omitempty" db:"state"`
Market string `json:"market,omitempty" db:"market"` Market string `json:"market,omitempty" db:"market"`
Volume string `json:"volume" db:"volume"` Volume string `json:"volume" db:"volume"`
RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"` RemainingVolume string `json:"remaining_volume,omitempty" db:"remaining_volume"`
@ -58,6 +66,36 @@ type Order struct {
InsertedAt time.Time `json:"-" db:"inserted_at"` 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. // All returns all orders for the authenticated account.
func (s *OrderService) All(market string, limit, page int, states ...OrderState) ([]Order, error) { func (s *OrderService) All(market string, limit, page int, states ...OrderState) ([]Order, error) {
payload := map[string]interface{}{ payload := map[string]interface{}{
@ -281,12 +319,12 @@ func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest {
} }
type CreateOrderRequestParams struct { type CreateOrderRequestParams struct {
PrivateRequestParams *PrivateRequestParams
Market string `json:"market"` Market string `json:"market"`
Volume string `json:"volume"` Volume string `json:"volume"`
Price string `json:"price"` Price string `json:"price,omitempty"`
StopPrice string `json:"stop_price"` StopPrice string `json:"stop_price,omitempty"`
Side string `json:"side"` Side string `json:"side"`
OrderType string `json:"ord_type"` OrderType string `json:"ord_type"`
ClientOrderID string `json:"client_oid,omitempty"` 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) { 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 { if err != nil {
return order, errors.Wrapf(err, "order create error") return order, errors.Wrapf(err, "order create error")
} }

View File

@ -174,6 +174,8 @@ func (e *BookEvent) Time() time.Time {
} }
func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) { func (e *BookEvent) OrderBook() (snapshot types.OrderBook, err error) {
snapshot.Symbol = strings.ToUpper(e.Market)
for _, bid := range e.Bids { for _, bid := range e.Bids {
pv, err := bid.PriceVolumePair() pv, err := bid.PriceVolumePair()
if err != nil { if err != nil {

View File

@ -250,7 +250,6 @@ func getPrivateRequestParamsObject(v interface{}) (*PrivateRequestParams, error)
vt = vt.Elem() vt = vt.Elem()
} }
if vt.Kind() != reflect.Struct { if vt.Kind() != reflect.Struct {
return nil, errors.New("reflect error: given object is not a struct" + vt.Kind().String()) return nil, errors.New("reflect error: given object is not a struct" + vt.Kind().String())
} }

View File

@ -49,6 +49,8 @@ func NewStream(key, secret string) *Stream {
return return
} }
newbook.Symbol = toGlobalSymbol(e.Market)
switch e.Event { switch e.Event {
case "snapshot": case "snapshot":
stream.EmitBookSnapshot(newbook) 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) { 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 { func (s *Stream) Connect(ctx context.Context) error {

View File

@ -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, Symbol: kline.Symbol,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,

View File

@ -37,7 +37,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s
} }
_ = quoteBalance _ = quoteBalance
err := orderExecutor.SubmitOrder(ctx, types.SubmitOrder{ err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: kline.Symbol, Symbol: kline.Symbol,
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
Type: types.OrderTypeMarket, Type: types.OrderTypeMarket,

View File

@ -45,6 +45,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s
ticker := time.NewTicker(1 * time.Minute) ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop() defer ticker.Stop()
s.update(orderExecutor)
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -52,10 +54,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor types.OrderExecutor, s
case <-s.book.C: case <-s.book.C:
s.book.C.Drain(2*time.Second, 5*time.Second) s.book.C.Drain(2*time.Second, 5*time.Second)
s.update() s.update(orderExecutor)
case <-ticker.C: 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 return nil
} }
func (s *Strategy) update() { func (s *Strategy) update(orderExecutor types.OrderExecutor) {
switch s.Side { switch s.Side {
case "buy": case "buy":
s.updateOrders(types.SideTypeBuy) s.updateOrders(orderExecutor, types.SideTypeBuy)
case "sell": case "sell":
s.updateOrders(types.SideTypeSell) s.updateOrders(orderExecutor, types.SideTypeSell)
case "both": case "both":
s.updateOrders(types.SideTypeBuy) s.updateOrders(orderExecutor, types.SideTypeBuy)
s.updateOrders(types.SideTypeSell) 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() book := s.book.Copy()
var pvs types.PriceVolumeSlice var pvs types.PriceVolumeSlice
@ -118,6 +123,8 @@ func (s *Strategy) updateOrders(side types.SideType) {
return return
} }
log.Infof("placing order behind volume: %f", s.BehindVolume.Float64())
index := pvs.IndexByVolumeDepth(s.BehindVolume) index := pvs.IndexByVolumeDepth(s.BehindVolume)
if index == -1 { if index == -1 {
// do not place orders // do not place orders
@ -132,6 +139,10 @@ func (s *Strategy) updateOrders(side types.SideType) {
return return
} }
log.Infof("submitting %d orders", len(orders)) 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) { 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, 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 { if len(orders) >= numOrders {
break break
@ -175,7 +186,7 @@ func (s *Strategy) generateOrders(symbol string, side types.SideType, price, pri
price = price + priceTick price = price + priceTick
declog := math.Log10(math.Abs(priceTick.Float64())) declog := math.Log10(math.Abs(priceTick.Float64()))
expBase += fixedpoint.NewFromFloat(math.Pow10(-int(declog)) * 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 return orders

View File

@ -54,7 +54,9 @@ type Exchange interface {
QueryWithdrawHistory(ctx context.Context, asset string, since, until time.Time) (allWithdraws []Withdraw, err error) 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 { type TradeQueryOptions struct {

View File

@ -26,8 +26,7 @@ type Market struct {
TickSize float64 TickSize float64
} }
func (m Market) FormatPrice(val float64) string { func (m Market) FormatPriceCurrency(val float64) string {
switch m.QuoteCurrency { switch m.QuoteCurrency {
case "USD", "USDT": 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) return strconv.FormatFloat(val, 'f', m.PricePrecision, 64)
} }
func (m Market) FormatVolume(val float64) string { 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) return strconv.FormatFloat(val, 'f', m.VolumePrecision, 64)
} }

View File

@ -1,7 +1,6 @@
package types package types
import ( import (
"github.com/adshao/go-binance"
"github.com/slack-go/slack" "github.com/slack-go/slack"
) )
@ -11,12 +10,33 @@ type OrderType string
const ( const (
OrderTypeLimit OrderType = "LIMIT" OrderTypeLimit OrderType = "LIMIT"
OrderTypeMarket OrderType = "MARKET" 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 { type SubmitOrder struct {
Symbol string Symbol string
Side SideType Side SideType
Type OrderType Type OrderType
Quantity float64 Quantity float64
Price float64 Price float64
@ -25,7 +45,7 @@ type SubmitOrder struct {
PriceString string PriceString string
QuantityString string QuantityString string
TimeInForce binance.TimeInForceType TimeInForce string `json:"timeInForce"` // GTC, IOC, FOK
} }
func (o *SubmitOrder) SlackAttachment() slack.Attachment { func (o *SubmitOrder) SlackAttachment() slack.Attachment {

View File

@ -6,12 +6,14 @@ type SideType string
const ( const (
SideTypeBuy = SideType("BUY") SideTypeBuy = SideType("BUY")
SideTypeSell = SideType("SELL") SideTypeSell = SideType("SELL")
SideTypeSelf = SideType("SELF")
) )
func (side SideType) Color() string { func (side SideType) Color() string {
if side == SideTypeBuy { if side == SideTypeBuy {
return Green return Green
} }
if side == SideTypeSell { if side == SideTypeSell {
return Red return Red
} }

View File

@ -21,7 +21,7 @@ type Trade struct {
QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"` QuoteQuantity float64 `json:"quoteQuantity" db:"quote_quantity"`
Symbol string `json:"symbol" db:"symbol"` 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"` IsBuyer bool `json:"isBuyer" db:"is_buyer"`
IsMaker bool `json:"isMaker" db:"is_maker"` IsMaker bool `json:"isMaker" db:"is_maker"`
Time time.Time `json:"tradedAt" db:"traded_at"` Time time.Time `json:"tradedAt" db:"traded_at"`

View File

@ -3,10 +3,10 @@ package types
import "context" import "context"
type OrderExecutor interface { type OrderExecutor interface {
SubmitOrder(ctx context.Context, order SubmitOrder) error SubmitOrders(ctx context.Context, orders ...SubmitOrder) error
} }
type OrderExecutionRouter interface { type OrderExecutionRouter interface {
// SubmitOrderTo submit order to a specific exchange session // 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
} }