219 lines
5.6 KiB
Go
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))
|
|
}
|