diff --git a/exchange/binance.go b/exchange/binance.go index a1e626a3..3712371c 100644 --- a/exchange/binance.go +++ b/exchange/binance.go @@ -114,14 +114,17 @@ func (b *Binance) AssetsInfo(pair string) model.AssetInfo { } func (b *Binance) validate(pair string, quantity float64) error { - info, ok := b.assetsInfo[pair] if !ok { return ErrInvalidAsset } if quantity > info.MaxQuantity || quantity < info.MinQuantity { - return fmt.Errorf("%w: min: %f max: %f", ErrInvalidQuantity, info.MinQuantity, info.MaxQuantity) + return &OrderError{ + Err: fmt.Errorf("%w: min: %f max: %f", ErrInvalidQuantity, info.MinQuantity, info.MaxQuantity), + Pair: pair, + Quantity: quantity, + } } return nil diff --git a/exchange/exchange.go b/exchange/exchange.go index 77043488..1e78c746 100644 --- a/exchange/exchange.go +++ b/exchange/exchange.go @@ -38,6 +38,16 @@ type Subscription struct { consumer DataFeedConsumer } +type OrderError struct { + Err error + Pair string + Quantity float64 +} + +func (o *OrderError) Error() string { + return fmt.Sprintf("order error: %v", o.Err) +} + type DataFeedConsumer func(model.Candle) func NewDataFeed(exchange service.Exchange) *DataFeedSubscription { diff --git a/exchange/paperwallet.go b/exchange/paperwallet.go index 8c10939b..1b0dfae8 100644 --- a/exchange/paperwallet.go +++ b/exchange/paperwallet.go @@ -9,11 +9,10 @@ import ( "sync" "time" - "github.com/rodrigo-brito/ninjabot/service" - log "github.com/sirupsen/logrus" "github.com/rodrigo-brito/ninjabot/model" + "github.com/rodrigo-brito/ninjabot/service" ) type assetInfo struct { @@ -217,7 +216,11 @@ func (p *PaperWallet) Summary() { func (p *PaperWallet) lockFunds(asset string, amount float64) error { if value, ok := p.assets[asset]; !ok || value.Free < amount { - return ErrInsufficientFunds + return &OrderError{ + Err: ErrInsufficientFunds, + Pair: asset, + Quantity: amount, + } } p.assets[asset].Free = p.assets[asset].Free - amount p.assets[asset].Lock = p.assets[asset].Lock + amount @@ -447,7 +450,11 @@ func (p *PaperWallet) createOrderMarket(side model.SideType, pair string, size f asset, quote := SplitAssetQuote(pair) if side == model.SideTypeSell { if value, ok := p.assets[asset]; !ok || value.Free < size { - return model.Order{}, ErrInsufficientFunds + return model.Order{}, &OrderError{ + Err: ErrInsufficientFunds, + Pair: pair, + Quantity: size, + } } if _, ok := p.assets[quote]; !ok { p.assets[quote] = &assetInfo{} @@ -456,7 +463,11 @@ func (p *PaperWallet) createOrderMarket(side model.SideType, pair string, size f p.assets[quote].Free = p.assets[quote].Free + p.lastCandle[pair].Close*size } else { if value, ok := p.assets[quote]; !ok || value.Free < size*p.lastCandle[pair].Close { - return model.Order{}, ErrInsufficientFunds + return model.Order{}, &OrderError{ + Err: ErrInsufficientFunds, + Pair: pair, + Quantity: size, + } } if _, ok := p.assets[asset]; !ok { p.assets[asset] = &assetInfo{} diff --git a/exchange/paperwallet_test.go b/exchange/paperwallet_test.go index dc70dace..60910ef6 100644 --- a/exchange/paperwallet_test.go +++ b/exchange/paperwallet_test.go @@ -35,7 +35,10 @@ func TestPaperWallet_OrderLimit(t *testing.T) { // try to buy again without funds order, err = wallet.CreateOrderLimit(model.SideTypeBuy, "BTCUSDT", 1, 100) require.Empty(t, order) - require.Equal(t, ErrInsufficientFunds, err) + require.Equal(t, &OrderError{ + Err: ErrInsufficientFunds, + Pair: "USDT", + Quantity: 100}, err) // try to sell and profit 100 USDT order, err = wallet.CreateOrderLimit(model.SideTypeSell, "BTCUSDT", 1, 200) @@ -138,7 +141,10 @@ func TestPaperWallet_OrderMarket(t *testing.T) { // insufficient funds order, err = wallet.CreateOrderMarket(model.SideTypeBuy, "BTCUSDT", 100) - require.Equal(t, ErrInsufficientFunds, err) + require.Equal(t, &OrderError{ + Err: ErrInsufficientFunds, + Pair: "BTCUSDT", + Quantity: 100}, err) require.Empty(t, order) // sell @@ -177,7 +183,10 @@ func TestPaperWallet_OrderOCO(t *testing.T) { // insufficient funds orders, err = wallet.CreateOrderOCO(model.SideTypeSell, "BTCUSDT", 1, 100, 40, 39) - require.Equal(t, ErrInsufficientFunds, err) + require.Equal(t, &OrderError{ + Err: ErrInsufficientFunds, + Pair: "BTC", + Quantity: 1}, err) require.Nil(t, orders) // execute stop and cancel target diff --git a/ninjabot.go b/ninjabot.go index 5ccdcd3b..fafdae06 100644 --- a/ninjabot.go +++ b/ninjabot.go @@ -276,7 +276,7 @@ func (n *NinjaBot) backtestCandles() { } if err := progressBar.Add(1); err != nil { - log.Warningf("update progresbar fail: %s", err.Error()) + log.Warningf("update progresbar fail: %v", err) } } } diff --git a/notification/telegram.go b/notification/telegram.go index e56cd3d2..158e742f 100644 --- a/notification/telegram.go +++ b/notification/telegram.go @@ -1,6 +1,7 @@ package notification import ( + "errors" "fmt" "regexp" "strconv" @@ -250,7 +251,6 @@ func (t telegram) BuyHandle(m *tb.Message) { order, err := t.orderController.CreateOrderMarketQuote(model.SideTypeBuy, pair, amount) if err != nil { - log.Error(err) return } log.Info("[TELEGRAM]: BUY ORDER CREATED: ", order) @@ -290,15 +290,12 @@ func (t telegram) SellHandle(m *tb.Message) { if command["percent"] != "" { asset, _, err := t.orderController.Position(pair) if err != nil { - log.Error(err) - t.OnError(err) return } amount = amount * asset / 100.0 order, err := t.orderController.CreateOrderMarket(model.SideTypeSell, pair, amount) if err != nil { - log.Error(err) return } log.Info("[TELEGRAM]: SELL ORDER CREATED: ", order) @@ -307,7 +304,6 @@ func (t telegram) SellHandle(m *tb.Message) { order, err := t.orderController.CreateOrderMarketQuote(model.SideTypeSell, pair, amount) if err != nil { - log.Error(err) return } log.Info("[TELEGRAM]: SELL ORDER CREATED: ", order) @@ -369,6 +365,18 @@ func (t telegram) OnOrder(order model.Order) { func (t telegram) OnError(err error) { title := "🛑 ERROR" - message := fmt.Sprintf("%s\n-----\n%s", title, err) - t.Notify(message) + + var orderError *exchange.OrderError + if errors.As(err, &orderError) { + message := fmt.Sprintf(`%s + ----- + Pair: %s + Quantity: %.4f + ----- + %s`, title, orderError.Pair, orderError.Quantity, orderError.Err) + t.Notify(message) + return + } + + t.Notify(fmt.Sprintf("%s\n-----\n%s", title, err)) } diff --git a/order/controller.go b/order/controller.go index 9f509d7c..0615db04 100644 --- a/order/controller.go +++ b/order/controller.go @@ -198,14 +198,14 @@ func (c *Controller) processTrade(order *model.Order) { // register order volume c.Results[order.Pair].Volume += order.Price * order.Quantity - // calculate profit only for sell orders + // calculate profit only to sell orders if order.Side != model.SideTypeSell { return } profitValue, profit, err := c.calculateProfit(order) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return } @@ -231,7 +231,7 @@ func (c *Controller) updateOrders() { model.OrderStatusTypePendingCancel, )) if err != nil { - c.notifyError(fmt.Errorf("orderController/start: %s", err)) + c.notifyError(err) c.mtx.Unlock() return } @@ -253,7 +253,7 @@ func (c *Controller) updateOrders() { excOrder.ID = order.ID err = c.storage.UpdateOrder(&excOrder) if err != nil { - c.notifyError(fmt.Errorf("orderControler/update: %s", err)) + c.notifyError(err) continue } @@ -331,14 +331,14 @@ func (c *Controller) CreateOrderOCO(side model.SideType, pair string, size, pric log.Infof("[ORDER] Creating OCO order for %s", pair) orders, err := c.exchange.CreateOrderOCO(side, pair, size, price, stop, stopLimit) if err != nil { - c.notifyError(fmt.Errorf("order/controller exchange: %s", err)) + c.notifyError(err) return nil, err } for i := range orders { err := c.storage.CreateOrder(&orders[i]) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return nil, err } go c.orderFeed.Publish(orders[i], true) @@ -354,13 +354,13 @@ func (c *Controller) CreateOrderLimit(side model.SideType, pair string, size, li log.Infof("[ORDER] Creating LIMIT %s order for %s", side, pair) order, err := c.exchange.CreateOrderLimit(side, pair, size, limit) if err != nil { - c.notifyError(fmt.Errorf("order/controller exchange: %s", err)) + c.notifyError(err) return model.Order{}, err } err = c.storage.CreateOrder(&order) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return model.Order{}, err } go c.orderFeed.Publish(order, true) @@ -375,13 +375,13 @@ func (c *Controller) CreateOrderMarketQuote(side model.SideType, pair string, am log.Infof("[ORDER] Creating MARKET %s order for %s", side, pair) order, err := c.exchange.CreateOrderMarketQuote(side, pair, amount) if err != nil { - c.notifyError(fmt.Errorf("order/controller exchange: %s", err)) + c.notifyError(err) return model.Order{}, err } err = c.storage.CreateOrder(&order) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return model.Order{}, err } @@ -399,13 +399,13 @@ func (c *Controller) CreateOrderMarket(side model.SideType, pair string, size fl log.Infof("[ORDER] Creating MARKET %s order for %s", side, pair) order, err := c.exchange.CreateOrderMarket(side, pair, size) if err != nil { - c.notifyError(fmt.Errorf("order/controller exchange: %s", err)) + c.notifyError(err) return model.Order{}, err } err = c.storage.CreateOrder(&order) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return model.Order{}, err } @@ -429,7 +429,7 @@ func (c *Controller) Cancel(order model.Order) error { order.Status = model.OrderStatusTypePendingCancel err = c.storage.UpdateOrder(&order) if err != nil { - c.notifyError(fmt.Errorf("order/controller storage: %s", err)) + c.notifyError(err) return err } log.Infof("[ORDER CANCELED] %s", order)