Skip to content

Commit 69e394f

Browse files
authored
Merge pull request #2102 from c9s/c9s/tradingdesk/slack-blocks
FEATURE: [tradingdesk] add slack blocks and fix ClosePosition conversion
2 parents 17503e5 + 0602a79 commit 69e394f

File tree

5 files changed

+171
-17
lines changed

5 files changed

+171
-17
lines changed

config/tradingdesk.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
---
2+
notifications:
3+
slack:
4+
defaultChannel: "dev-bbgo"
5+
errorChannel: "bbgo-error"
6+
27
sessions:
38
binance:
49
exchange: binance

pkg/exchange/binance/futures.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd
218218
Type: response.Type,
219219
Side: response.Side,
220220
ReduceOnly: response.ReduceOnly,
221+
ClosePosition: response.ClosePosition,
221222
}, false)
222223

223224
return createdOrder, err

pkg/notifier/slacknotifier/slack.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type SlackAttachmentCreator interface {
3535
}
3636

3737
type SlackBlocksCreator interface {
38-
SlackBlocks() slack.Blocks
38+
SlackBlocks() []slack.Block
3939
}
4040

4141
type notifyTask struct {
@@ -516,6 +516,9 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}
516516
case *slack.Attachment:
517517
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{*a}, slackAttachments...)...))
518518

519+
case SlackBlocksCreator:
520+
opts = append(opts, slack.MsgOptionBlocks(a.SlackBlocks()...))
521+
519522
case SlackAttachmentCreator:
520523
// convert object to slack attachment (if supported)
521524
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...))

pkg/strategy/tradingdesk/manager.go

Lines changed: 121 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
"github.com/sirupsen/logrus"
11+
"github.com/slack-go/slack"
1112

1213
"github.com/c9s/bbgo/pkg/bbgo"
1314
"github.com/c9s/bbgo/pkg/fixedpoint"
@@ -151,11 +152,9 @@ func (m *TradingManager) OpenPosition(ctx context.Context, params OpenPositionPa
151152
}
152153
}
153154

154-
switch strings.ToUpper(string(params.Side)) {
155-
case "LONG":
156-
params.Side = types.SideTypeBuy
157-
case "SHORT":
158-
params.Side = types.SideTypeSell
155+
params.Side = normalizeSide(string(params.Side))
156+
if params.Side == "" {
157+
return fmt.Errorf("invalid side: %s", params.Side)
159158
}
160159

161160
// get current price for validation
@@ -258,7 +257,7 @@ func (m *TradingManager) OpenPosition(ctx context.Context, params OpenPositionPa
258257
return fmt.Errorf("failed to submit take profit orders for %s: %w", params.Symbol, err)
259258
}
260259

261-
m.strategy.logger.Infof("created take loss orders: %+v", takeProfitOrders)
260+
m.strategy.logger.Infof("created take profit orders: %+v", takeProfitOrders)
262261
m.TakeProfitOrders = takeProfitOrders
263262
}
264263

