diff --git a/pkg/exchange/binance.go b/pkg/exchange/binance.go index a732be16..fa7c4092 100644 --- a/pkg/exchange/binance.go +++ b/pkg/exchange/binance.go @@ -329,6 +329,38 @@ func (b *Binance) OrderMarket(side model.SideType, symbol string, quantity float }, nil } +func (b *Binance) OrderMarketQuote(side model.SideType, symbol string, quantity float64) (model.Order, error) { + err := b.validate(side, model.OrderTypeMarket, symbol, quantity, nil) + if err != nil { + return model.Order{}, err + } + + order, err := b.client.NewCreateOrderService(). + Symbol(symbol). + Type(binance.OrderTypeMarket). + Side(binance.SideType(side)). + QuoteOrderQty(fmt.Sprintf("%.f", quantity)). + NewOrderRespType(binance.NewOrderRespTypeFULL). + Do(b.ctx) + if err != nil { + return model.Order{}, err + } + + cost, _ := strconv.ParseFloat(order.CummulativeQuoteQuantity, 64) + quantity, _ = strconv.ParseFloat(order.ExecutedQuantity, 64) + return model.Order{ + ExchangeID: order.OrderID, + CreatedAt: time.Unix(0, order.TransactTime*int64(time.Millisecond)), + UpdatedAt: time.Unix(0, order.TransactTime*int64(time.Millisecond)), + Symbol: order.Symbol, + Side: model.SideType(order.Side), + Type: model.OrderType(order.Type), + Status: model.OrderStatusType(order.Status), + Price: cost / quantity, + Quantity: quantity, + }, nil +} + func (b *Binance) Cancel(order model.Order) error { _, err := b.client.NewCancelOrderService(). Symbol(order.Symbol). diff --git a/pkg/exchange/paperwallet.go b/pkg/exchange/paperwallet.go index 6b276438..a8a96db8 100644 --- a/pkg/exchange/paperwallet.go +++ b/pkg/exchange/paperwallet.go @@ -369,6 +369,10 @@ func (p *PaperWallet) OrderMarket(side model.SideType, symbol string, size float return order, nil } +func (p *PaperWallet) OrderMarketQuote(side model.SideType, symbol string, quantity float64) (model.Order, error) { + return p.OrderMarket(side, symbol, quantity/p.lastCandle[symbol].Close) +} + func (p *PaperWallet) Cancel(order model.Order) error { p.Lock() defer p.Unlock() diff --git a/pkg/model/model.go b/pkg/model/model.go index b774866d..f3a2cef6 100644 --- a/pkg/model/model.go +++ b/pkg/model/model.go @@ -112,8 +112,8 @@ type Order struct { } func (o Order) String() string { - return fmt.Sprintf("%s %s | ID: %d, Type: %s - %f x $%f (%s)", - o.Side, o.Symbol, o.ID, o.Type, o.Quantity, o.Price, o.Status) + return fmt.Sprintf("[%s] %s %s | ID: %d, Type: %s, %f x $%f (~$%.f)", + o.Status, o.Side, o.Symbol, o.ID, o.Type, o.Quantity, o.Price, o.Quantity*o.Price) } type Account struct { diff --git a/pkg/notification/telegram.go b/pkg/notification/telegram.go index f04983f2..f73fd00c 100644 --- a/pkg/notification/telegram.go +++ b/pkg/notification/telegram.go @@ -2,6 +2,8 @@ package notification import ( "fmt" + "regexp" + "strconv" "strings" "time" @@ -14,6 +16,11 @@ import ( "github.com/rodrigo-brito/ninjabot/pkg/service" ) +var ( + buyRegexp = regexp.MustCompile(`/buy\s+(?P\w+)\s+(?P[0-9]+(?:\.\d+)?)(?P%)?`) + sellRegexp = regexp.MustCompile(`/sell\s+(?P\w+)\s+(?P[0-9]+(?:\.\d+)?)(?P%)?`) +) + type telegram struct { settings model.Settings orderController *order.Controller @@ -174,8 +181,8 @@ func (t telegram) ProfitHandle(m *tb.Message) { return } - for _, summary := range t.orderController.Results { - _, err := t.client.Send(m.Sender, fmt.Sprintf("`%s`", summary.String())) + for pair, summary := range t.orderController.Results { + _, err := t.client.Send(m.Sender, fmt.Sprintf("*PAIR*: `%s`\n`%s`", pair, summary.String())) if err != nil { log.Error(err) } @@ -183,22 +190,111 @@ func (t telegram) ProfitHandle(m *tb.Message) { } func (t telegram) BuyHandle(m *tb.Message) { - _, err := t.client.Send(m.Sender, "not implemented yet") + match := buyRegexp.FindStringSubmatch(m.Text) + if len(match) == 0 { + _, err := t.client.Send(m.Sender, "Invalid command.\nExamples of usage:\n`/buy BTCUSDT 100`\n\n`/buy BTCUSDT 50%`") + if err != nil { + log.Error(err) + } + return + } + + command := make(map[string]string) + for i, name := range buyRegexp.SubexpNames() { + if i != 0 && name != "" { + command[name] = match[i] + } + } + + pair := strings.ToUpper(command["pair"]) + amount, err := strconv.ParseFloat(command["amount"], 64) if err != nil { - log.Error(err) + t.OrError(err) + return + } else if amount <= 0 { + _, err := t.client.Send(m.Sender, "Invalid amount") + if err != nil { + log.Error(err) + } + return } -} -func (t telegram) StatusHandle(m *tb.Message) { - status := t.orderController.Status() - _, err := t.client.Send(m.Sender, fmt.Sprintf("Status: `%s`", status)) + if command["percent"] != "" { + _, quote, err := t.orderController.Position(pair) + if err != nil { + t.OrError(err) + return + } + + amount = amount * quote / 100.0 + } + + order, err := t.orderController.OrderMarketQuote(model.SideTypeBuy, pair, amount) if err != nil { - log.Error(err) + t.OrError(err) + return } + t.OnOrder(order) } func (t telegram) SellHandle(m *tb.Message) { - _, err := t.client.Send(m.Sender, "not implemented yet") + match := sellRegexp.FindStringSubmatch(m.Text) + if len(match) == 0 { + _, err := t.client.Send(m.Sender, "Invalid command.\nExample of usage:\n`/sell BTCUSDT 100`\n\n`/sell BTCUSDT 50%") + if err != nil { + log.Error(err) + } + return + } + + command := make(map[string]string) + for i, name := range sellRegexp.SubexpNames() { + if i != 0 && name != "" { + command[name] = match[i] + } + } + + pair := strings.ToUpper(command["pair"]) + amount, err := strconv.ParseFloat(command["amount"], 64) + if err != nil { + t.OrError(err) + return + } else if amount <= 0 { + _, err := t.client.Send(m.Sender, "Invalid amount") + if err != nil { + log.Error(err) + } + return + } + + if command["percent"] != "" { + asset, _, err := t.orderController.Position(pair) + if err != nil { + t.OrError(err) + return + } + + amount = amount * asset / 100.0 + order, err := t.orderController.OrderMarket(model.SideTypeSell, pair, amount) + if err != nil { + t.OrError(err) + return + } + t.OnOrder(order) + return + } + + order, err := t.orderController.OrderMarketQuote(model.SideTypeSell, pair, amount) + if err != nil { + t.OrError(err) + return + } + t.OnOrder(order) +} + +func (t telegram) StatusHandle(m *tb.Message) { + status := t.orderController.Status() + _, err := t.client.Send(m.Sender, fmt.Sprintf("Status: `%s`", status)) if err != nil { log.Error(err) } @@ -251,6 +347,7 @@ func (t telegram) OnOrder(order model.Order) { } func (t telegram) OrError(err error) { + log.Error(err) title := "🛑 ERROR" message := fmt.Sprintf("%s\n-----\n%s", title, err) t.Notify(message) diff --git a/pkg/order/controller.go b/pkg/order/controller.go index 96c97d01..47889540 100644 --- a/pkg/order/controller.go +++ b/pkg/order/controller.go @@ -343,6 +343,33 @@ func (c *Controller) OrderLimit(side model.SideType, symbol string, size, limit return order, nil } +func (c *Controller) OrderMarketQuote(side model.SideType, symbol string, amount float64) (model.Order, error) { + c.mtx.Lock() + defer c.mtx.Unlock() + + log.Infof("[ORDER] Creating MARKET %s order for %s", side, symbol) + order, err := c.exchange.OrderMarketQuote(side, symbol, amount) + if err != nil { + log.Errorf("order/controller exchange: %s", err) + return model.Order{}, err + } + + err = c.createOrder(&order) + if err != nil { + log.Errorf("order/controller storage: %s", err) + return model.Order{}, err + } + + // calculate profit + if order.Side == model.SideTypeSell { + c.processTrade(&order) + } + + go c.orderFeed.Publish(order, true) + log.Infof("[ORDER CREATED] %s", order) + return order, err +} + func (c *Controller) OrderMarket(side model.SideType, symbol string, size float64) (model.Order, error) { c.mtx.Lock() defer c.mtx.Unlock() diff --git a/pkg/service/service.go b/pkg/service/service.go index 4738657b..28d4f593 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -25,6 +25,7 @@ type Broker interface { OrderOCO(side model.SideType, symbol string, size, price, stop, stopLimit float64) ([]model.Order, error) OrderLimit(side model.SideType, symbol string, size float64, limit float64) (model.Order, error) OrderMarket(side model.SideType, symbol string, size float64) (model.Order, error) + OrderMarketQuote(side model.SideType, symbol string, quote float64) (model.Order, error) Cancel(model.Order) error }