148 lines
3.4 KiB
Go
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
|
|
}
|