Files
aitrade/pkg/app/client/wrapper.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

219 lines
5.6 KiB
Go

package client
import (
"fmt"
"log/slog"
"time"
"github.com/scmhub/ibapi"
)
// CustomWrapper wraps the default ibapi.Wrapper and overrides only the callbacks we need
type CustomWrapper struct {
ibapi.Wrapper // Embed default wrapper for all other methods
logger *slog.Logger
client *IBClient
}
// NewCustomWrapper creates a new wrapper with default implementations for all EWrapper methods
func NewCustomWrapper(logger *slog.Logger, client *IBClient) *CustomWrapper {
return &CustomWrapper{
Wrapper: ibapi.Wrapper{},
logger: logger,
client: client,
}
}
// ConnectAck - override to signal connection is ready
func (w *CustomWrapper) ConnectAck() {
w.logger.Info("IB connection acknowledged")
// Signal that connection is ready
if w.client != nil {
select {
case w.client.readyChan <- struct{}{}:
// Signal sent
default:
// Channel already closed or full, ignore
}
}
}
// NextValidID - override for logging
func (w *CustomWrapper) NextValidID(orderID int64) {
w.logger.Debug("NextValidID received", slog.Int64("order_id", orderID))
}
// ManagedAccounts - override for logging (scmhub uses []string, not string)
func (w *CustomWrapper) ManagedAccounts(accountsList []string) {
w.logger.Info("ManagedAccounts received", slog.Any("accounts", accountsList))
}
// Error - override to handle IB API errors
func (w *CustomWrapper) Error(reqID int64, errTime int64, errCode int64, errString string, advancedOrderRejectJson string) {
w.logger.Error("IB API error",
slog.Int64("req_id", reqID),
slog.Int64("err_time", errTime),
slog.Int64("code", errCode),
slog.String("message", errString),
)
// Special handling for market data errors
if errCode == 354 {
w.logger.Warn("Market data permission error - check your IB account subscriptions",
slog.Int64("req_id", reqID))
}
}
// ConnectionClosed - override for logging
func (w *CustomWrapper) ConnectionClosed() {
w.logger.Warn("IB connection closed")
}
// TickPrice - handle price ticks for market data
func (w *CustomWrapper) TickPrice(reqID int64, tickType ibapi.TickType, price float64, attrib ibapi.TickAttrib) {
w.logger.Debug("received tick price",
slog.Int64("req_id", reqID),
slog.Int64("tick_type", int64(tickType)),
slog.Float64("price", price))
if w.client == nil {
return
}
// Map tick types to our market data structure
// tickType: 1=bid, 2=ask, 4=last, 6=high, 7=low, 9=close
symbol := w.getSymbolForReqID(reqID)
if symbol == "" {
w.logger.Warn("received tick for unknown reqID", slog.Int64("req_id", reqID))
return
}
w.client.marketDataMu.Lock()
defer w.client.marketDataMu.Unlock()
// Get or create market data entry
data, exists := w.client.marketData[symbol]
if !exists {
data = &MarketData{
Symbol: symbol,
Timestamp: time.Now(),
}
w.client.marketData[symbol] = data
}
// Update timestamp and data based on tick type
data.Timestamp = time.Now()
switch tickType {
case 1, 66: // Bid (Live or Delayed)
data.BidPrice = price
case 2, 67: // Ask (Live or Delayed)
data.AskPrice = price
case 4, 68: // Last (Live or Delayed)
data.LastPrice = price
}
w.logger.Info("updated market data",
slog.String("symbol", symbol),
slog.Float64("last", data.LastPrice),
slog.Float64("bid", data.BidPrice),
slog.Float64("ask", data.AskPrice))
}
// TickSize - handle size ticks for market data
func (w *CustomWrapper) TickSize(reqID int64, tickType ibapi.TickType, size ibapi.Decimal) {
if w.client == nil {
return
}
// tickType: 0=bid size, 3=ask size, 5=last size, 8=volume
symbol := w.getSymbolForReqID(reqID)
if symbol == "" {
return
}
w.client.marketDataMu.Lock()
defer w.client.marketDataMu.Unlock()
data, exists := w.client.marketData[symbol]
if !exists {
data = &MarketData{
Symbol: symbol,
Timestamp: time.Now(),
}
w.client.marketData[symbol] = data
}
// Update timestamp and volume
data.Timestamp = time.Now()
if tickType == 8 {
data.Volume = size.Int()
}
}
// Helper function to extract symbol from reqID
func (w *CustomWrapper) getSymbolForReqID(reqID int64) string {
if w.client == nil {
return ""
}
w.client.marketDataMu.RLock()
defer w.client.marketDataMu.RUnlock()
symbol, ok := w.client.reqIDToSymbol[reqID]
if !ok {
return ""
}
return symbol
}
// AccountSummary - handle account summary updates
func (w *CustomWrapper) AccountSummary(reqID int64, account string, tag string, value string, currency string) {
if w.client == nil {
return
}
w.logger.Debug("received account summary",
slog.Int64("req_id", reqID),
slog.String("account", account),
slog.String("tag", tag),
slog.String("value", value),
slog.String("currency", currency))
w.client.accountSummaryMu.Lock()
defer w.client.accountSummaryMu.Unlock()
// Parse value as float
var floatValue float64
if _, err := fmt.Sscanf(value, "%f", &floatValue); err != nil {
w.logger.Warn("failed to parse account summary value", slog.String("tag", tag), slog.String("value", value))
return
}
// Update account summary based on tag
switch tag {
case "NetLiquidation":
w.client.accountSummary.TotalValue = floatValue
case "TotalCashValue":
w.client.accountSummary.CashBalance = floatValue
case "BuyingPower":
w.client.accountSummary.BuyingPower = floatValue
case "UnrealizedPnL":
w.client.accountSummary.UnrealizedPnL = floatValue
case "RealizedPnL":
w.client.accountSummary.RealizedPnL = floatValue
}
w.logger.Debug("updated account summary",
slog.String("tag", tag),
slog.Float64("value", floatValue))
}
// AccountSummaryEnd - handle end of account summary
func (w *CustomWrapper) AccountSummaryEnd(reqID int64) {
w.logger.Debug("account summary end", slog.Int64("req_id", reqID))
}