@@ -517,3 +516,119 @@ func fallbackStopLossTakeProfit(
517516
}
518517
return stopLoss, takeProfit, nil
519518
}
519+
520+
// extractClosePositionPrice extracts the StopPrice of the first order with ClosePosition set to true.
521+
func extractClosePositionPrice(orders types.OrderSlice) fixedpoint.Value {
522+
for _, order := range orders {
523+
if order.ClosePosition {
524+
return order.StopPrice
525+
}
526+
}
527+
528+
return fixedpoint.Zero
529+
}
530+
531+
// SlackBlocks generates Slack message blocks with position details, take profit, stop loss prices, and unrealized profit.
532+
func (m *TradingManager) SlackBlocks() []slack.Block {
533+
var blocks []slack.Block
534+
535+
// Query the latest price
536+
ticker, err := m.session.Exchange.QueryTicker(context.Background(), m.Position.Symbol)
537+
var currentPrice fixedpoint.Value
538+
if err != nil {
539+
m.logger.WithError(err).Errorf("failed to query ticker for %s", m.Position.Symbol)
540+
} else {
541+
currentPrice = ticker.GetValidPrice()
542+
}
543+
544+
// Add position details
545+
positionDetails := fmt.Sprintf("*TradingManager %s Position Details:*\n- Side: `%s`\n - Entry Price: `%s`\n - Size: `%s` (`%s` in %s) ",
546+
m.market.Symbol,
547+
m.Position.Side(),
548+
m.Position.AverageCost.String(),
549+
m.Position.GetBase().String(),
550+
m.Position.AverageCost.Mul(m.Position.GetBase().Abs()),
551+
m.market.QuoteCurrency,
552+
)
553+
554+
if !currentPrice.IsZero() {
555+
positionDetails += fmt.Sprintf("\n - Current Price: `%s`", currentPrice.String())
556+
}
557+
558+
positionSection := slack.NewSectionBlock(
559+
slack.NewTextBlockObject(
560+
slack.MarkdownType,
561+
positionDetails,
562+
false,
563+
false,
564+
),
565+
nil,
566+
nil,
567+
)
568+
blocks = append(blocks, positionSection)
569+
570+
// Add unrealized profit
571+
if !currentPrice.IsZero() {
572+
unrealizedProfit := m.Position.UnrealizedProfit(currentPrice)
573+
profitText := fmt.Sprintf("*Unrealized Profit:* `%s`", unrealizedProfit.String())
574+
575+
// Add ROI to the Slack blocks
576+
if !currentPrice.IsZero() {
577+
roi := m.Position.ROI(currentPrice)
578+
profitText += fmt.Sprintf(" / *ROI*: `%s`", roi.FormatPercentage(2))
579+
}
580+
581+
profitSection := slack.NewSectionBlock(
582+
slack.NewTextBlockObject(
583+
slack.MarkdownType,
584+
profitText,
585+
false,
586+
false,
587+
),
588+
nil,
589+
nil,
590+
)
591+
blocks = append(blocks, profitSection)
592+
}
593+
594+
// Add take profit and stop loss prices
595+
takeProfitPrice := extractClosePositionPrice(m.TakeProfitOrders)
596+
stopLossPrice := extractClosePositionPrice(m.StopLossOrders)
597+
if !takeProfitPrice.IsZero() || !stopLossPrice.IsZero() {
598+
priceSection := slack.NewSectionBlock(
599+
slack.NewTextBlockObject(
600+
slack.MarkdownType,
601+
fmt.Sprintf("*Take Profit Price:* %s / *Stop Loss Price:* %s", takeProfitPrice.String(), stopLossPrice.String()),
602+
false,
603+
false,
604+
),
605+
nil,
606+
nil,
607+
)
608+
blocks = append(blocks, priceSection)
609+
}
610+
611+
// Add footer with last updated time
612+
if !m.Position.OpenedAt.IsZero() {
613+
footerText := fmt.Sprintf("Opened at: %s", m.Position.OpenedAt.Format(time.RFC1123))
614+
footerSection := slack.NewContextBlock(
615+
"footer",
616+
slack.NewTextBlockObject(slack.MarkdownType, footerText, false, false),
617+
)
618+
blocks = append(blocks, footerSection)
619+
}
620+
621+
return blocks
622+
}
623+
624+
// normalizeSide converts a string representation of a side (e.g., "LONG", "BUY") to its corresponding types.SideType.
625+
func normalizeSide(side string) types.SideType {
626+
switch strings.ToUpper(side) {
627+
case "LONG", "BUY":
628+
return types.SideTypeBuy
629+
case "SHORT", "SELL":
630+
return types.SideTypeSell
631+
default:
632+
return ""
633+
}
634+
}

pkg/strategy/tradingdesk/strategy.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -158,21 +158,46 @@ func (s *Strategy) loadFromPositionRisks(ctx context.Context) error {
158158
}
159159

160160
// Restore take profit and stop loss orders
161-
for _, order := range orders {
162-
switch order.Type {
163-
case types.OrderTypeTakeProfitMarket:
164-
m.TakeProfitOrders = append(m.TakeProfitOrders, order)
165-
case types.OrderTypeStopMarket:
166-
m.StopLossOrders = append(m.StopLossOrders, order)
161+
// Add the order to the active order book of the order executor
162+
m.orderExecutor.ActiveMakerOrders().Add(orders...)
163+
m.orderExecutor.OrderStore().Add(orders...)
164+
165+
switch m.Position.Side() {
166+
167+
case types.SideTypeBuy:
168+
for _, order := range orders {
169+
switch order.Type {
170+
case types.OrderTypeTakeProfitMarket:
171+
m.TakeProfitOrders.Add(order)
172+
case types.OrderTypeStopMarket:
173+
if order.StopPrice.Compare(m.Position.AverageCost) < 0 {
174+
m.StopLossOrders.Add(order)
175+
} else {
176+
m.TakeProfitOrders.Add(order)
177+
}
178+
}
179+
}
180+
181+
case types.SideTypeSell:
182+
for _, order := range orders {
183+
switch order.Type {
184+
case types.OrderTypeTakeProfitMarket:
185+
m.TakeProfitOrders.Add(order)
186+
case types.OrderTypeStopMarket:
187+
if order.StopPrice.Compare(m.Position.AverageCost) > 0 {
188+
m.StopLossOrders.Add(order)
189+
} else {
190+
m.TakeProfitOrders.Add(order)
191+
}
192+
}
167193
}
168194

169-
// Add the order to the active order book of the order executor
170-
m.orderExecutor.ActiveMakerOrders().Add(order)
171-
m.orderExecutor.OrderStore().Add(order)
172195
}
173196

174197
m.logger.Infof("updated position: %+v", m.Position)
175198
bbgo.Notify("TradingManager %s loaded position", m.Position.Symbol, m.Position)
199+
200+
bbgo.Notify(m)
176201
}
177202

178203
return nil
@@ -258,7 +283,12 @@ func (s *Strategy) OpenPosition(ctx context.Context, param OpenPositionParams) e
258283
return err
259284
}
260285

261-
return m.OpenPosition(ctx, param)
286+
if err := m.OpenPosition(ctx, param); err != nil {
287+
return fmt.Errorf("open position error: %w", err)
288+
}
289+
290+
bbgo.Notify(m)
291+
return nil
262292
}
263293

264294
func (s *Strategy) HasPosition() bool {

0 commit comments

Comments
 (0)