Build and Push Docker Image / build-and-push (push) Successful in 4m7s
Signed-off-by: kaedwen <kaedwen@heinrich.blue>
229 lines
6.9 KiB
Go
229 lines
6.9 KiB
Go
package main
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/signal"
|
||
"strconv"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/scmhub/ibapi"
|
||
)
|
||
|
||
type Wrapper struct {
|
||
ibapi.Wrapper
|
||
connected bool
|
||
nextID int64
|
||
errorMsg string
|
||
marketDataRecv bool
|
||
}
|
||
|
||
func (w *Wrapper) ConnectAck() {
|
||
fmt.Println("✓ Connected to IB Gateway")
|
||
w.connected = true
|
||
}
|
||
|
||
func (w *Wrapper) NextValidID(orderId int64) {
|
||
fmt.Printf("✓ Next valid order ID: %d\n", orderId)
|
||
w.nextID = orderId
|
||
}
|
||
|
||
func (w *Wrapper) ManagedAccounts(accountsList []string) {
|
||
fmt.Printf("✓ Managed accounts: %v\n", accountsList)
|
||
}
|
||
|
||
func (w *Wrapper) Error(id int64, errorCode int64, errorMsgType int64, errorString string, advancedOrderRejectJson string) {
|
||
// Info messages (2100-2199) or connection status messages
|
||
if errorCode >= 2100 && errorCode < 2200 {
|
||
fmt.Printf("ℹ Info [%d]: %s\n", errorCode, errorString)
|
||
} else if errorCode >= 1100 && errorCode < 1300 {
|
||
// Warning messages (1100-1299) - system/connection warnings
|
||
fmt.Printf("⚠ Warning [%d]: %s\n", errorCode, errorString)
|
||
} else if errorCode > 1783000000000 {
|
||
// Very high error codes are typically connection status messages
|
||
// Ignore or log as debug
|
||
} else {
|
||
fmt.Printf("✗ Error [%d]: %s\n", errorCode, errorString)
|
||
w.errorMsg = errorString
|
||
}
|
||
}
|
||
|
||
func (w *Wrapper) CurrentTime(t int64) {
|
||
serverTime := time.Unix(t, 0)
|
||
fmt.Printf("✓ Server time: %s\n", serverTime.Format("2006-01-02 15:04:05 MST"))
|
||
}
|
||
|
||
func (w *Wrapper) ConnectionClosed() {
|
||
fmt.Println("⚠ Connection closed by server")
|
||
w.connected = false
|
||
}
|
||
|
||
func (w *Wrapper) TickPrice(reqID int64, tickType ibapi.TickType, price float64, attrib ibapi.TickAttrib) {
|
||
w.marketDataRecv = true
|
||
tickName := map[ibapi.TickType]string{
|
||
1: "BID", 2: "ASK", 4: "LAST", 6: "HIGH", 7: "LOW", 9: "CLOSE",
|
||
14: "OPEN", 15: "LOW_13_WEEK", 16: "HIGH_13_WEEK", 17: "LOW_26_WEEK", 18: "HIGH_26_WEEK",
|
||
19: "LOW_52_WEEK", 20: "HIGH_52_WEEK", 35: "LAST_TIMESTAMP", 37: "MARK_PRICE",
|
||
66: "BID_EXCH", 67: "ASK_EXCH", 68: "LAST_EXCH", 72: "HIGH_52W", 73: "LOW_52W",
|
||
75: "PREV_CLOSE", 76: "HALTED",
|
||
}
|
||
name := tickName[tickType]
|
||
if name == "" {
|
||
name = fmt.Sprintf("Type_%d", tickType)
|
||
}
|
||
|
||
timestamp := time.Now().Format("15:04:05")
|
||
fmt.Printf("[%s] %-22s %.2f\n", timestamp, name, price)
|
||
}
|
||
|
||
func (w *Wrapper) TickSize(reqID int64, tickType ibapi.TickType, size ibapi.Decimal) {
|
||
w.marketDataRecv = true
|
||
tickName := map[ibapi.TickType]string{
|
||
0: "BID_SIZE", 3: "ASK_SIZE", 5: "LAST_SIZE", 8: "VOLUME",
|
||
21: "AVG_VOLUME", 27: "CALL_OPTION_VOLUME", 28: "PUT_OPTION_VOLUME",
|
||
29: "CALL_OPEN_INTEREST", 30: "PUT_OPEN_INTEREST", 34: "AUCTION_VOLUME",
|
||
69: "BID_SIZE_EXCH", 70: "ASK_SIZE_EXCH", 71: "LAST_SIZE_EXCH", 74: "VOLUME_52W",
|
||
}
|
||
name := tickName[tickType]
|
||
if name == "" {
|
||
name = fmt.Sprintf("Type_%d", tickType)
|
||
}
|
||
|
||
timestamp := time.Now().Format("15:04:05")
|
||
fmt.Printf("[%s] %-22s %s\n", timestamp, name, size.String())
|
||
}
|
||
|
||
func (w *Wrapper) TickString(reqID int64, tickType ibapi.TickType, value string) {
|
||
w.marketDataRecv = true
|
||
tickName := map[ibapi.TickType]string{
|
||
32: "BID_EXCH", 33: "ASK_EXCH", 45: "LAST_TIMESTAMP", 48: "RT_VOLUME",
|
||
84: "LAST_EXCH", 85: "LAST_REG_TIME", 88: "DELAYED_LAST_TIMESTAMP",
|
||
}
|
||
name := tickName[tickType]
|
||
if name == "" {
|
||
name = fmt.Sprintf("Type_%d", tickType)
|
||
}
|
||
|
||
// Convert Unix timestamp to ISO format for timestamp fields
|
||
displayValue := value
|
||
if tickType == 45 || tickType == 88 { // LAST_TIMESTAMP or DELAYED_LAST_TIMESTAMP
|
||
if ts, err := strconv.ParseInt(value, 10, 64); err == nil {
|
||
displayValue = time.Unix(ts, 0).Format(time.RFC3339)
|
||
}
|
||
}
|
||
|
||
timestamp := time.Now().Format("15:04:05")
|
||
fmt.Printf("[%s] %-22s %s\n", timestamp, name, displayValue)
|
||
}
|
||
|
||
func main() {
|
||
host := flag.String("host", "127.0.0.1", "IB Gateway host")
|
||
port := flag.Int("port", 4002, "IB Gateway port (4001 for live, 4002 for paper)")
|
||
clientID := flag.Int("client", 1, "Client ID")
|
||
timeout := flag.Int("timeout", 10, "Connection timeout in seconds")
|
||
symbol := flag.String("symbol", "", "Symbol to subscribe to market data (e.g. AAPL, TSLA)")
|
||
exchange := flag.String("exchange", "SMART", "Exchange for market data")
|
||
currency := flag.String("currency", "USD", "Currency for market data")
|
||
marketDataType := flag.Int("mdtype", 3, "Market data type: 1=Live, 2=Frozen, 3=Delayed(15min), 4=Delayed-Frozen")
|
||
flag.Parse()
|
||
|
||
fmt.Printf("Testing IB Gateway connection...\n")
|
||
fmt.Printf("Host: %s:%d\n", *host, *port)
|
||
fmt.Printf("Client ID: %d\n\n", *clientID)
|
||
|
||
wrapper := &Wrapper{}
|
||
client := ibapi.NewEClient(wrapper)
|
||
|
||
// Connect
|
||
fmt.Println("→ Connecting...")
|
||
if err := client.Connect(*host, *port, int64(*clientID)); err != nil {
|
||
log.Fatalf("✗ Connection failed: %v\n", err)
|
||
}
|
||
defer client.Disconnect()
|
||
|
||
// Wait for connection
|
||
start := time.Now()
|
||
for !wrapper.connected && time.Since(start) < time.Duration(*timeout)*time.Second {
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
|
||
if !wrapper.connected {
|
||
log.Fatal("✗ Connection timeout")
|
||
}
|
||
|
||
// Wait for NextValidId
|
||
start = time.Now()
|
||
for wrapper.nextID == 0 && time.Since(start) < time.Duration(*timeout)*time.Second {
|
||
time.Sleep(100 * time.Millisecond)
|
||
}
|
||
|
||
if wrapper.nextID == 0 {
|
||
log.Fatal("✗ Did not receive NextValidId")
|
||
}
|
||
|
||
// Request server time
|
||
fmt.Println("\n→ Requesting server time...")
|
||
client.ReqCurrentTime()
|
||
|
||
// Wait for response
|
||
time.Sleep(1 * time.Second)
|
||
|
||
// Subscribe to market data if symbol provided
|
||
if *symbol != "" {
|
||
fmt.Printf("\n→ Subscribing to market data for %s...\n", *symbol)
|
||
|
||
contract := &ibapi.Contract{
|
||
Symbol: *symbol,
|
||
SecType: "STK",
|
||
Exchange: *exchange,
|
||
Currency: *currency,
|
||
}
|
||
|
||
// Request market data type
|
||
// MarketDataType: 1=Live, 2=Frozen, 3=Delayed(15min), 4=Delayed-Frozen
|
||
mdTypeDesc := map[int]string{
|
||
1: "Live",
|
||
2: "Frozen",
|
||
3: "Delayed (15min)",
|
||
4: "Delayed-Frozen",
|
||
}
|
||
fmt.Printf(" Market data type: %s\n", mdTypeDesc[*marketDataType])
|
||
client.ReqMarketDataType(int64(*marketDataType))
|
||
time.Sleep(500 * time.Millisecond)
|
||
|
||
// Request market data
|
||
// ReqID: 1, Contract, GenericTickList: "", Snapshot: false, RegulatorySnapshot: false, MktDataOptions: nil
|
||
client.ReqMktData(1, contract, "", false, false, nil)
|
||
|
||
// Wait for initial market data
|
||
fmt.Println(" Receiving market data... (Press Ctrl+C to exit)")
|
||
time.Sleep(2 * time.Second)
|
||
|
||
if !wrapper.marketDataRecv {
|
||
fmt.Println(" ⚠ No market data received yet - may require market data subscription or market is closed")
|
||
}
|
||
|
||
// Setup signal handler for graceful shutdown
|
||
sigChan := make(chan os.Signal, 1)
|
||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||
|
||
// Wait for Ctrl+C
|
||
<-sigChan
|
||
fmt.Println("\n\n→ Shutting down...")
|
||
|
||
// Cancel market data subscription
|
||
client.CancelMktData(1)
|
||
fmt.Println(" Market data subscription cancelled")
|
||
} else {
|
||
fmt.Println("\n✓ Connection test successful!")
|
||
fmt.Println("\nConnection details:")
|
||
fmt.Printf(" - Next Order ID: %d\n", wrapper.nextID)
|
||
fmt.Printf(" - Client ID: %d\n", *clientID)
|
||
}
|
||
|
||
os.Exit(0)
|
||
}
|