128 lines
3.1 KiB
Go
128 lines
3.1 KiB
Go
package trader
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"time"
|
|
|
|
"github.com/pheinrich/aitrade/pkg/app/client"
|
|
"github.com/pheinrich/aitrade/pkg/db"
|
|
"github.com/pheinrich/aitrade/pkg/model"
|
|
)
|
|
|
|
type StopLossManager struct {
|
|
client *client.IBClient
|
|
tradeRepo *db.TradeRepository
|
|
logger *slog.Logger
|
|
enabled bool
|
|
}
|
|
|
|
func NewStopLossManager(client *client.IBClient, tradeRepo *db.TradeRepository, enabled bool, logger *slog.Logger) *StopLossManager {
|
|
return &StopLossManager{
|
|
client: client,
|
|
tradeRepo: tradeRepo,
|
|
logger: logger,
|
|
enabled: enabled,
|
|
}
|
|
}
|
|
|
|
func (s *StopLossManager) CreateStopLoss(ctx context.Context, trade *model.Trade, stopLossPercent float64) error {
|
|
if !s.enabled || trade.ExecutedPrice == nil {
|
|
return nil
|
|
}
|
|
|
|
executedPrice := *trade.ExecutedPrice
|
|
stopPrice := executedPrice * (1.0 - stopLossPercent/100.0)
|
|
|
|
s.logger.Info("creating stop-loss order",
|
|
slog.Int64("trade_id", trade.ID),
|
|
slog.String("symbol", trade.Symbol),
|
|
slog.Float64("executed_price", executedPrice),
|
|
slog.Float64("stop_price", stopPrice),
|
|
)
|
|
|
|
order := &client.Order{
|
|
Symbol: trade.Symbol,
|
|
Action: model.ActionSell, // Stop-loss is always a sell
|
|
Quantity: trade.Quantity,
|
|
OrderType: "STP",
|
|
StopPrice: &stopPrice,
|
|
}
|
|
|
|
orderID, err := s.client.PlaceOrder(ctx, order)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to place stop-loss order: %w", err)
|
|
}
|
|
|
|
// Store stop-loss details
|
|
trade.StopLossPrice = &stopPrice
|
|
trade.IBOrderID = &orderID
|
|
|
|
if err := s.tradeRepo.Update(ctx, trade); err != nil {
|
|
return fmt.Errorf("failed to update trade with stop-loss: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *StopLossManager) MonitorStopLoss(ctx context.Context, trade *model.Trade) error {
|
|
if !s.enabled || trade.StopLossPrice == nil {
|
|
return nil
|
|
}
|
|
|
|
// In a real implementation, this would check the current market price
|
|
// and trigger the stop-loss if the price has fallen below the threshold
|
|
// For now, this is a placeholder
|
|
|
|
s.logger.Debug("monitoring stop-loss",
|
|
slog.Int64("trade_id", trade.ID),
|
|
slog.String("symbol", trade.Symbol),
|
|
slog.Float64("stop_price", *trade.StopLossPrice),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *StopLossManager) Run(ctx context.Context) error {
|
|
if !s.enabled {
|
|
s.logger.Info("stop-loss manager disabled")
|
|
return nil
|
|
}
|
|
|
|
s.logger.Info("stop-loss manager starting")
|
|
|
|
ticker := time.NewTicker(10 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
s.logger.Info("stop-loss manager stopping")
|
|
return ctx.Err()
|
|
case <-ticker.C:
|
|
if err := s.monitorAllActiveTrades(ctx); err != nil {
|
|
s.logger.Error("failed to monitor stop-losses", slog.Any("error", err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *StopLossManager) monitorAllActiveTrades(ctx context.Context) error {
|
|
trades, err := s.tradeRepo.GetActiveTrades(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get active trades: %w", err)
|
|
}
|
|
|
|
for _, trade := range trades {
|
|
if err := s.MonitorStopLoss(ctx, trade); err != nil {
|
|
s.logger.Error("failed to monitor stop-loss for trade",
|
|
slog.Int64("trade_id", trade.ID),
|
|
slog.Any("error", err),
|
|
)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|