Skip to content

Commit 50e8fa3

Browse files
committed
feat(telegram): buy and sell commands
1 parent f14b22f commit 50e8fa3

File tree

6 files changed

+173
-12
lines changed

6 files changed

+173
-12
lines changed

pkg/exchange/binance.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,38 @@ func (b *Binance) OrderMarket(side model.SideType, symbol string, quantity float
329329
}, nil
330330
}
331331

332+
func (b *Binance) OrderMarketQuote(side model.SideType, symbol string, quantity float64) (model.Order, error) {
333+
err := b.validate(side, model.OrderTypeMarket, symbol, quantity, nil)
334+
if err != nil {
335+
return model.Order{}, err
336+
}
337+
338+
order, err := b.client.NewCreateOrderService().
339+
Symbol(symbol).
340+
Type(binance.OrderTypeMarket).
341+
Side(binance.SideType(side)).
342+
QuoteOrderQty(fmt.Sprintf("%.f", quantity)).
343+
NewOrderRespType(binance.NewOrderRespTypeFULL).
344+
Do(b.ctx)
345+
if err != nil {
346+
return model.Order{}, err
347+
}
348+
349+
cost, _ := strconv.ParseFloat(order.CummulativeQuoteQuantity, 64)
350+
quantity, _ = strconv.ParseFloat(order.ExecutedQuantity, 64)
351+
return model.Order{
352+
ExchangeID: order.OrderID,
353+
CreatedAt: time.Unix(0, order.TransactTime*int64(time.Millisecond)),
354+
UpdatedAt: time.Unix(0, order.TransactTime*int64(time.Millisecond)),
355+
Symbol: order.Symbol,
356+
Side: model.SideType(order.Side),
357+
Type: model.OrderType(order.Type),
358+
Status: model.OrderStatusType(order.Status),
359+
Price: cost / quantity,
360+
Quantity: quantity,
361+
}, nil
362+
}
363+
332364
func (b *Binance) Cancel(order model.Order) error {
333365
_, err := b.client.NewCancelOrderService().
334366
Symbol(order.Symbol).

pkg/exchange/paperwallet.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,10 @@ func (p *PaperWallet) OrderMarket(side model.SideType, symbol string, size float
369369
return order, nil
370370
}
371371

372+
func (p *PaperWallet) OrderMarketQuote(side model.SideType, symbol string, quantity float64) (model.Order, error) {
373+
return p.OrderMarket(side, symbol, quantity/p.lastCandle[symbol].Close)
374+
}
375+
372376
func (p *PaperWallet) Cancel(order model.Order) error {
373377
p.Lock()
374378
defer p.Unlock()

pkg/model/model.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ type Order struct {
112112
}
113113

114114
func (o Order) String() string {
115-
return fmt.Sprintf("%s %s | ID: %d, Type: %s - %f x $%f (%s)",
116-
o.Side, o.Symbol, o.ID, o.Type, o.Quantity, o.Price, o.Status)
115+
return fmt.Sprintf("[%s] %s %s | ID: %d, Type: %s, %f x $%f (~$%.f)",
116+
o.Status, o.Side, o.Symbol, o.ID, o.Type, o.Quantity, o.Price, o.Quantity*o.Price)
117117
}
118118

119119
type Account struct {

pkg/notification/telegram.go

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package notification
22

33
import (
44
"fmt"
5+
"regexp"
6+
"strconv"
57
"strings"
68
"time"
79

@@ -14,6 +16,11 @@ import (
1416
"github.com/rodrigo-brito/ninjabot/pkg/service"
1517
)
1618

19+
var (
20+
buyRegexp = regexp.MustCompile(`/buy\s+(?P<pair>\w+)\s+(?P<amount>[0-9]+(?:\.\d+)?)(?P<percent>%)?`)
21+
sellRegexp = regexp.MustCompile(`/sell\s+(?P<pair>\w+)\s+(?P<amount>[0-9]+(?:\.\d+)?)(?P<percent>%)?`)
22+
)
23+
1724
type telegram struct {
1825
settings model.Settings
1926
orderController *order.Controller
@@ -174,31 +181,120 @@ func (t telegram) ProfitHandle(m *tb.Message) {
174181
return
175182
}
176183

177-
for _, summary := range t.orderController.Results {
178-
_, err := t.client.Send(m.Sender, fmt.Sprintf("`%s`", summary.String()))
184+
for pair, summary := range t.orderController.Results {
185+
_, err := t.client.Send(m.Sender, fmt.Sprintf("*PAIR*: `%s`\n`%s`", pair, summary.String()))
179186
if err != nil {
180187
log.Error(err)
181188
}
182189
}
183190
}
184191

185192
func (t telegram) BuyHandle(m *tb.Message) {
186-
_, err := t.client.Send(m.Sender, "not implemented yet")
193+
match := buyRegexp.FindStringSubmatch(m.Text)
194+
if len(match) == 0 {
195+
_, err := t.client.Send(m.Sender, "Invalid command.\nExamples of usage:\n`/buy BTCUSDT 100`\n\n`/buy BTCUSDT 50%`")
196+
if err != nil {
197+
log.Error(err)
198+
}
199+
return
200+
}
201+
202+
command := make(map[string]string)
203+
for i, name := range buyRegexp.SubexpNames() {
204+
if i != 0 && name != "" {
205+
command[name] = match[i]
206+
}
207+
}
208+
209+
pair := strings.ToUpper(command["pair"])
210+
amount, err := strconv.ParseFloat(command["amount"], 64)
187211
if err != nil {
188-
log.Error(err)
212+
t.OrError(err)
213+
return
214+
} else if amount <= 0 {
215+
_, err := t.client.Send(m.Sender, "Invalid amount")
216+
if err != nil {
217+
log.Error(err)
218+
}
219+
return
189220
}
190-
}
191221

192-
func (t telegram) StatusHandle(m *tb.Message) {
193-
status := t.orderController.Status()
194-
_, err := t.client.Send(m.Sender, fmt.Sprintf("Status: `%s`", status))
222+
if command["percent"] != "" {
223+
_, quote, err := t.orderController.Position(pair)
224+
if err != nil {
225+
t.OrError(err)
226+
return
227+
}
228+
229+
amount = amount * quote / 100.0
230+
}
231+
232+
order, err := t.orderController.OrderMarketQuote(model.SideTypeBuy, pair, amount)
195233
if err != nil {
196-
log.Error(err)
234+
t.OrError(err)
235+
return
197236
}
237+
t.OnOrder(order)
198238
}
199239

200240
func (t telegram) SellHandle(m *tb.Message) {
201-
_, err := t.client.Send(m.Sender, "not implemented yet")
241+
match := sellRegexp.FindStringSubmatch(m.Text)
242+
if len(match) == 0 {
243+
_, err := t.client.Send(m.Sender, "Invalid command.\nExample of usage:\n`/sell BTCUSDT 100`\n\n`/sell BTCUSDT 50%")
244+
if err != nil {
245+
log.Error(err)
246+
}
247+
return
248+
}
249+
250+
command := make(map[string]string)
251+
for i, name := range sellRegexp.SubexpNames() {
252+
if i != 0 && name != "" {
253+
command[name] = match[i]
254+
}
255+
}
256+
257+
pair := strings.ToUpper(command["pair"])
258+
amount, err := strconv.ParseFloat(command["amount"], 64)
259+
if err != nil {
260+
t.OrError(err)
261+
return
262+
} else if amount <= 0 {
263+
_, err := t.client.Send(m.Sender, "Invalid amount")
264+
if err != nil {
265+
log.Error(err)
266+
}
267+
return
268+
}
269+
270+
if command["percent"] != "" {
271+
asset, _, err := t.orderController.Position(pair)
272+
if err != nil {
273+
t.OrError(err)
274+
return
275+
}
276+
277+
amount = amount * asset / 100.0
278+
order, err := t.orderController.OrderMarket(model.SideTypeSell, pair, amount)
279+
if err != nil {
280+
t.OrError(err)
281+
return
282+
}
283+
t.OnOrder(order)
284+
return
285+
}
286+
287+
order, err := t.orderController.OrderMarketQuote(model.SideTypeSell, pair, amount)
288+
if err != nil {
289+
t.OrError(err)
290+
return
291+
}
292+
t.OnOrder(order)
293+
}
294+
295+
func (t telegram) StatusHandle(m *tb.Message) {
296+
status := t.orderController.Status()
297+
_, err := t.client.Send(m.Sender, fmt.Sprintf("Status: `%s`", status))
202298
if err != nil {
203299
log.Error(err)
204300
}
@@ -251,6 +347,7 @@ func (t telegram) OnOrder(order model.Order) {
251347
}
252348

253349
func (t telegram) OrError(err error) {
350+
log.Error(err)
254351
title := "🛑 ERROR"
255352
message := fmt.Sprintf("%s\n-----\n%s", title, err)
256353
t.Notify(message)

pkg/order/controller.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,33 @@ func (c *Controller) OrderLimit(side model.SideType, symbol string, size, limit
343343
return order, nil
344344
}
345345

346+
func (c *Controller) OrderMarketQuote(side model.SideType, symbol string, amount float64) (model.Order, error) {
347+
c.mtx.Lock()
348+
defer c.mtx.Unlock()
349+
350+
log.Infof("[ORDER] Creating MARKET %s order for %s", side, symbol)
351+
order, err := c.exchange.OrderMarketQuote(side, symbol, amount)
352+
if err != nil {
353+
log.Errorf("order/controller exchange: %s", err)
354+
return model.Order{}, err
355+
}
356+
357+
err = c.createOrder(&order)
358+
if err != nil {
359+
log.Errorf("order/controller storage: %s", err)
360+
return model.Order{}, err
361+
}
362+
363+
// calculate profit
364+
if order.Side == model.SideTypeSell {
365+
c.processTrade(&order)
366+
}
367+
368+
go c.orderFeed.Publish(order, true)
369+
log.Infof("[ORDER CREATED] %s", order)
370+
return order, err
371+
}
372+
346373
func (c *Controller) OrderMarket(side model.SideType, symbol string, size float64) (model.Order, error) {
347374
c.mtx.Lock()
348375
defer c.mtx.Unlock()

pkg/service/service.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type Broker interface {
2525
OrderOCO(side model.SideType, symbol string, size, price, stop, stopLimit float64) ([]model.Order, error)
2626
OrderLimit(side model.SideType, symbol string, size float64, limit float64) (model.Order, error)
2727
OrderMarket(side model.SideType, symbol string, size float64) (model.Order, error)
28+
OrderMarketQuote(side model.SideType, symbol string, quote float64) (model.Order, error)
2829
Cancel(model.Order) error
2930
}
3031

0 commit comments

Comments
 (0)