Files
aitrade/pkg/app/trader/executor.go
T
kaedwen ea141eb012
Build and Push Docker Image / build-and-push (push) Failing after 1m41s
initial
2026-07-02 20:09:44 +02:00

148 lines
3.4 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 Executor struct {
client *client.IBClient
tradeRepo *db.TradeRepository
stopLossMgr *StopLossManager
logger *slog.Logger
}
func NewExecutor(
client *client.IBClient,
tradeRepo *db.TradeRepository,
stopLossMgr *StopLossManager,
logger *slog.Logger,
) *Executor {
return &Executor{
client: client,
tradeRepo: tradeRepo,
stopLossMgr: stopLossMgr,
logger: logger,
}
}
func (e *Executor) ExecuteTrade(ctx context.Context, trade *model.Trade) error {
e.logger.Info("executing trade",
slog.Int64("trade_id", trade.ID),
slog.String("symbol", trade.Symbol),
slog.String("action", string(trade.Action)),
slog.Int("quantity", trade.Quantity),
)
// Create order
order := &client.Order{
Symbol: trade.Symbol,
Action: trade.Action,
Quantity: trade.Quantity,
OrderType: "MKT", // Market order
}
// Place order with IB
orderID, err := e.client.PlaceOrder(ctx, order)
if err != nil {
return fmt.Errorf("failed to place order: %w", err)
}
// Update trade status
now := time.Now()
trade.Status = model.TradeSubmitted
trade.SubmittedAt = &now
trade.IBOrderID = &orderID
if err := e.tradeRepo.Update(ctx, trade); err != nil {
return fmt.Errorf("failed to update trade status: %w", err)
}
e.logger.Info("trade submitted to IB",
slog.Int64("trade_id", trade.ID),
slog.Int64("ib_order_id", orderID),
)
// Simulate order fill (in real implementation, this would come from IB callbacks)
go e.simulateOrderFill(trade)
return nil
}
func (e *Executor) simulateOrderFill(trade *model.Trade) {
// Wait a bit to simulate order execution
time.Sleep(2 * time.Second)
ctx := context.Background()
// Update trade as filled
now := time.Now()
trade.Status = model.TradeFilled
trade.FilledAt = &now
// Simulate executed price (in real implementation, this comes from IB)
if trade.TargetPrice != nil {
trade.ExecutedPrice = trade.TargetPrice
} else {
// Use a mock price
mockPrice := 150.0
trade.ExecutedPrice = &mockPrice
}
if err := e.tradeRepo.Update(ctx, trade); err != nil {
e.logger.Error("failed to update filled trade", slog.Any("error", err))
return
}
e.logger.Info("trade filled",
slog.Int64("trade_id", trade.ID),
slog.Float64("executed_price", *trade.ExecutedPrice),
)
// Create stop-loss if enabled
if trade.StopLossPrice != nil {
stopLossPercent := 3.0 // TODO: Get from strategy
if err := e.stopLossMgr.CreateStopLoss(ctx, trade, stopLossPercent); err != nil {
e.logger.Error("failed to create stop-loss",
slog.Int64("trade_id", trade.ID),
slog.Any("error", err),
)
return
}
}
// Mark as completed
now = time.Now()
trade.Status = model.TradeCompleted
trade.CompletedAt = &now
if err := e.tradeRepo.Update(ctx, trade); err != nil {
e.logger.Error("failed to mark trade completed", slog.Any("error", err))
return
}
e.logger.Info("trade completed",
slog.Int64("trade_id", trade.ID),
)
}
func (e *Executor) CancelTrade(ctx context.Context, trade *model.Trade) error {
if trade.IBOrderID != nil {
if err := e.client.CancelOrder(ctx, *trade.IBOrderID); err != nil {
return fmt.Errorf("failed to cancel IB order: %w", err)
}
}
e.logger.Info("trade cancelled",
slog.Int64("trade_id", trade.ID),
)
return nil
}