|
8 | 8 | "time" |
9 | 9 |
|
10 | 10 | "github.com/sirupsen/logrus" |
| 11 | + "github.com/slack-go/slack" |
11 | 12 |
|
12 | 13 | "github.com/c9s/bbgo/pkg/bbgo" |
13 | 14 | "github.com/c9s/bbgo/pkg/fixedpoint" |
@@ -151,11 +152,9 @@ func (m *TradingManager) OpenPosition(ctx context.Context, params OpenPositionPa |
151 | 152 | } |
152 | 153 | } |
153 | 154 |
|
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) |
159 | 158 | } |
160 | 159 |
|
161 | 160 | // get current price for validation |
@@ -258,7 +257,7 @@ func (m *TradingManager) OpenPosition(ctx context.Context, params OpenPositionPa |
258 | 257 | return fmt.Errorf("failed to submit take profit orders for %s: %w", params.Symbol, err) |
259 | 258 | } |
260 | 259 |
|
261 | | - m.strategy.logger.Infof("created take loss orders: %+v", takeProfitOrders) |
| 260 | + m.strategy.logger.Infof("created take profit orders: %+v", takeProfitOrders) |
262 | 261 | m.TakeProfitOrders = takeProfitOrders |
263 | 262 | } |
264 | 263 |
|
@@ -517,3 +516,119 @@ func fallbackStopLossTakeProfit( |
517 | 516 | } |
518 | 517 | return stopLoss, takeProfit, nil |
519 | 518 | } |
| 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 | +} |
0 commit comments