├── .gitignore ├── LICENSE ├── README.md ├── common.go ├── config.go ├── config_test.go ├── contract.go ├── encoder.go ├── encoder_test.go ├── errors.go ├── examples ├── bar_data │ └── bar_data.go ├── basics │ └── basics.go ├── contract_details │ └── contract_details.go ├── market_depth │ └── market_depth.go ├── option_chain │ └── option_chain.go ├── orders │ └── orders.go ├── pnl │ └── pnl.go ├── scanners │ ├── scanner_parameters.xml │ └── scanners.go └── tick_data │ └── tick_data.go ├── go.mod ├── go.sum ├── ib.go ├── ib_test.go ├── ibapi.go ├── pubsub.go ├── pubsub_test.go ├── state.go ├── ticker.go ├── trade.go ├── trade_test.go ├── utils.go ├── utils_test.go └── wrapper.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | go.work.sum 23 | 24 | # env file 25 | .env 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 SCM 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Go Report Card](https://goreportcard.com/badge/github.com/scmhub/ibsync)](https://goreportcard.com/report/github.com/scmhub/ibsync) 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/scmhub/ibsync.svg)](https://pkg.go.dev/github.com/scmhub/ibsync) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | 5 | # Interactive Brokers Synchronous Golang Client 6 | 7 | `ibsync` is a Go package designed to simplify interaction with the [Interactive Brokers](https://www.interactivebrokers.com/en/home.php) API. It is inspired by the great [ib_insync](https://github.com/erdewit/ib_insync) Python library and based on [ibapi](https://github.com/scmhub/ibapi). It provides a synchronous, easy-to-use interface for account management, trade execution, real-time and historical market data within the IB ecosystem. 8 | 9 | > [!CAUTION] 10 | > This package is in the **beta phase**. While functional, it may still have bugs or incomplete features. Please test extensively in non-production environments. 11 | 12 | ## Getting Started 13 | 14 | ### Prerequisites 15 | - **Go** version 1.23 or higher (recommended) 16 | - An **Interactive Brokers** account with TWS or IB Gateway installed and running 17 | 18 | ### Installation 19 | Install the package via `go get`: 20 | 21 | ```bash 22 | go get -u github.com/scmhub/ibsync 23 | ``` 24 | 25 | ## Quick start 26 | Here’s a basic example to connect and get the managed accounts list: 27 | ```go 28 | package main 29 | 30 | import "github.com/scmhub/ibsync" 31 | 32 | func main() { 33 | // Get the logger (zerolog) 34 | log := ibapi.Logger() 35 | ibsync.SetConsoleWriter() // pretty logs to console, for dev and test. 36 | 37 | // New IB client & Connect 38 | ib := ibsync.NewIB() 39 | 40 | err := ib.Connect() 41 | if err != nil { 42 | log.Error().Err(err).Msg("Connect") 43 | return 44 | } 45 | defer ib.Disconnect() 46 | 47 | managedAccounts := ib.ManagedAccounts() 48 | log.Info().Strs("accounts", managedAccounts).Msg("Managed accounts list") 49 | } 50 | ``` 51 | 52 | ## Usage guide 53 | 54 | ### Configuration 55 | Connect with a different configuration. 56 | ```go 57 | // New IB client & Connect 58 | ib := ibsync.NewIB() 59 | 60 | err := ib.Connect( 61 | ibsync.NewConfig( 62 | ibsync.WithHost("10.74.0.9"), // Default: "127.0.0.1". 63 | ibsync.WithPort(4002), // Default: 7497. 64 | ibsync.WithClientID(5), // Default: a random number. If set to 0 it will also retreive manual orders. 65 | ibsync.WithTimeout(1*time.Second), // Default is 30 seconds. 66 | ), 67 | ) 68 | if err != nil { 69 | log.Error().Err(err).Msg("Connect") 70 | return 71 | } 72 | defer ib.Disconnect() 73 | ``` 74 | 75 | ### Account 76 | Account value, summary, positions, trades... 77 | ```go 78 | // Account Values 79 | accountValues := ib.AccountValues() 80 | 81 | // Account Summary 82 | accountSummary := ib.AccountSummary() 83 | 84 | // Portfolio 85 | portfolio := ib.Portfolio() 86 | 87 | // Positions 88 | // Subscribe to Postion 89 | ib.ReqPositions() 90 | // Position Channel 91 | posChan := ib.PositionChan() 92 | 93 | // Trades 94 | trades := ib.Trades() 95 | openTrades := ib.OpenTrades() 96 | 97 | ``` 98 | 99 | ### Contract details 100 | Request contract details from symbol, exchange 101 | ```go 102 | // NewStock("AMD", "", "") 103 | amd := ibsync.NewStock("AMD", "", "") 104 | cd, err := ib.ReqContractDetails(amd) 105 | if err != nil { 106 | log.Error().Err(err).Msg("request contract details") 107 | return 108 | } 109 | fmt.Println("number of contract found for request NewStock(\"AMD\", \"\", \"\") :", len(cd)) 110 | ``` 111 | 112 | ### Pnl 113 | Subscribe to the pnl stream 114 | ```go 115 | // Request PnL subscription for the account. 116 | ib.ReqPnL(account, modelCode) 117 | 118 | // Get a PnL channel to receive updates... 119 | pnlChan := ib.PnlChan(account, modelCode) 120 | 121 | go func() { 122 | for pnl := range pnlChan { 123 | fmt.Println("Received PnL from channel:", pnl) 124 | } 125 | }() 126 | 127 | //... Or read the last PnL on the client state 128 | pnl := ib.Pnl(account, modelCode) 129 | fmt.Println("Current PnL:", pnl) 130 | ``` 131 | 132 | ### Orders 133 | Place an order and create a new trade. Modify or cancel the trade. Cancel all trades 134 | ```go 135 | // Create the contract 136 | eurusd := ibsync.NewForex("EUR", "IDEALPRO", "USD") 137 | 138 | // Create the order 139 | order := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20000"), 1.05) 140 | 141 | // Place the order 142 | trade := ib.PlaceOrder(eurusd, order) 143 | 144 | go func() { 145 | <-trade.Done() 146 | fmt.Println("The trade is done!!!") 147 | }() 148 | 149 | // Cancel the order 150 | ib.CancelOrder(Order, ibsync.NewOrderCancel()) 151 | 152 | // Cancel all orders 153 | ib.ReqGlobalCancel() 154 | ``` 155 | 156 | ### Bar data 157 | Real time and historical bar data. 158 | ```go 159 | // Historical data 160 | barChan, _ := ib.ReqHistoricalData(eurusd, endDateTime, duration, barSize, whatToShow, useRTH, formatDate) 161 | var bars []ibsync.Bar 162 | for bar := range barChan { 163 | bars = append(bars, bar) 164 | } 165 | 166 | // Historical data up to date 167 | barChan, cancel := ib.ReqHistoricalDataUpToDate(eurusd, duration, barSize, whatToShow, useRTH, formatDate) 168 | go func() { 169 | for bar := range barChan { 170 | bars = append(bars, bar) 171 | } 172 | }() 173 | time.Sleep(10 * time.Second) 174 | cancel() 175 | 176 | // Real time bars 177 | rtBarChan, cancel := ib.ReqRealTimeBars(eurusd, 5, "MIDPOINT", useRTH) 178 | <-rtBarChan 179 | cancel() 180 | ``` 181 | 182 | ### Tick data 183 | Real time and historical tick data. 184 | ```go 185 | // Snapshot - Market price 186 | snapshot, err := ib.Snapshot(eurusd) 187 | if err != nil { 188 | panic(fmt.Errorf("snapshot eurusd: %v", err)) 189 | } 190 | fmt.Println("Snapshot market price", snapshot.MarketPrice()) 191 | 192 | // Tick by tick data 193 | tickByTick := ib.ReqTickByTickData(eurusd, "BidAsk", 100, true) 194 | time.Sleep(5 * time.Second) 195 | ib.CancelTickByTickData(eurusd, "BidAsk") 196 | 197 | // HistoricalTicks 198 | historicalTicks, err, done := ib.ReqHistoricalTicks(aapl, startDateTime, time.Time{}, 100, true, true) 199 | ``` 200 | 201 | ### Scanner 202 | Request scanner parameters and scanner subscritpion 203 | ```go 204 | // Scanner Parameters 205 | xml, err := ib.ReqScannerParameters() 206 | 207 | // Scanner subscription 208 | scanSubscription := ibsync.NewScannerSubscription() 209 | scanSubscription.Instrument = "STK" 210 | scanSubscription.LocationCode = "STK.US.MAJOR" 211 | scanSubscription.ScanCode = "TOP_PERC_GAIN" 212 | 213 | scanData, err := ib.ReqScannerSubscription(scanSubscription) 214 | 215 | // Scanner subcscription with filter option 216 | opts := ibsync.ScannerSubscriptionOptions{ 217 | FilterOptions: []ibsync.TagValue{ 218 | {Tag: "changePercAbove", Value: "20"}, 219 | {Tag: "priceAbove", Value: "5"}, 220 | {Tag: "priceBelow", Value: "50"}, 221 | }, 222 | } 223 | 224 | filterScanData, err := ib.ReqScannerSubscription(scanSubscription, opts) 225 | ``` 226 | 227 | ## Documentation 228 | For more information on how to use this package, please refer to the [GoDoc](https://pkg.go.dev/github.com/scmhub/ibsync) documentation and check the [examples](https://github.com/scmhub/ibsync/tree/main/examples) directory. You can also have a look at the `ib_test.go` file 229 | 230 | ## Acknowledgments 231 | - [ibapi](https://github.com/scmhub/ibapi) for core API functionality. 232 | - [ib_insync](https://github.com/erdewit/ib_insync) for API inspiration. (ib_insync is now [ib_async](https://github.com/ib-api-reloaded/ib_async)) 233 | 234 | ## Notice of Non-Affiliation and Disclaimer 235 | > [!CAUTION] 236 | > This project is in the **beta phase** and is still undergoing testing and development. Users are advised to thoroughly test the software in non-production environments before relying on it for live trading. Features may be incomplete, and bugs may exist. Use at your own risk. 237 | 238 | > [!IMPORTANT] 239 | >This project is **not affiliated** with Interactive Brokers Group, Inc. All references to Interactive Brokers, including trademarks, logos, and brand names, belong to their respective owners. The use of these names is purely for informational purposes and does not imply endorsement by Interactive Brokers. 240 | 241 | > [!IMPORTANT] 242 | >The authors of this package make **no guarantees** regarding the software's reliability, accuracy, or suitability for any particular purpose, including trading or financial decisions. **No liability** will be accepted for any financial losses, damages, or misinterpretations arising from the use of this software. 243 | 244 | ## License 245 | Distributed under the MIT License. See [LICENSE](./LICENSE) for more information. 246 | 247 | ## Author 248 | **Philippe Chavanne** - [contact](https://scm.cx/contact) -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | type AccountValue struct { 10 | Account string 11 | Tag string 12 | Value string 13 | Currency string 14 | } 15 | 16 | type AccountValues []AccountValue 17 | 18 | func (avs AccountValues) String() string { 19 | ss := make(map[string]string) 20 | var dots string 21 | for _, av := range avs { 22 | dots = strings.Repeat(".", 40-len(av.Tag)-len(av.Value)) 23 | ss[av.Account] = ss[av.Account] + fmt.Sprintf("\t%v%v%v %v\n", av.Tag, dots, av.Value, av.Currency) 24 | } 25 | s := "\n" 26 | for k, v := range ss { 27 | s = s + fmt.Sprintf("Account: %v\n", k) 28 | s = s + v 29 | } 30 | return s 31 | } 32 | 33 | type AccountSummary []AccountValue 34 | 35 | func (as AccountSummary) String() string { 36 | return fmt.Sprint(AccountValues(as)) 37 | } 38 | 39 | type ReceiveFA struct { 40 | FaDataType FaDataType 41 | Cxml string 42 | } 43 | 44 | type TickData struct { 45 | Time time.Time 46 | TickType TickType 47 | Price float64 48 | Size Decimal 49 | } 50 | 51 | type Tick interface { 52 | Type() TickType 53 | String() string 54 | } 55 | 56 | type TickPrice struct { 57 | TickType TickType 58 | Price float64 59 | Attrib TickAttrib 60 | } 61 | 62 | func (t TickPrice) Type() TickType { 63 | return t.TickType 64 | } 65 | 66 | func (t TickPrice) String() string { 67 | return fmt.Sprintf("<%v> price:%v, attrib:%v", TickName(t.TickType), t.Price, t.Attrib) 68 | } 69 | 70 | type TickSize struct { 71 | TickType TickType 72 | Size Decimal 73 | } 74 | 75 | func (t TickSize) Type() TickType { 76 | return t.TickType 77 | } 78 | 79 | func (t TickSize) String() string { 80 | return fmt.Sprintf("<%v> size:%v", TickName(t.TickType), t.Size) 81 | } 82 | 83 | type TickOptionComputation struct { 84 | TickType TickType 85 | TickAttrib int64 86 | ImpliedVol float64 87 | Delta float64 88 | OptPrice float64 89 | PvDividend float64 90 | Gamma float64 91 | Vega float64 92 | Theta float64 93 | UndPrice float64 94 | } 95 | 96 | func (t TickOptionComputation) Type() TickType { 97 | return t.TickType 98 | } 99 | 100 | func (t TickOptionComputation) String() string { 101 | return fmt.Sprintf("<%v> tickAttrib:%v, impliedVol:%v, delta:%v, optPrice:%v, pvDividend: %v, gamma:%v, vega:%v, theta:%v, undPrice:%v", 102 | TickName(t.TickType), t.TickAttrib, t.ImpliedVol, t.Delta, t.OptPrice, t.PvDividend, t.Gamma, t.Vega, t.Theta, t.UndPrice) 103 | } 104 | 105 | type TickGeneric struct { 106 | TickType TickType 107 | Value float64 108 | } 109 | 110 | func (t TickGeneric) Type() TickType { 111 | return t.TickType 112 | } 113 | 114 | func (t TickGeneric) String() string { 115 | return fmt.Sprintf("<%v> value:%v", TickName(t.TickType), t.Value) 116 | } 117 | 118 | type TickString struct { 119 | TickType TickType 120 | Value string 121 | } 122 | 123 | func (t TickString) Type() TickType { 124 | return t.TickType 125 | } 126 | 127 | func (t TickString) String() string { 128 | return fmt.Sprintf("<%v> value:%v", TickName(t.TickType), t.Value) 129 | } 130 | 131 | type TickEFP struct { 132 | TickType TickType 133 | BasisPoints float64 134 | FormattedBasisPoints string 135 | TotalDividends float64 136 | HoldDays int64 137 | FutureLastTradeDate string 138 | DividendImpact float64 139 | DividendsToLastTradeDate float64 140 | } 141 | 142 | func (t TickEFP) Type() TickType { 143 | return t.TickType 144 | } 145 | 146 | func (t TickEFP) String() string { 147 | return fmt.Sprintf("<%v> basisPoints:%v, formattedBasisPoints:%v, totalDividends:%v, holdDays:%v, futureLastTradeDate:%v, dividendImpact:%v, dividendsToLastTradeDate:%v", 148 | TickName(t.TickType), t.BasisPoints, t.FormattedBasisPoints, t.TotalDividends, t.HoldDays, t.FutureLastTradeDate, t.DividendImpact, t.DividendsToLastTradeDate) 149 | } 150 | 151 | // TickByTick 152 | type TickByTick interface { 153 | Timestamp() time.Time 154 | String() string 155 | } 156 | 157 | type TickByTickAllLast struct { 158 | Time int64 159 | TickType int64 160 | Price float64 161 | Size Decimal 162 | TickAttribLast TickAttribLast 163 | Exchange string 164 | SpecialConditions string 165 | } 166 | 167 | func (t TickByTickAllLast) Timestamp() time.Time { 168 | return time.Unix(t.Time, 0) 169 | } 170 | 171 | func (t TickByTickAllLast) String() string { 172 | return fmt.Sprintf(" timestamp:%v, tickType:%v, price:%v, size:%v, tickAttribLast:%v, exchange:%v, specialConditions:%v", 173 | t.Timestamp(), t.TickType, t.Price, t.Size, t.TickAttribLast, t.Exchange, t.SpecialConditions) 174 | } 175 | 176 | type TickByTickBidAsk struct { 177 | Time int64 178 | BidPrice float64 179 | AskPrice float64 180 | BidSize Decimal 181 | AskSize Decimal 182 | TickAttribBidAsk TickAttribBidAsk 183 | } 184 | 185 | func (t TickByTickBidAsk) Timestamp() time.Time { 186 | return time.Unix(t.Time, 0) 187 | } 188 | 189 | func (t TickByTickBidAsk) String() string { 190 | return fmt.Sprintf(" timestamp:%v, bidPrice:%v, askPrice:%v, bidSize:%v, askSize:%v, tickAttribBidAsk:%v", 191 | t.Timestamp(), t.BidPrice, t.AskPrice, t.BidSize, t.AskSize, t.TickAttribBidAsk) 192 | } 193 | 194 | type TickByTickMidPoint struct { 195 | Time int64 196 | MidPoint float64 197 | } 198 | 199 | func (t TickByTickMidPoint) Timestamp() time.Time { 200 | return time.Unix(t.Time, 0) 201 | } 202 | 203 | func (t TickByTickMidPoint) String() string { 204 | return fmt.Sprintf(" timestamp:%v, midPoint:%v", t.Timestamp(), t.MidPoint) 205 | } 206 | 207 | type MktDepthData struct { 208 | Time time.Time 209 | Position int64 210 | MarketMaker string 211 | Operation int64 212 | Side int64 213 | Price float64 214 | Size Decimal 215 | IsSmartDepth bool 216 | } 217 | 218 | // DOMLevel represents a single level in the order book 219 | type DOMLevel struct { 220 | Price float64 221 | Size Decimal 222 | MarketMaker string 223 | } 224 | 225 | // String provides a readable representation of a DOM level 226 | func (dl DOMLevel) String() string { 227 | return fmt.Sprintf("Price: %v, Size: %v, Market Maker: %s", dl.Price, dl.Size, dl.MarketMaker) 228 | } 229 | 230 | type Dividends struct { 231 | Past12Months float64 232 | Next12Months float64 233 | NextDate time.Time 234 | NextAmount float64 235 | } 236 | 237 | type NewsArticle struct { 238 | ArticleType int64 239 | ArticleText string 240 | } 241 | 242 | type HistoricalNews struct { 243 | Time time.Time 244 | ProviderCode string 245 | ArticleID string 246 | Headline string 247 | } 248 | 249 | type NewsTick struct { 250 | TimeStamp int64 251 | ProviderCode string 252 | ArticleId string 253 | Headline string 254 | ExtraData string 255 | } 256 | 257 | type NewsBulletin struct { 258 | MsgID int64 259 | MsgType int64 260 | NewsMessage string 261 | OriginExch string 262 | } 263 | 264 | type PortfolioItem struct { 265 | Contract *Contract 266 | Position Decimal 267 | MarketPrice float64 268 | MarketValue float64 269 | AverageCost float64 270 | UnrealizedPNL float64 271 | RealizedPNL float64 272 | Account string 273 | } 274 | 275 | type Position struct { 276 | Account string 277 | Contract *Contract 278 | Position Decimal 279 | AvgCost float64 280 | } 281 | 282 | type Pnl struct { 283 | Account string 284 | ModelCode string 285 | DailyPNL float64 286 | UnrealizedPnl float64 287 | RealizedPNL float64 288 | } 289 | 290 | type PnlSingle struct { 291 | Account string 292 | ModelCode string 293 | ConID int64 294 | Position Decimal 295 | DailyPNL float64 296 | UnrealizedPnl float64 297 | RealizedPNL float64 298 | Value float64 299 | } 300 | 301 | type HistoricalSchedule struct { 302 | StartDateTime string 303 | EndDateTime string 304 | TimeZone string 305 | Sessions []HistoricalSession 306 | } 307 | 308 | type OptionChain struct { 309 | Exchange string 310 | UnderlyingConId int64 311 | TradingClass string 312 | Multiplier string 313 | Expirations []string 314 | Strikes []float64 315 | } 316 | 317 | type FundamentalRatios map[string]float64 318 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var log = Logger() 9 | 10 | const ( 11 | // Default values for the connection parameters. 12 | TIMEOUT = 30 * time.Second // Default timeout duration 13 | HOST = "127.0.0.1" // Default host 14 | PORT = 7497 // Default port 15 | ) 16 | 17 | // Config holds the connection parameters for the client. 18 | // This struct centralizes all the configurable options for creating a connection. 19 | type Config struct { 20 | Host string // Host address for the connection 21 | Port int // Port number for the connection 22 | ClientID int64 // Client ID, default is randomized for uniqueness 23 | InSync bool // Stay in sync with server 24 | Timeout time.Duration // Timeout for the connection 25 | ReadOnly bool // Indicates if the client should be in read-only mode 26 | Account string // Optional account identifier 27 | } 28 | 29 | // NewConfig creates a new Config with default values, and applies any functional options. 30 | // The functional options allow customization of the config without directly modifying fields. 31 | func NewConfig(options ...func(*Config)) *Config { 32 | config := &Config{ 33 | Host: HOST, // Default host 34 | Port: PORT, // Default port 35 | ClientID: rand.Int63n(999999) + 1, // Random default client ID to avoid collisions. +1 for non 0 id. 36 | InSync: true, // Default true. Client is kept in sync with the TWS/IBG application 37 | Timeout: TIMEOUT, // Default timeout 38 | } 39 | 40 | // Apply any functional options passed to the NewConfig function 41 | for _, option := range options { 42 | option(config) 43 | } 44 | 45 | return config 46 | } 47 | 48 | // WithHost is a functional option to set a custom host for the Config. 49 | func WithHost(host string) func(*Config) { 50 | return func(c *Config) { 51 | c.Host = host 52 | } 53 | } 54 | 55 | // WithPort is a functional option to set a custom port for the Config. 56 | func WithPort(port int) func(*Config) { 57 | return func(c *Config) { 58 | c.Port = port 59 | } 60 | } 61 | 62 | // WithClientID is a functional option to set a specific ClientID. 63 | // Useful if you want to set a fixed client ID instead of a random one. 64 | func WithClientID(id int64) func(*Config) { 65 | return func(c *Config) { 66 | c.ClientID = id 67 | } 68 | } 69 | 70 | // WithClientZero is a shortcut functional option that sets the ClientID to 0. 71 | // ClientID = 0 is a privileged ClientID that gives your API session access to all order updates, 72 | // including those entered manually in the TWS or by other API clients. 73 | func WithClientZero() func(*Config) { 74 | return func(c *Config) { 75 | c.ClientID = 0 76 | } 77 | 78 | } 79 | 80 | // WithoutSync is a functional option that sets InSybc 81 | func WithoutSync() func(*Config) { 82 | return func(c *Config) { 83 | c.InSync = false 84 | } 85 | } 86 | 87 | // WithTimeout is a functional option to customize the timeout for the connection. 88 | func WithTimeout(timeout time.Duration) func(*Config) { 89 | return func(c *Config) { 90 | c.Timeout = timeout 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestDefaultConfig(t *testing.T) { 9 | config := NewConfig() 10 | 11 | if config.Host != HOST { 12 | t.Errorf("expected default Host to be %s, got %s", HOST, config.Host) 13 | } 14 | 15 | if config.Port != PORT { 16 | t.Errorf("expected default Port to be %d, got %d", PORT, config.Port) 17 | } 18 | 19 | if config.ClientID < 1 || config.ClientID > 999999 { 20 | t.Errorf("expected default ClientID to be between 1 and 999999, got %d", config.ClientID) 21 | } 22 | 23 | if !config.InSync { 24 | t.Errorf("expected default InSync to be true, got %v", config.InSync) 25 | } 26 | 27 | if config.Timeout != TIMEOUT { 28 | t.Errorf("expected default Timeout to be %v, got %v", TIMEOUT, config.Timeout) 29 | } 30 | } 31 | 32 | func TestWithHost(t *testing.T) { 33 | config := NewConfig(WithHost("192.168.1.1")) 34 | 35 | if config.Host != "192.168.1.1" { 36 | t.Errorf("expected Host to be %s, got %s", "192.168.1.1", config.Host) 37 | } 38 | } 39 | 40 | func TestWithPort(t *testing.T) { 41 | config := NewConfig(WithPort(4001)) 42 | 43 | if config.Port != 4001 { 44 | t.Errorf("expected Port to be %d, got %d", 4001, config.Port) 45 | } 46 | } 47 | 48 | func TestWithClientID(t *testing.T) { 49 | config := NewConfig(WithClientID(12345)) 50 | 51 | if config.ClientID != 12345 { 52 | t.Errorf("expected ClientID to be %d, got %d", 12345, config.ClientID) 53 | } 54 | } 55 | 56 | func TestWithClientZero(t *testing.T) { 57 | config := NewConfig(WithClientZero()) 58 | 59 | if config.ClientID != 0 { 60 | t.Errorf("expected ClientID to be 0, got %d", config.ClientID) 61 | } 62 | } 63 | 64 | func TestWithoutSync(t *testing.T) { 65 | config := NewConfig(WithoutSync()) 66 | 67 | if config.InSync { 68 | t.Errorf("expected InSync to be false, got %v", config.InSync) 69 | } 70 | } 71 | 72 | func TestWithTimeout(t *testing.T) { 73 | customTimeout := 15 * time.Second 74 | config := NewConfig(WithTimeout(customTimeout)) 75 | 76 | if config.Timeout != customTimeout { 77 | t.Errorf("expected Timeout to be %v, got %v", customTimeout, config.Timeout) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /contract.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | // NewStock creates a stock contract (STK) for the specified symbol, exchange, and currency. 4 | // The symbol represents the stock ticker (e.g., "AAPL"), the exchange is where the stock is traded (e.g., "NASDAQ"), 5 | // and the currency is the denomination of the stock (e.g., "USD"). 6 | func NewStock(symbol, exchange, currency string) *Contract { 7 | 8 | contract := NewContract() 9 | contract.Symbol = symbol 10 | contract.SecType = "STK" 11 | contract.Currency = currency 12 | contract.Exchange = exchange 13 | 14 | return contract 15 | } 16 | 17 | // NewOption creates an option contract (OPT) based on the symbol of the underlying asset, expiration date, 18 | // strike price, option right, exchange, contract size, and currency. 19 | // lastTradeDateOrContractMonth is the expiration date. "YYYYMM" format will specify the last trading month or "YYYYMMDD" format the last trading day. 20 | // The right specifies if it's a call ("C" or "CALL") or a put ("P" or "PUT") option. 21 | func NewOption(symbol, lastTradeDateOrContractMonth string, strike float64, right, exchange, multiplier, currency string) *Contract { 22 | 23 | contract := NewContract() 24 | contract.Symbol = symbol 25 | contract.SecType = "OPT" 26 | contract.Currency = currency 27 | contract.Exchange = exchange 28 | contract.LastTradeDateOrContractMonth = lastTradeDateOrContractMonth 29 | contract.Right = right 30 | contract.Strike = strike 31 | contract.Multiplier = multiplier 32 | 33 | return contract 34 | } 35 | 36 | // NewFuture creates a future contract (FUT) based on the symbol of the underlying asset, expiration date, 37 | // exchange, contract size (multiplier), and currency. 38 | // lastTradeDateOrContractMonth is the expiration date. "YYYYMM" format will specify the last trading month or "YYYYMMDD" format the last trading day. 39 | func NewFuture(symbol, lastTradeDateOrContractMonth string, exchange, multiplier, currency string) *Contract { 40 | 41 | contract := NewContract() 42 | contract.Symbol = symbol 43 | contract.SecType = "FUT" 44 | contract.Currency = currency 45 | contract.Exchange = exchange 46 | contract.LastTradeDateOrContractMonth = lastTradeDateOrContractMonth 47 | contract.Multiplier = multiplier 48 | 49 | return contract 50 | } 51 | 52 | // NewContFuture creates a continuous future contract (CONFUT) for the given symbol, exchange, contract size, and currency. 53 | // A continuous future contract represents a series of futures contracts for the same asset. 54 | func NewContFuture(symbol, exchange, multiplier, currency string) *Contract { 55 | 56 | contract := NewContract() 57 | contract.Symbol = symbol 58 | contract.SecType = "CONTFUT" 59 | contract.Currency = currency 60 | contract.Exchange = exchange 61 | contract.Multiplier = multiplier 62 | 63 | return contract 64 | } 65 | 66 | // NewForex creates a forex contract (CASH) for a currency pair. 67 | // symbol is the base currency, and currency is the quote currency. 68 | // For a pair like "EURUSD", "EUR" is the symbol and "USD" the currency. 69 | func NewForex(symbol, exchange, currency string) *Contract { 70 | 71 | contract := NewContract() 72 | contract.Symbol = symbol 73 | contract.SecType = "CASH" 74 | contract.Currency = currency 75 | contract.Exchange = exchange 76 | 77 | return contract 78 | } 79 | 80 | // NewIndex creates an index contract (IND) for the given index symbol, exchange, and currency. 81 | // The symbol typically represents a stock index (e.g., "SPX"). 82 | func NewIndex(symbol, exchange, currency string) *Contract { 83 | 84 | contract := NewContract() 85 | contract.Symbol = symbol 86 | contract.SecType = "IND" 87 | contract.Currency = currency 88 | contract.Exchange = exchange 89 | 90 | return contract 91 | } 92 | 93 | // NewCFD creates a contract for difference (CFD) for the specified symbol, exchange, and currency. 94 | func NewCFD(symbol, exchange, currency string) *Contract { 95 | 96 | contract := NewContract() 97 | contract.Symbol = symbol 98 | contract.SecType = "CFD" 99 | contract.Currency = currency 100 | contract.Exchange = exchange 101 | 102 | return contract 103 | } 104 | 105 | // NewCommodity creates a commodity contract (CMDTY) for the given symbol, exchange, and currency. 106 | func NewCommodity(symbol, exchange, currency string) *Contract { 107 | 108 | contract := NewContract() 109 | contract.Symbol = symbol 110 | contract.SecType = "CMDTY" 111 | contract.Currency = currency 112 | contract.Exchange = exchange 113 | 114 | return contract 115 | } 116 | 117 | // NewBond creates a bond contract (Bond). 118 | func NewBond(symbol, exchange, currency string) *Contract { 119 | 120 | contract := NewContract() 121 | contract.Symbol = symbol 122 | contract.SecType = "Bond" 123 | contract.Currency = currency 124 | contract.Exchange = exchange 125 | 126 | return contract 127 | } 128 | 129 | // NewFutureOption creates a future option contract (FOP) based on the symbol of the underlying asset, expiration date, 130 | // strike price, option right, exchange, contract size, and currency. 131 | // lastTradeDateOrContractMonth is the expiration date. "YYYYMM" format will specify the last trading month or "YYYYMMDD" format the last trading day. 132 | // The right specifies if it's a call ("C" or "CALL") or a put ("P" or "PUT") option. 133 | func NewFutureOption(symbol, lastTradeDateOrContractMonth string, strike float64, right, exchange, multiplier, currency string) *Contract { 134 | 135 | contract := NewContract() 136 | contract.Symbol = symbol 137 | contract.SecType = "FOP" 138 | contract.Currency = currency 139 | contract.Exchange = exchange 140 | contract.LastTradeDateOrContractMonth = lastTradeDateOrContractMonth 141 | contract.Right = right 142 | contract.Strike = strike 143 | contract.Multiplier = multiplier 144 | 145 | return contract 146 | } 147 | 148 | // NewMutualFund creates a mutual fund contract (FUND). 149 | func NewMutualFund() *Contract { 150 | 151 | contract := NewContract() 152 | contract.SecType = "FUND" 153 | 154 | return contract 155 | } 156 | 157 | // NewWarrant creates a warrant contract (WAR). 158 | func NewWarrant() *Contract { 159 | 160 | contract := NewContract() 161 | contract.SecType = "WAR" 162 | 163 | return contract 164 | } 165 | 166 | // NewBag creates a bag contract (BAG), which may represent a collection of contracts bundled together. 167 | func NewBag() *Contract { 168 | 169 | contract := NewContract() 170 | contract.SecType = "BAG" 171 | 172 | return contract 173 | } 174 | 175 | // NewCrypto creates a cryptocurrency contract (CRYPTO) for the specified symbol, exchange, and currency. 176 | // The symbol represents the cryptocurrency being traded (e.g., "BTC"). 177 | func NewCrypto(symbol, exchange, currency string) *Contract { 178 | 179 | contract := NewContract() 180 | contract.Symbol = symbol 181 | contract.SecType = "CRYPTO" 182 | contract.Currency = currency 183 | contract.Exchange = exchange 184 | 185 | return contract 186 | } 187 | -------------------------------------------------------------------------------- /encoder.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/scmhub/ibapi" 10 | ) 11 | 12 | const sep = "::" 13 | 14 | // init registers various common structs with gob for encoding/decoding. 15 | func init() { 16 | // Common 17 | gob.Register(Fill{}) 18 | gob.Register(OptionChain{}) 19 | gob.Register(TickPrice{}) 20 | gob.Register(TickSize{}) 21 | gob.Register(TickOptionComputation{}) 22 | gob.Register(TickGeneric{}) 23 | gob.Register(TickString{}) 24 | gob.Register(TickEFP{}) 25 | 26 | // ibapi 27 | gob.Register(ibapi.Bar{}) 28 | gob.Register(ibapi.CodeMsgPair{}) 29 | gob.Register(ibapi.ComboLeg{}) 30 | gob.Register(ibapi.CommissionAndFeesReport{}) 31 | gob.Register(ibapi.Contract{}) 32 | gob.Register(ibapi.ContractDetails{}) 33 | gob.Register(ibapi.ContractDescription{}) 34 | gob.Register(ibapi.Decimal{}) 35 | gob.Register(ibapi.DeltaNeutralContract{}) 36 | gob.Register(ibapi.DepthMktDataDescription{}) 37 | gob.Register(ibapi.Execution{}) 38 | gob.Register(ibapi.FamilyCode{}) 39 | gob.Register(ibapi.HistogramData{}) 40 | gob.Register(ibapi.HistoricalTick{}) 41 | gob.Register(ibapi.HistoricalTickBidAsk{}) 42 | gob.Register(ibapi.HistoricalTickLast{}) 43 | gob.Register(ibapi.HistoricalSession{}) 44 | gob.Register(ibapi.IneligibilityReason{}) 45 | gob.Register(ibapi.NewsProvider{}) 46 | gob.Register(ibapi.Order{}) 47 | gob.Register(ibapi.OrderState{}) 48 | gob.Register(ibapi.PriceIncrement{}) 49 | gob.Register(ibapi.RealTimeBar{}) 50 | gob.Register(ibapi.SmartComponent{}) 51 | gob.Register(ibapi.SoftDollarTier{}) 52 | gob.Register(ibapi.TagValue{}) 53 | gob.Register(ibapi.TickAttrib{}) 54 | gob.Register(ibapi.TickAttribBidAsk{}) 55 | gob.Register(ibapi.TickAttribLast{}) 56 | gob.Register(ibapi.WshEventData{}) 57 | // gob.Register(IneligibilityReason("")) 58 | // gob.Register(FundDistributionPolicyIndicator("")) 59 | } 60 | 61 | // isErrorMsg returns true if provided msg contains "error" string. 62 | func isErrorMsg(msg string) bool { 63 | return strings.Contains(msg, "error") 64 | } 65 | 66 | // msg2Error decodes msg to ibapi CodeMsgPair 67 | func msg2Error(msg string) ibapi.CodeMsgPair { 68 | var cmp ibapi.CodeMsgPair 69 | if err := Decode(&cmp, Split(msg)[1]); err != nil { 70 | return ibapi.CodeMsgPair{Code: -1, Msg: fmt.Sprintf("error decoding error %s", msg)} 71 | } 72 | return normaliseCodeMsgPair(cmp) 73 | } 74 | 75 | // orderKey generates a unique key for an order based on client ID, order ID, or permanent ID. 76 | func orderKey(clientID int64, orderID OrderID, permID int64) string { 77 | if orderID <= 0 { 78 | return Key(permID) 79 | } 80 | return Key(clientID, orderID) 81 | } 82 | 83 | // Key constructs a unique string key from a variadic number of values, separated by a specified delimiter. 84 | // default delimiter is "::" 85 | func Key(keys ...any) string { 86 | if len(keys) == 0 { 87 | return "" 88 | } 89 | var sb strings.Builder 90 | fmt.Fprint(&sb, keys[0]) 91 | 92 | for i := 1; i < len(keys); i++ { 93 | fmt.Fprintf(&sb, "%s%v", sep, keys[i]) 94 | } 95 | 96 | return sb.String() 97 | } 98 | 99 | // Encode serializes an input value to a gob-encoded string. 100 | func Encode(e any) string { 101 | var b bytes.Buffer 102 | enc := gob.NewEncoder(&b) 103 | if err := enc.Encode(e); err != nil { 104 | log.Panic().Err(err).Any("e", e).Msg("internal encoding error") 105 | } 106 | return b.String() 107 | } 108 | 109 | // Decode deserializes a gob-encoded string back into a Go value. 110 | // 111 | // Parameters: 112 | // - e: A pointer to the target value where decoded data will be stored 113 | // - data: The gob-encoded string to be decoded 114 | func Decode(e any, data string) error { 115 | buf := bytes.NewBufferString(data) 116 | dec := gob.NewDecoder(buf) 117 | err := dec.Decode(e) 118 | return err 119 | } 120 | 121 | // Join concatenates multiple strings using the package's default separator ("::"). 122 | func Join(strs ...string) string { 123 | return strings.Join(strs, sep) 124 | } 125 | 126 | // Split divides a string into substrings using the package's default separator ("::"). 127 | func Split(str string) []string { 128 | return strings.Split(str, sep) 129 | } 130 | -------------------------------------------------------------------------------- /encoder_test.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import "testing" 4 | 5 | func TestKey(t *testing.T) { 6 | tests := []struct { 7 | input []any 8 | expected string 9 | }{ 10 | {[]any{"key1", "key2", "key3"}, "key1" + sep + "key2" + sep + "key3"}, 11 | {[]any{}, ""}, 12 | {[]any{42, "test"}, "42" + sep + "test"}, 13 | } 14 | 15 | for _, test := range tests { 16 | result := Key(test.input...) 17 | if result != test.expected { 18 | t.Errorf("Key(%v) = %v; expected %v", test.input, result, test.expected) 19 | } 20 | } 21 | } 22 | 23 | func TestEncode(t *testing.T) { 24 | pnl := Pnl{Account: "12345", ModelCode: "ABC", DailyPNL: 100.0, UnrealizedPnl: 50.0, RealizedPNL: 75.0} 25 | encoded := Encode(pnl) 26 | 27 | if encoded == "" { 28 | t.Errorf("encoded data should not be empty") 29 | } 30 | } 31 | 32 | func TestDecode(t *testing.T) { 33 | pnl := Pnl{Account: "12345", ModelCode: "ABC", DailyPNL: 100.0, UnrealizedPnl: 50.0, RealizedPNL: 75.0} 34 | encoded := Encode(pnl) 35 | 36 | var decoded Pnl 37 | err := Decode(&decoded, encoded) 38 | if err != nil { 39 | t.Errorf("expected no error during decoding, got %v", err) 40 | } 41 | if decoded != pnl { 42 | t.Errorf("decoded Pnl should match original: got %+v, want %+v", decoded, pnl) 43 | } 44 | } 45 | 46 | func TestJoin(t *testing.T) { 47 | strs := []string{"Hello", "World"} 48 | result := Join(strs...) 49 | expected := "Hello" + sep + "World" 50 | if result != expected { 51 | t.Errorf("Join did not return the expected result: got %v, want %v", result, expected) 52 | } 53 | } 54 | 55 | func TestSplit(t *testing.T) { 56 | str := "Hello" + sep + "World" 57 | result := Split(str) 58 | expected := []string{"Hello", "World"} 59 | if len(result) != len(expected) { 60 | t.Errorf("Split did not return expected slices: got %v, want %v", result, expected) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "errors" 5 | "slices" 6 | 7 | "github.com/scmhub/ibapi" 8 | ) 9 | 10 | // Exported Errors 11 | var ( 12 | ErrNotPaperTrading = errors.New("this account is not a paper trading account") 13 | ErrNotFinancialAdvisor = errors.New("this account is not a financial advisor account") 14 | ErrAmbiguousContract = errors.New("ambiguous contract") 15 | ErrNoDataSubscription = errors.New("no data subscription") 16 | ) 17 | 18 | // Internal Errors 19 | var ( 20 | errUnknowReqID = errors.New("unknown reqID") 21 | errUnknowOrder = errors.New("unknown order") 22 | errUnknowExecution = errors.New("unknown execution") 23 | errUnknowItemType = errors.New("unknown item type") 24 | errUnknownTickType = errors.New("unknown tick type") 25 | ) 26 | 27 | // TWS Errors 28 | // https://www.interactivebrokers.eu/campus/ibkr-api-page/tws-api-error-codes/ 29 | 30 | // warnigCodes are errors codes received from TWS that should be treated as warnings 31 | var warningCodes = []int64{ // 32 | 161, // Cancel attempted when order is not in a cancellable state. Order permId = // An attempt was made to cancel an order not active at the time. 33 | 162, // Historical market data Service error message. <- This error is triggered on historical data cancel. 34 | 202, // Order cancelled – Reason: An active order on the IB server was cancelled. // See Order Placement Considerations for additional information/considerations for these errors. 35 | 2104, // Market data farm connection is OK. 36 | 2106, // A historical data farm is connected. 37 | 2107, // HMDS data farm connection is inactive but should be available upon demand. 38 | 2108, // A market data farm connection has become inactive but should be available upon demand. 39 | 2119, // Market data farm is connecting. 40 | 2158, // Sec-def data farm connection is OK. 41 | 10167, // Requested market data is not subscribed. Displaying delayed market data. 42 | 10197, // No market data during competing live session. 43 | } 44 | 45 | func IsWarning(cmp ibapi.CodeMsgPair) bool { 46 | return slices.Contains(warningCodes, cmp.Code) 47 | } 48 | 49 | var ( 50 | // Errors 51 | ErrMaxNbTickerReached = ibapi.CodeMsgPair{Code: 101, Msg: "Max number of tickers has been reached."} 52 | ErrMissingReportType = ibapi.CodeMsgPair{Code: 430, Msg: "The fundamentals data for the security specified is not available."} 53 | ErrNewsFeedNotAllowed = ibapi.CodeMsgPair{Code: 10276, Msg: "News feed is not allowed."} 54 | ErrAdditionalSubscriptionRequired = ibapi.CodeMsgPair{Code: 10089, Msg: "Requested market data requires additional subscription for API."} 55 | ErrPartlyNotSubsribed = ibapi.CodeMsgPair{Code: 10090, Msg: "Part of requested market data is not subscribed."} 56 | 57 | // Warnings 58 | WarnDelayedMarketData = ibapi.CodeMsgPair{Code: 10167, Msg: "Requested market data is not subscribed. Displaying delayed market data."} 59 | WarnCompetingLiveSession = ibapi.CodeMsgPair{Code: 10197, Msg: "No market data during competing live session."} 60 | ) 61 | 62 | // normaliseCodeMsgPair nomalise IB errors 63 | // IB errors can have different error messages for a given code. 64 | // We get rid of the original message in order to have consitent errors. 65 | // Original message can be seen in the logs. 66 | func normaliseCodeMsgPair(cmp ibapi.CodeMsgPair) ibapi.CodeMsgPair { 67 | switch cmp.Code { 68 | case 2104, 2106, 2107, 2108, 21019: 69 | return cmp 70 | case 10167: 71 | return WarnDelayedMarketData 72 | case 10197: 73 | return WarnCompetingLiveSession 74 | case 430: 75 | return ErrMissingReportType 76 | case 10089: 77 | return ErrAdditionalSubscriptionRequired 78 | case 10090: 79 | return ErrPartlyNotSubsribed 80 | default: 81 | return cmp 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /examples/bar_data/bar_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/scmhub/ibsync" 9 | ) 10 | 11 | // Connection constants for the Interactive Brokers API. 12 | const ( 13 | // host specifies the IB API server address 14 | host = "localhost" 15 | // port specifies the IB API server port 16 | port = 7497 17 | // clientID is the unique identifier for this client connection 18 | clientID = 5 19 | ) 20 | 21 | func main() { 22 | // We set logger for pretty logs to console 23 | log := ibsync.Logger() 24 | ibsync.SetLogLevel(int(zerolog.DebugLevel)) 25 | ibsync.SetConsoleWriter() 26 | 27 | // New IB client & Connect 28 | ib := ibsync.NewIB() 29 | 30 | err := ib.ConnectWithGracefulShutdown( 31 | ibsync.NewConfig( 32 | ibsync.WithHost(host), 33 | ibsync.WithPort(port), 34 | ibsync.WithClientID(clientID), 35 | ), 36 | ) 37 | if err != nil { 38 | log.Error().Err(err).Msg("Connect") 39 | return 40 | } 41 | defer ib.Disconnect() 42 | 43 | eurusd := ibsync.NewForex("EUR", "IDEALPRO", "USD") 44 | 45 | // HeadStamp 46 | headStamp, err := ib.ReqHeadTimeStamp(eurusd, "MIDPOINT", true, 1) 47 | if err != nil { 48 | log.Error().Err(err).Msg("ReqHeadTimeStamp") 49 | return 50 | } 51 | fmt.Printf("The Headstamp for EURSUD is %v\n", headStamp) 52 | 53 | lastWednesday12ESTString := ibsync.FormatIBTimeUSEastern(ibsync.LastWednesday12EST()) 54 | 55 | // Historical Data 56 | endDateTime := lastWednesday12ESTString // format "yyyymmdd HH:mm:ss ttt", where "ttt" is an optional time zone 57 | duration := "1 D" // "60 S", "30 D", "13 W", "6 M", "10 Y". The unit must be specified (S for seconds, D for days, W for weeks, etc.). 58 | barSize := "15 mins" // "1 secs", "5 secs", "10 secs", "15 secs", "30 secs", "1 min", "2 mins", "5 mins", etc. 59 | whatToShow := "MIDPOINT" // "TRADES", "MIDPOINT", "BID", "ASK", "BID_ASK", "HISTORICAL_VOLATILITY", etc. 60 | useRTH := true // `true` limits data to regular trading hours (RTH), `false` includes all data. 61 | formatDate := 1 // `1` for the "yyyymmdd HH:mm:ss ttt" format, or `2` for Unix timestamps. 62 | barChan, _ := ib.ReqHistoricalData(eurusd, endDateTime, duration, barSize, whatToShow, useRTH, formatDate) 63 | 64 | var bars []ibsync.Bar 65 | for bar := range barChan { 66 | fmt.Println(bar) 67 | bars = append(bars, bar) 68 | } 69 | 70 | fmt.Println("Number of bars:", len(bars)) 71 | fmt.Println("First Bar", bars[0]) 72 | fmt.Println("Last Bar", bars[len(bars)-1]) 73 | 74 | // Historical Data with realtime Updates 75 | duration = "60 S" 76 | barSize = "1 secs" 77 | barChan, cancel := ib.ReqHistoricalDataUpToDate(eurusd, duration, barSize, whatToShow, useRTH, formatDate) 78 | 79 | go func() { 80 | for bar := range barChan { 81 | fmt.Println(bar) 82 | bars = append(bars, bar) 83 | } 84 | 85 | }() 86 | 87 | time.Sleep(10 * time.Second) 88 | cancel() 89 | 90 | // Historical schedule 91 | historicalSchedule, err := ib.ReqHistoricalSchedule(eurusd, endDateTime, duration, useRTH) 92 | if err != nil { 93 | log.Error().Err(err).Msg("ReqHistoricalSchedule") 94 | return 95 | } 96 | 97 | fmt.Printf("Historical schedule start date: %v, end date: %v, time zone: %v\n", historicalSchedule.StartDateTime, historicalSchedule.EndDateTime, historicalSchedule.TimeZone) 98 | for i, session := range historicalSchedule.Sessions { 99 | fmt.Printf("session %v: %v\n", i, session) 100 | } 101 | 102 | // Real time bars 103 | whatToShow = "MIDPOINT" // "TRADES", "MIDPOINT", "BID" or "ASK" 104 | rtBarChan, cancel := ib.ReqRealTimeBars(eurusd, 5, whatToShow, useRTH) 105 | 106 | var rtBars []ibsync.RealTimeBar 107 | go func() { 108 | for rtBar := range rtBarChan { 109 | fmt.Println(rtBar) 110 | rtBars = append(rtBars, rtBar) 111 | } 112 | 113 | }() 114 | 115 | time.Sleep(10 * time.Second) 116 | cancel() 117 | 118 | fmt.Println("Number of RT bars:", len(rtBars)) 119 | fmt.Println("First RT Bar", rtBars[0]) 120 | fmt.Println("Last RT Bar", rtBars[len(rtBars)-1]) 121 | 122 | time.Sleep(1 * time.Second) 123 | log.Info().Msg("Good Bye!!!") 124 | } 125 | -------------------------------------------------------------------------------- /examples/basics/basics.go: -------------------------------------------------------------------------------- 1 | // the basics file demonstrates the usage of the ibsync library for 2 | // connecting to Interactive Brokers API and retrieving various 3 | // financial account and market information. 4 | // 5 | // This example script showcases: 6 | // - Configuring and connecting to Interactive Brokers 7 | // - Logging configuration 8 | // - Retrieving account-related information 9 | // - Subscribing to and managing real-time data streams 10 | // - Handling trades, orders, and executions 11 | // - Receiving news bulletins 12 | // 13 | // Key operations include: 14 | // - Establishing a connection with specific configuration 15 | // - Fetching managed accounts 16 | // - Retrieving account values and portfolio 17 | // - Subscribing to positions and P&L 18 | // - Monitoring trades and orders 19 | // - Requesting and processing news bulletins 20 | package main 21 | 22 | import ( 23 | "context" 24 | "fmt" 25 | "time" 26 | 27 | "github.com/rs/zerolog" 28 | "github.com/scmhub/ibsync" 29 | ) 30 | 31 | // Connection constants for the Interactive Brokers API. 32 | const ( 33 | // host specifies the IB API server address 34 | host = "localhost" 35 | // port specifies the IB API server port 36 | port = 7497 37 | // clientID is the unique identifier for this client connection 38 | clientID = 5 39 | ) 40 | 41 | func main() { 42 | // We get ibsync logger 43 | log := ibsync.Logger() 44 | // Set log level to Debug 45 | ibsync.SetLogLevel(int(zerolog.InfoLevel)) 46 | // Set logger for pretty logs to console 47 | ibsync.SetConsoleWriter() 48 | 49 | // New IB client & Connect 50 | ib := ibsync.NewIB() 51 | 52 | // Connect ib with config 53 | err := ib.Connect( 54 | ibsync.NewConfig( 55 | ibsync.WithHost(host), 56 | ibsync.WithPort(port), 57 | ibsync.WithClientID(clientID), 58 | // ibsync.WithoutSync(), 59 | ibsync.WithTimeout(10*time.Second), 60 | ), 61 | ) 62 | if err != nil { 63 | log.Error().Err(err).Msg("Connect") 64 | return 65 | } 66 | defer ib.Disconnect() 67 | 68 | // Managed accounts 69 | managedAccounts := ib.ManagedAccounts() 70 | log.Info().Strs("accounts", managedAccounts).Msg("Managed accounts list") 71 | 72 | // Account Values 73 | accountValues := ib.AccountValues() 74 | fmt.Println("acount values", ibsync.AccountSummary(accountValues)) 75 | 76 | // Account Summary 77 | accountSummary := ib.AccountSummary() 78 | fmt.Println("account summary", accountSummary) 79 | 80 | // Portfolio 81 | portfolio := ib.Portfolio() 82 | fmt.Println("portfolio", portfolio) 83 | 84 | // Positions 85 | // Subscribe to Postion 86 | ib.ReqPositions() 87 | // Position Channel 88 | posChan := ib.PositionChan() 89 | go func() { 90 | for pos := range posChan { 91 | fmt.Println("Position from chan:", pos) 92 | } 93 | }() 94 | time.Sleep(1 * time.Second) 95 | positions := ib.Positions() 96 | fmt.Println("positions", positions) 97 | // Cancel position subscription 98 | ib.CancelPositions() 99 | 100 | // Pnl 101 | // Subscribe to P&L 102 | ib.ReqPnL(managedAccounts[0], "") 103 | // P&l Channel 104 | pnlChan := ib.PnlChan(managedAccounts[0], "") 105 | go func() { 106 | for pnl := range pnlChan { 107 | fmt.Println("P&L from chan:", pnl) 108 | } 109 | }() 110 | time.Sleep(1 * time.Second) 111 | // Get P&L for specific account 112 | pnl := ib.Pnl(managedAccounts[0], "") 113 | fmt.Println("pnl", pnl) 114 | // Cancel P&l subscription 115 | ib.CancelPnL(managedAccounts[0], "") 116 | 117 | // Trades 118 | trades := ib.Trades() 119 | fmt.Println("trades", trades) 120 | openTrades := ib.OpenTrades() 121 | fmt.Println("open trades", openTrades) 122 | 123 | // Orders 124 | orders := ib.Orders() 125 | fmt.Println("orders", orders) 126 | openOrders := ib.OpenOrders() 127 | fmt.Println("open orders", openOrders) 128 | 129 | // Get previous sessions executions and fills 130 | _, err = ib.ReqExecutions() 131 | if err != nil { 132 | log.Error().Err(err).Msg("ReqExecutions") 133 | return 134 | } 135 | 136 | // Fills 137 | fills := ib.Fills() 138 | fmt.Println("fills", fills) 139 | 140 | // Executions 141 | executions := ib.Executions() 142 | fmt.Println("executions", executions) 143 | 144 | // User info 145 | whiteBrandingId, _ := ib.ReqUserInfo() 146 | fmt.Println("whiteBrandingId", whiteBrandingId) 147 | 148 | // News bulletins Channel 149 | nbChan := ib.NewsBulletinsChan() 150 | ctx, cancel := context.WithCancel(ib.Context()) 151 | defer cancel() 152 | go func() { 153 | var i int 154 | for { 155 | select { 156 | case <-ctx.Done(): 157 | return 158 | case bulletin, ok := <-nbChan: 159 | if !ok { 160 | return 161 | } 162 | fmt.Printf("News bulletin from channel %v: %v\n", i, bulletin) 163 | i++ 164 | } 165 | } 166 | }() 167 | 168 | // Request news bulletins 169 | ib.ReqNewsBulletins(true) 170 | 171 | // Wait for bulletins 172 | time.Sleep(10 * time.Second) 173 | 174 | // Recorded bulletins 175 | bulletins := ib.NewsBulletins() 176 | 177 | for i, bulletin := range bulletins { 178 | fmt.Printf("News bulletin %v: %v\n", i, bulletin) 179 | } 180 | ib.CancelNewsBulletins() 181 | 182 | time.Sleep(1 * time.Second) 183 | log.Info().Msg("Good Bye!!!") 184 | } 185 | -------------------------------------------------------------------------------- /examples/contract_details/contract_details.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/rs/zerolog" 9 | "github.com/scmhub/ibsync" 10 | ) 11 | 12 | // Connection constants for the Interactive Brokers API. 13 | const ( 14 | // host specifies the IB API server address 15 | host = "localhost" 16 | // port specifies the IB API server port 17 | port = 7497 18 | // clientID is the unique identifier for this client connection 19 | clientID = 5 20 | ) 21 | 22 | func main() { 23 | // We set logger for pretty logs to console 24 | log := ibsync.Logger() 25 | ibsync.SetLogLevel(int(zerolog.TraceLevel)) 26 | ibsync.SetConsoleWriter() 27 | 28 | // New IB client & Connect 29 | ib := ibsync.NewIB() 30 | 31 | err := ib.Connect( 32 | ibsync.NewConfig( 33 | ibsync.WithHost(host), 34 | ibsync.WithPort(port), 35 | ibsync.WithClientID(clientID), 36 | ), 37 | ) 38 | if err != nil { 39 | log.Error().Err(err).Msg("Connect") 40 | return 41 | } 42 | defer ib.Disconnect() 43 | 44 | // Request matching symbols 45 | 46 | cdescs, err := ib.ReqMatchingSymbols("amd") 47 | if err != nil { 48 | log.Error().Err(err).Msg("request matching symbols") 49 | } 50 | for i, cdesc := range cdescs { 51 | fmt.Printf("Contract description %v: %v\n", i, cdesc) 52 | } 53 | 54 | // Request contract details 55 | 56 | // NewStock("XXX", "XXX", "XXX") -> No security definition has been found 57 | amd := ibsync.NewStock("XXX", "XXX", "XXX") 58 | cds, err := ib.ReqContractDetails(amd) 59 | if err != nil { 60 | log.Error().Err(err).Msg("request contract details") 61 | } 62 | fmt.Println("number of contract found for request NewStock(\"AMD\", \"\", \"\") :", len(cds)) 63 | 64 | // NewStock("AMD", "", "") 65 | amd = ibsync.NewStock("AMD", "", "") 66 | cds, err = ib.ReqContractDetails(amd) 67 | if err != nil { 68 | log.Error().Err(err).Msg("request contract details") 69 | } 70 | fmt.Println("number of contract found for request NewStock(\"AMD\", \"\", \"\") :", len(cds)) 71 | 72 | // NewStock("AMD", "", "USD") 73 | amd = ibsync.NewStock("AMD", "", "USD") 74 | cds, err = ib.ReqContractDetails(amd) 75 | if err != nil { 76 | log.Error().Err(err).Msg("request contract details") 77 | } 78 | fmt.Println("number of contract found for request NewStock(\"AMD\", \"\", \"USD\") :", len(cds)) 79 | 80 | // NewStock("AMD", "SMART", "USD") 81 | amd = ibsync.NewStock("AMD", "SMART", "USD") 82 | cds, err = ib.ReqContractDetails(amd) 83 | if err != nil { 84 | log.Error().Err(err).Msg("request contract details") 85 | } 86 | fmt.Println("number of contract found for request NewStock(\"AMD\", \"SMART\", \"USD\") :", len(cds)) 87 | fmt.Println(cds[0]) 88 | 89 | // get all US T-Notes 90 | tNote := ibsync.NewBond("US-T", "SMART", "USD") 91 | cds, err = ib.ReqContractDetails(tNote) 92 | if err != nil { 93 | log.Error().Err(err).Msg("request contract details") 94 | } 95 | fmt.Println("number of contract found for request NewBond(\"US-T\", \"SMART\", \"USD\") :", len(cds)) 96 | // Get the 10 years Notes 97 | for i, cd := range cds { 98 | split := strings.Split(cd.DescAppend, " ") 99 | maturity, err := time.Parse("01/02/06", split[len(split)-1]) 100 | if err != nil { 101 | log.Error().Err(err).Msg("couldn't parse maturity") 102 | } 103 | if maturity.Year() == time.Now().Year()+10 { 104 | fmt.Printf("bond %v: %v\n", i, cd) 105 | } 106 | 107 | } 108 | 109 | // Qualify contract 110 | 111 | // NewStock("XXX", "XXX", "XXX") -> No security definition has been found 112 | amd = ibsync.NewStock("XXX", "XXX", "XXX") 113 | err = ib.QualifyContract(amd) 114 | if err != nil { 115 | log.Error().Err(err).Msg("qualify contract details") 116 | } 117 | 118 | // NewStock("AMD", "", "") -> ambiguous contract 119 | amd = ibsync.NewStock("AMD", "", "") 120 | err = ib.QualifyContract(amd) 121 | if err != nil { 122 | log.Error().Err(err).Msg("qualify contract details") 123 | } 124 | 125 | // Qualifiable 126 | amd = ibsync.NewStock("AMD", "SMART", "USD") 127 | log.Info().Stringer("AMD", amd).Msg("AMD before qualifiying") 128 | err = ib.QualifyContract(amd) 129 | if err != nil { 130 | log.Error().Err(err).Msg("qualify contract details") 131 | } 132 | log.Info().Stringer("AMD", amd).Msg("AMD after qualifiying") 133 | 134 | // Qualify bond from CUSIP 135 | tNote = ibsync.NewBond("91282CLW9", "SMART", "USD") 136 | err = ib.QualifyContract(tNote) 137 | if err != nil { 138 | log.Error().Err(err).Msg("qualify contract details") 139 | } 140 | log.Info().Stringer("91282CLW9", tNote).Msg("US 10 years Notes") 141 | 142 | log.Info().Msg("Good Bye!!!") 143 | } 144 | -------------------------------------------------------------------------------- /examples/market_depth/market_depth.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/scmhub/ibsync" 9 | ) 10 | 11 | // Connection constants for the Interactive Brokers API. 12 | const ( 13 | // host specifies the IB API server address 14 | host = "localhost" 15 | // port specifies the IB API server port 16 | port = 7497 17 | // clientID is the unique identifier for this client connection 18 | clientID = 5 19 | ) 20 | 21 | func main() { 22 | // We get ibsync logger 23 | log := ibsync.Logger() 24 | // Set log level to Debug 25 | ibsync.SetLogLevel(int(zerolog.DebugLevel)) 26 | // Set logger for pretty logs to console 27 | ibsync.SetConsoleWriter() 28 | 29 | // New IB client & Connect 30 | ib := ibsync.NewIB() 31 | 32 | // Connect ib with config 33 | err := ib.Connect( 34 | ibsync.NewConfig( 35 | ibsync.WithHost(host), 36 | ibsync.WithPort(port), 37 | ibsync.WithClientID(clientID), 38 | ), 39 | ) 40 | if err != nil { 41 | log.Error().Err(err).Msg("Connect") 42 | return 43 | } 44 | defer ib.Disconnect() 45 | 46 | // Market depth exchange list 47 | mktDepthsExchanges, err := ib.ReqMktDepthExchanges() 48 | if err != nil { 49 | log.Error().Err(err).Msg("ReqMktDepthExchanges") 50 | return 51 | } 52 | for i, mde := range mktDepthsExchanges { 53 | fmt.Printf("Depth market data description %v: %v\n", i, mde) 54 | } 55 | 56 | eurusd := ibsync.NewForex("EUR", "IDEALPRO", "USD") 57 | ib.QualifyContract(eurusd) 58 | 59 | // Request Market depth 60 | ticker, err := ib.ReqMktDepth(eurusd, 10, false) 61 | if err != nil { 62 | log.Error().Err(err).Msg("ReqMktDepth") 63 | return 64 | } 65 | 66 | time.Sleep(1 * time.Second) 67 | 68 | // Cancel Market depth 69 | err = ib.CancelMktDepth(eurusd, false) 70 | if err != nil { 71 | log.Error().Err(err).Msg("CancelMktDepth") 72 | return 73 | } 74 | bids := ticker.DomBids() 75 | asks := ticker.DomAsks() 76 | fmt.Println("DOM") 77 | for i := range min(len(bids), len(asks)) { 78 | fmt.Printf("level %v: Bid: %v | Ask %v\n", i, bids[i], asks[i]) 79 | } 80 | 81 | log.Info().Msg("Good Bye!!!") 82 | } 83 | -------------------------------------------------------------------------------- /examples/option_chain/option_chain.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "time" 7 | 8 | "github.com/scmhub/ibsync" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | // Connection constants for the Interactive Brokers API. 13 | const ( 14 | // host specifies the IB API server address 15 | host = "localhost" 16 | // port specifies the IB API server port 17 | port = 7497 18 | // clientID is the unique identifier for this client connection 19 | clientID = 5 20 | ) 21 | 22 | func main() { 23 | // We set logger for pretty logs to console 24 | log := ibsync.Logger() 25 | ibsync.SetLogLevel(int(zerolog.DebugLevel)) 26 | ibsync.SetConsoleWriter() 27 | 28 | // New IB client & Connect 29 | ib := ibsync.NewIB() 30 | 31 | err := ib.Connect( 32 | ibsync.NewConfig( 33 | ibsync.WithHost(host), 34 | ibsync.WithPort(port), 35 | ibsync.WithClientID(clientID), 36 | ), 37 | ) 38 | if err != nil { 39 | log.Fatal().Err(err).Msg("Connect") 40 | } 41 | defer ib.Disconnect() 42 | 43 | // Requests delayed "frozen" data for a user without market data subscriptions. 44 | ib.ReqMarketDataType(4) 45 | 46 | // S&P index 47 | spx := ibsync.NewIndex("SPX", "CBOE", "USD") 48 | 49 | err = ib.QualifyContract(spx) 50 | if err != nil { 51 | panic(fmt.Errorf("qualify spx: %v", err)) 52 | } 53 | 54 | spxTicker, err := ib.Snapshot(spx) 55 | fmt.Println(spxTicker) 56 | if err != nil && err != ibsync.WarnDelayedMarketData { 57 | panic(fmt.Errorf("spx snapshot: %v", err)) 58 | } 59 | spxPrice := spxTicker.MarketPrice() 60 | fmt.Println("SPX market price:", spxPrice) 61 | 62 | // Options chain 63 | chains, err := ib.ReqSecDefOptParams(spx.Symbol, "", spx.SecType, spx.ConID) 64 | if err != nil { 65 | panic(fmt.Errorf("security defimition option parameters: %v", err)) 66 | } 67 | 68 | for i, oc := range chains { 69 | fmt.Printf("Option chain %v: %v\n", i, oc) 70 | } 71 | 72 | maturity := time.Now().AddDate(0, 3, 0).Format("200601") // three month from now 73 | strike := math.Round(spxPrice/250) * 250 74 | call := ibsync.NewOption("SPX", maturity, strike, "C", "SMART", "100", "USD") 75 | call.TradingClass = "SPX" 76 | 77 | err = ib.QualifyContract(call) 78 | if err != nil { 79 | panic(fmt.Errorf("qualify option: %v", err)) 80 | } 81 | 82 | // Get option market price (if available) or model price. 83 | callTicker, err := ib.Snapshot(call) 84 | if err != nil && err != ibsync.WarnCompetingLiveSession && err != ibsync.WarnDelayedMarketData { 85 | panic(fmt.Errorf("call snapshot: %v", err)) 86 | } 87 | 88 | greeks := callTicker.Greeks() 89 | fmt.Println(callTicker) 90 | fmt.Printf("Option price: %.2f, implied volatility: %.2f%%, vega:%.2f\n", greeks.OptPrice, greeks.ImpliedVol*100, greeks.Vega) 91 | 92 | // Option price for a given implied volatility & underlying price 93 | optionPrice, err := ib.CalculateOptionPrice(call, greeks.ImpliedVol+0.01, greeks.UndPrice) 94 | if err != nil { 95 | panic(fmt.Errorf("calculate option price: %v", err)) 96 | } 97 | fmt.Printf("Option price: %.2f, was expecting: %.2f\n", optionPrice.OptPrice, greeks.OptPrice+greeks.Vega) 98 | 99 | // Implied Volatility for a given option price & underlying price 100 | impliedVol, err := ib.CalculateImpliedVolatility(call, greeks.OptPrice+greeks.Vega, greeks.UndPrice) 101 | if err != nil { 102 | panic(fmt.Errorf("calculate implied volatility: %v", err)) 103 | } 104 | fmt.Printf("Implied Volatility: %.2f%%, was expecting: %.2f%%\n", impliedVol.ImpliedVol*100, greeks.ImpliedVol*100+1) 105 | 106 | log.Info().Msg("Good Bye!!!") 107 | } 108 | -------------------------------------------------------------------------------- /examples/orders/orders.go: -------------------------------------------------------------------------------- 1 | // This package demonstrates how to interact with ibsunc f 2 | // for forex trading operations including placing, modifying, and canceling orders. 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | "time" 9 | 10 | "github.com/scmhub/ibsync" 11 | ) 12 | 13 | // Connection constants for the Interactive Brokers API. 14 | const ( 15 | // host specifies the IB API server address 16 | host = "localhost" 17 | // port specifies the IB API server port 18 | port = 7497 19 | // clientID is the unique identifier for this client connection 20 | clientID = 5 21 | ) 22 | 23 | // printTrades prints the current state of trades. 24 | // It displays both all trades and open trades separately. 25 | // 26 | // Parameters: 27 | // - header: descriptive text to identify the trading state 28 | // - count: sequential number for tracking multiple trade printouts 29 | // - printPrevious: when true, prints all trades including those with OrderID=0 30 | // - ib: pointer to the IB connection instance 31 | func printTrades(header string, count int, printPrevious bool, ib *ibsync.IB) { 32 | fmt.Println("*** *** ***") 33 | // Header 34 | fmt.Printf("*%v* *** %v ***\n", count, header) 35 | // Print Trades 36 | trades := ib.Trades() 37 | fmt.Printf("*%v* # trades: %v\n", count, len(trades)) 38 | for i, t := range trades { 39 | if printPrevious || t.Order.OrderID != 0 { 40 | fmt.Printf("*%v-%v* %v\n", count, i, t) 41 | } 42 | } 43 | fmt.Println("***") 44 | // Print Open Trades 45 | openTrades := ib.OpenTrades() 46 | fmt.Printf("*%v* # open trades: %v\n", count, len(openTrades)) 47 | for i, t := range openTrades { 48 | if printPrevious || t.Order.OrderID != 0 { 49 | fmt.Printf("*%v-%v* %v\n", count, i, t) 50 | } 51 | } 52 | fmt.Println("*** *** ***") 53 | } 54 | 55 | // main demonstrates a complete workflow for forex trading using the IB API. 56 | // It includes: 57 | // - Establishing connection to IB 58 | // - Verifying paper trading account 59 | // - Getting forex midpoint prices 60 | // - Placing buy and sell orders 61 | // - Modifying existing orders 62 | // - Canceling specific orders 63 | // - Performing global cancellation 64 | // 65 | // The example uses EUR/USD currency pair and includes error handling 66 | // and proper cleanup with deferred disconnect. 67 | func main() { 68 | // We set logger for pretty logs to console 69 | log := ibsync.Logger() 70 | ibsync.SetLogLevel(0) 71 | ibsync.SetConsoleWriter() 72 | 73 | // New IB client & Connect 74 | ib := ibsync.NewIB() 75 | 76 | err := ib.Connect( 77 | ibsync.NewConfig( 78 | ibsync.WithHost(host), 79 | ibsync.WithPort(port), 80 | ibsync.WithClientID(clientID), 81 | ), 82 | ) 83 | if err != nil { 84 | log.Error().Err(err).Msg("Connect") 85 | return 86 | } 87 | defer ib.Disconnect() 88 | 89 | // Make sure that the account is a paper account. 90 | if !ib.IsPaperAccount() { 91 | log.Warn().Msg("This is not a paper trading account! Exiting!") 92 | log.Warn().Msg("Good Bye!!!") 93 | return 94 | } 95 | 96 | // Create EUR/USD forex instrument 97 | eurusd := ibsync.NewForex("EUR", "IDEALPRO", "USD") 98 | 99 | // Get current market midpoint 100 | eurusdMidpoint, err := ib.MidPoint(eurusd) 101 | if err != nil { 102 | log.Error().Err(err).Msg("Midpoint") 103 | return 104 | } 105 | fmt.Println("EURUSD midpoint: ", eurusdMidpoint) 106 | 107 | // Calculate order price at 95% of midpoint and rounding to three decimals. 108 | // sell orders will be filled and buy orders will be submitted 109 | orderprice := math.Round(95*eurusdMidpoint.MidPoint) / 100 110 | 111 | // Print Trades on server before placing orders 112 | printTrades("Trades from server before placing orders", 1, true, ib) 113 | 114 | // Place Orders 115 | 116 | // Place sell order 117 | fmt.Println("*** Place Sell order ***") 118 | sellOrder := ibsync.LimitOrder("SELL", ibsync.StringToDecimal("20000"), orderprice) 119 | sellTrade := ib.PlaceOrder(eurusd, sellOrder) 120 | 121 | fmt.Println("submitted sell trade:", sellTrade) 122 | 123 | <-sellTrade.Done() 124 | fmt.Println("The sell trade is done!!!") 125 | 126 | // Place buy orders 127 | fmt.Println("*** Place Buy orders ***") 128 | buyOrder := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20001"), orderprice) 129 | buyTrade := ib.PlaceOrder(eurusd, buyOrder) 130 | 131 | fmt.Println("submitted buy trade:", buyTrade) 132 | go func() { 133 | <-buyTrade.Done() 134 | fmt.Println("The buy trade is done!!!") 135 | }() 136 | 137 | // Place additional buy orders 138 | buyOrder2 := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20002"), orderprice) 139 | buyTrade2 := ib.PlaceOrder(eurusd, buyOrder2) 140 | buyOrder3 := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20003"), orderprice) 141 | buyTrade3 := ib.PlaceOrder(eurusd, buyOrder3) 142 | buyOrder4 := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20004"), orderprice) 143 | buyTrade4 := ib.PlaceOrder(eurusd, buyOrder4) 144 | buyOrder5 := ibsync.LimitOrder("BUY", ibsync.StringToDecimal("20005"), orderprice) 145 | buyTrade5 := ib.PlaceOrder(eurusd, buyOrder5) 146 | 147 | // Wait one second and check trades 148 | fmt.Println("*** Updated trades ***") 149 | time.Sleep(1 * time.Second) 150 | fmt.Println("updated buy trade:", buyTrade) 151 | fmt.Println("updated sell trade:", sellTrade) 152 | 153 | // Print Trades after placing orders 154 | time.Sleep(1 * time.Second) 155 | printTrades("Trades after placing orders", 2, false, ib) 156 | 157 | // Modify order 158 | buyOrder2.LmtPrice = math.Round(105*eurusdMidpoint.MidPoint) / 100 159 | mofifiedbuyTrade2 := ib.PlaceOrder(eurusd, buyOrder2) 160 | <-buyTrade2.Done() 161 | <-mofifiedbuyTrade2.Done() 162 | 163 | // Print Trades after modifying order 164 | printTrades("Trades after modifying order", 3, false, ib) 165 | 166 | // Cancel Orders 167 | 168 | // Cancel buy Order 169 | fmt.Println("*** Cancel buy order ***") 170 | ib.CancelOrder(buyOrder3, ibsync.NewOrderCancel()) 171 | <-buyTrade3.Done() 172 | fmt.Println("cancelled buy trade", buyTrade3) 173 | 174 | // Print Trades after canceling order 175 | printTrades("Trades after canceling order", 4, false, ib) 176 | 177 | // Global Cancel 178 | fmt.Println("*** Global cancel ***") 179 | ib.ReqGlobalCancel() 180 | <-buyTrade4.Done() 181 | fmt.Println("*** status of trade 4:", buyTrade4.OrderStatus.Status) 182 | <-buyTrade5.Done() 183 | fmt.Println("*** status of trade 5:", buyTrade5.OrderStatus.Status) 184 | 185 | // Print Trades after global cancel 186 | time.Sleep(1 * time.Second) 187 | printTrades("Trades after global cancel", 5, false, ib) 188 | 189 | // Executions & Fills 190 | 191 | // Executions & Fills from state, i.e for this session 192 | fills := ib.Fills() 193 | fmt.Printf("*** Executions & Fills from state, total number: %v ***\n", len(fills)) 194 | execs := ib.Executions() 195 | for i, exec := range execs { 196 | fmt.Printf("*%v* %v \n", i, exec) 197 | } 198 | for i, fill := range fills { 199 | fmt.Printf("*%v* %v \n", i, fill) 200 | } 201 | 202 | // Executions & Fills after filtered request 203 | ef := ibsync.NewExecutionFilter() 204 | ef.Side = "BUY" 205 | execs, err = ib.ReqExecutions(ef) 206 | if err != nil { 207 | log.Error().Err(err).Msg("Request Executions") 208 | return 209 | } 210 | fmt.Printf("*** Executions & Fills after filtered request, total number: %v ***\n ***", len(execs)) 211 | for i, exec := range execs { 212 | fmt.Printf("*%v* %v \n", i, exec) 213 | } 214 | fills, err = ib.ReqFills() 215 | if err != nil { 216 | log.Error().Err(err).Msg("Request Fills") 217 | return 218 | } 219 | for i, fill := range fills { 220 | fmt.Printf("*%v* %v \n", i, fill) 221 | } 222 | 223 | // Executions & Fills after no filter request 224 | execs, err = ib.ReqExecutions() 225 | if err != nil { 226 | log.Error().Err(err).Msg("Request Executions") 227 | return 228 | } 229 | fmt.Printf("*** Executions & Fills after no filter request, total number: %v ***\n ***", len(execs)) 230 | for i, exec := range execs { 231 | fmt.Printf("*%v* %v \n", i, exec) 232 | } 233 | fills, err = ib.ReqFills() 234 | if err != nil { 235 | log.Error().Err(err).Msg("Request Fills") 236 | return 237 | } 238 | for i, fill := range fills { 239 | fmt.Printf("*%v* %v \n", i, fill) 240 | } 241 | 242 | log.Info().Msg("Good Bye!!!") 243 | } 244 | -------------------------------------------------------------------------------- /examples/pnl/pnl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/scmhub/ibsync" 8 | ) 9 | 10 | // Connection constants for the Interactive Brokers API. 11 | const ( 12 | // host specifies the IB API server address 13 | host = "localhost" 14 | // port specifies the IB API server port 15 | port = 7497 16 | // clientID is the unique identifier for this client connection 17 | clientID = 5 18 | ) 19 | 20 | func main() { 21 | // We set logger for pretty logs to console 22 | log := ibsync.Logger() 23 | //ibsync.SetLogLevel(int(zerolog.DebugLevel)) 24 | ibsync.SetConsoleWriter() 25 | 26 | // New IB client & Connect 27 | ib := ibsync.NewIB() 28 | 29 | err := ib.Connect( 30 | ibsync.NewConfig( 31 | ibsync.WithHost(host), 32 | ibsync.WithPort(port), 33 | ibsync.WithClientID(clientID), 34 | ), 35 | ) 36 | if err != nil { 37 | log.Error().Err(err).Msg("Connect") 38 | return 39 | } 40 | defer ib.Disconnect() 41 | 42 | // Retrieve the list of managed accounts. 43 | managedAccounts := ib.ManagedAccounts() 44 | log.Info().Strs("accounts", managedAccounts).Msg("Managed accounts list") 45 | 46 | account := managedAccounts[0] // Use the first managed account for PnL requests. 47 | modelCode := "" // Optional model code 48 | contractID := int64(756733) // Example contract ID 49 | 50 | // Request and handle PnL updates for the account. 51 | ib.ReqPnL(account, modelCode) 52 | pnlChan := ib.PnlChan(account, modelCode) 53 | 54 | // Start a goroutine to listen for PnL updates. 55 | go func() { 56 | for pnl := range pnlChan { 57 | fmt.Println("Received PnL from channel:", pnl) 58 | } 59 | }() 60 | 61 | // Allow time for updates and display current PnL. 62 | time.Sleep(5 * time.Second) 63 | pnl := ib.Pnl(account, modelCode) 64 | fmt.Println("Current PnL:", pnl) 65 | 66 | // Cancel PnL requests and check the status. 67 | ib.CancelPnL(account, modelCode) 68 | time.Sleep(2 * time.Second) 69 | pnl = ib.Pnl(account, modelCode) 70 | fmt.Println("PnL after cancellation:", pnl) 71 | 72 | // Request and handle single PnL data. 73 | ib.ReqPnLSingle(account, modelCode, contractID) 74 | pnlSingleChan := ib.PnlSingleChan(account, modelCode, contractID) 75 | 76 | // Start a goroutine to listen for single PnL updates. 77 | go func() { 78 | for pnlSingle := range pnlSingleChan { 79 | fmt.Println("Received single PnL from channel:", pnlSingle) 80 | } 81 | }() 82 | 83 | // Allow time for updates and display current single PnL. 84 | time.Sleep(5 * time.Second) 85 | pnlSingle := ib.PnlSingle(account, modelCode, contractID) 86 | fmt.Println("Current single PnL:", pnlSingle) 87 | 88 | // Cancel single PnL requests and check the status. 89 | ib.CancelPnLSingle(account, modelCode, contractID) 90 | time.Sleep(2 * time.Second) 91 | pnlSingle = ib.PnlSingle(account, modelCode, contractID) 92 | fmt.Println("Single PnL after cancellation:", pnlSingle) 93 | 94 | time.Sleep(1 * time.Second) // Allow time for final logs to flush. 95 | log.Info().Msg("Good Bye!!!") 96 | } 97 | -------------------------------------------------------------------------------- /examples/scanners/scanners.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/rs/zerolog" 10 | "github.com/scmhub/ibsync" 11 | ) 12 | 13 | // Connection constants for the Interactive Brokers API. 14 | const ( 15 | // host specifies the IB API server address 16 | host = "localhost" 17 | // port specifies the IB API server port 18 | port = 7497 19 | // clientID is the unique identifier for this client connection 20 | clientID = 5 21 | ) 22 | 23 | func save2File(xmlData string, filePath string) { 24 | file, err := os.Create(filePath) 25 | if err != nil { 26 | panic(err) 27 | } 28 | defer file.Close() 29 | 30 | _, err = file.WriteString(xmlData) 31 | if err != nil { 32 | panic(err) 33 | } 34 | } 35 | 36 | func parseXML(xmlData string) ([]string, error) { 37 | // Define types for XML structure 38 | type AbstractField struct { 39 | Code string `xml:"code"` 40 | } 41 | type RangeFilter struct { 42 | AbstractFields []AbstractField `xml:"AbstractField"` 43 | } 44 | type FilterList struct { 45 | RangeFilters []RangeFilter `xml:"RangeFilter"` 46 | } 47 | 48 | type ScanParameterResponse struct { 49 | XMLName xml.Name `xml:"ScanParameterResponse"` 50 | FilterLists []FilterList `xml:"FilterList"` 51 | } 52 | 53 | // Parse the XML 54 | var response ScanParameterResponse 55 | err := xml.Unmarshal([]byte(xmlData), &response) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | // Extract all values 61 | var codes []string 62 | for _, fls := range response.FilterLists { 63 | for _, rfs := range fls.RangeFilters { 64 | for _, af := range rfs.AbstractFields { 65 | codes = append(codes, af.Code) 66 | } 67 | } 68 | } 69 | 70 | return codes, nil 71 | } 72 | 73 | func main() { 74 | // We get ibsync logger 75 | log := ibsync.Logger() 76 | // Set log level to Debug 77 | ibsync.SetLogLevel(int(zerolog.DebugLevel)) 78 | // Set logger for pretty logs to console 79 | ibsync.SetConsoleWriter() 80 | 81 | // New IB client & Connect 82 | ib := ibsync.NewIB() 83 | 84 | // Connect ib with config 85 | err := ib.Connect( 86 | ibsync.NewConfig( 87 | ibsync.WithHost(host), 88 | ibsync.WithPort(port), 89 | ibsync.WithClientID(clientID), 90 | ), 91 | ) 92 | if err != nil { 93 | log.Error().Err(err).Msg("Connect") 94 | return 95 | } 96 | defer ib.Disconnect() 97 | 98 | // Scanner Parameter 99 | xml, err := ib.ReqScannerParameters() 100 | if err != nil { 101 | log.Error().Err(err).Msg("Request scanner parameters") 102 | return 103 | } 104 | // Save xml scanner parameters to scanner_parameters.xml file 105 | save2File(xml, "scanner_parameters.xml") 106 | 107 | // Scanner subscription 108 | scanSubscription := ibsync.NewScannerSubscription() 109 | scanSubscription.Instrument = "STK" 110 | scanSubscription.LocationCode = "STK.US.MAJOR" 111 | scanSubscription.ScanCode = "TOP_PERC_GAIN" 112 | 113 | scanDatas, err := ib.ReqScannerSubscription(scanSubscription) 114 | if err != nil { 115 | log.Error().Err(err).Msg("Request scanner subscription") 116 | return 117 | } 118 | fmt.Println("scanner subscription") 119 | for _, scan := range scanDatas { 120 | fmt.Printf("%v\n", scan) 121 | } 122 | 123 | // Filter scanner the old way 124 | oldFilterSubscrition := scanSubscription 125 | oldFilterSubscrition.AbovePrice = 5 126 | oldFilterSubscrition.BelowPrice = 50 127 | oldFilterScanDatas, err := ib.ReqScannerSubscription(oldFilterSubscrition) 128 | if err != nil { 129 | log.Error().Err(err).Msg("Request old filter scanner subscription") 130 | return 131 | } 132 | 133 | fmt.Println("old filter scanner subscription") 134 | for _, scan := range oldFilterScanDatas { 135 | fmt.Printf("%v\n", scan) 136 | } 137 | 138 | // Filter scanner data the new way 139 | // View all tags 140 | tags, err := parseXML(xml) 141 | if err != nil { 142 | log.Error().Err(err).Msg("parsing xml string") 143 | return 144 | } 145 | fmt.Printf("nb tags: %v, first 10 tags:%v...\n", len(tags), strings.Join(tags[:10], ", ")) 146 | 147 | // AbovPrice is now priceAbove 148 | opts := ibsync.ScannerSubscriptionOptions{ 149 | FilterOptions: []ibsync.TagValue{ 150 | {Tag: "changePercAbove", Value: "20"}, 151 | {Tag: "priceAbove", Value: "5"}, 152 | {Tag: "priceBelow", Value: "50"}, 153 | }, 154 | } 155 | 156 | newFilterScanDatas, err := ib.ReqScannerSubscription(scanSubscription, opts) 157 | if err != nil { 158 | log.Error().Err(err).Msg("Request new filter scanner subscription") 159 | return 160 | } 161 | fmt.Println("new filter scanner subscription") 162 | for i, scan := range newFilterScanDatas { 163 | fmt.Printf("%v - %v\n", i, scan) 164 | } 165 | 166 | log.Info().Msg("Good Bye!!!") 167 | } 168 | -------------------------------------------------------------------------------- /examples/tick_data/tick_data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/scmhub/ibsync" 8 | ) 9 | 10 | // Connection constants for the Interactive Brokers API. 11 | const ( 12 | // host specifies the IB API server address 13 | host = "localhost" 14 | // port specifies the IB API server port 15 | port = 7497 16 | // clientID is the unique identifier for this client connection 17 | clientID = 5 18 | ) 19 | 20 | func main() { 21 | // We get ibsync logger 22 | log := ibsync.Logger() 23 | // Set log level to Debug 24 | // ibsync.SetLogLevel(int(zerolog.DebugLevel)) 25 | // Set logger for pretty logs to console 26 | ibsync.SetConsoleWriter() 27 | 28 | // New IB client & Connect 29 | ib := ibsync.NewIB() 30 | 31 | // Connect ib with config 32 | err := ib.Connect( 33 | ibsync.NewConfig( 34 | ibsync.WithHost(host), 35 | ibsync.WithPort(port), 36 | ibsync.WithClientID(clientID), 37 | ), 38 | ) 39 | if err != nil { 40 | log.Error().Err(err).Msg("Connect") 41 | return 42 | } 43 | defer ib.Disconnect() 44 | 45 | eurusd := ibsync.NewForex("EUR", "IDEALPRO", "USD") 46 | 47 | err = ib.QualifyContract(eurusd) 48 | if err != nil { 49 | panic(fmt.Errorf("qualify eurusd: %v", err)) 50 | } 51 | 52 | // Snapshot - Market price 53 | snapshot, err := ib.Snapshot(eurusd) 54 | if err != nil { 55 | panic(fmt.Errorf("snapshot eurusd: %v", err)) 56 | } 57 | fmt.Println("Snapshot", snapshot) 58 | fmt.Println("Snapshot market price", snapshot.MarketPrice()) 59 | 60 | // Streaming Tick data 61 | eurusdTicker := ib.ReqMktData(eurusd, "") 62 | time.Sleep(5 * time.Second) 63 | ib.CancelMktData(eurusd) 64 | 65 | fmt.Println("Streaming Tick data:", eurusdTicker) 66 | 67 | // Tick by tick data 68 | tickByTick := ib.ReqTickByTickData(eurusd, "BidAsk", 100, true) 69 | time.Sleep(5 * time.Second) 70 | ib.CancelTickByTickData(eurusd, "BidAsk") 71 | 72 | fmt.Println("Tick By Tick dat:", tickByTick) 73 | 74 | // Historical ticks 75 | 76 | aapl := ibsync.NewStock("AAPL", "SMART", "USD") 77 | 78 | // HistoricalTicks 79 | historicalTicks, err, done := ib.ReqHistoricalTicks(aapl, ibsync.LastWednesday12EST(), time.Time{}, 100, true, true) 80 | 81 | if err != nil { 82 | log.Error().Err(err).Msg("ReqHistoricalTicks") 83 | return 84 | } 85 | 86 | fmt.Printf("Historical Ticks number %v, is done? %v\n", len(historicalTicks), done) 87 | for i, ht := range historicalTicks { 88 | fmt.Printf("%v: %v\n", i, ht) 89 | } 90 | 91 | // HistoricalTickLast 92 | historicalTickLast, err, done := ib.ReqHistoricalTickLast(aapl, ibsync.LastWednesday12EST(), time.Time{}, 100, true, true) 93 | 94 | if err != nil { 95 | log.Error().Err(err).Msg("ReqHistoricalTicks") 96 | return 97 | } 98 | 99 | fmt.Printf("Historical Last Ticks number %v, is done? %v\n", len(historicalTickLast), done) 100 | for i, htl := range historicalTickLast { 101 | fmt.Printf("%v: %v\n", i, htl) 102 | } 103 | 104 | // HistoricalTickBidAsk 105 | historicalTickBidAsk, err, done := ib.ReqHistoricalTickBidAsk(aapl, ibsync.LastWednesday12EST(), time.Time{}, 100, true, true) 106 | 107 | if err != nil { 108 | log.Error().Err(err).Msg("ReqHistoricalTicks") 109 | return 110 | } 111 | 112 | fmt.Printf("Historical Bid Ask Ticks number %v, is done? %v\n", len(historicalTickBidAsk), done) 113 | for i, htba := range historicalTickBidAsk { 114 | fmt.Printf("%v: %v\n", i, htba) 115 | } 116 | 117 | log.Info().Msg("Good Bye!!!") 118 | } 119 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/scmhub/ibsync 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/rs/zerolog v1.34.0 7 | github.com/scmhub/ibapi v0.10.37 8 | ) 9 | 10 | require ( 11 | github.com/mattn/go-colorable v0.1.14 // indirect 12 | github.com/mattn/go-isatty v0.0.20 // indirect 13 | github.com/robaho/fixed v0.0.0-20250130054609-fd0e46fcd988 // indirect 14 | golang.org/x/sys v0.33.0 // indirect 15 | google.golang.org/protobuf v1.36.6 // indirect 16 | ) 17 | 18 | // Use local version for development 19 | // replace github.com/scmhub/ibapi => ../ibapi 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 2 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 3 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 4 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 6 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 7 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 8 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 9 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 10 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 11 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 12 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 13 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/robaho/fixed v0.0.0-20250130054609-fd0e46fcd988 h1:aHw3VW2Oe8Q2Icq1eUradihZqn/zBVlNQonXw+swAgM= 16 | github.com/robaho/fixed v0.0.0-20250130054609-fd0e46fcd988/go.mod h1:gOuZr6norIEHlPghhACq3f8PL6ZFF5uJVMOgh2/M7xQ= 17 | github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 18 | github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= 19 | github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= 20 | github.com/scmhub/ibapi v0.10.37-0.20250526133928-b77a8ef42f86 h1:LTC232IkvKFhAr7OWT/aDZ1J+G+XCIp5ZRIFBAoXzlw= 21 | github.com/scmhub/ibapi v0.10.37-0.20250526133928-b77a8ef42f86/go.mod h1:/SW/xjAIG/7f/Deu6joy025myq3SpWn458puaZGO30Y= 22 | github.com/scmhub/ibapi v0.10.37 h1:JwRNwxo2xAmizMD05uLWW+AY8ngsEwtGcnD4d0BLPN0= 23 | github.com/scmhub/ibapi v0.10.37/go.mod h1:/SW/xjAIG/7f/Deu6joy025myq3SpWn458puaZGO30Y= 24 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 25 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 26 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 28 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 30 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 32 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 33 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 34 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 35 | -------------------------------------------------------------------------------- /ib_test.go: -------------------------------------------------------------------------------- 1 | // Note: 2 | // - Most of the tests in this file depend on the market being open to function correctly. 3 | // Ensure the market is open before running these tests to avoid failures or unexpected behavior. 4 | // - The tests are designed to run exclusively on paper trading accounts. 5 | // Attempting to run these tests on a live trading account will do nothing and mark all the tests as failed. 6 | package ibsync 7 | 8 | import ( 9 | "context" 10 | "encoding/json" 11 | "flag" 12 | "fmt" 13 | "math" 14 | "os" 15 | "testing" 16 | "time" 17 | ) 18 | 19 | const ( 20 | testHost = "localhost" 21 | testPort = 7497 22 | testClientID = 1973 23 | testTimeOut = 30 * time.Second 24 | account = "DU5352527" 25 | modelCode = "" 26 | contractID int64 = 756733 27 | ) 28 | 29 | var testConfig = NewConfig( 30 | WithHost(testHost), 31 | WithPort(testPort), 32 | WithClientID(testClientID), 33 | WithTimeout(testTimeOut), 34 | ) 35 | var prettyFlag bool 36 | var logLevel int 37 | 38 | func init() { 39 | testing.Init() 40 | flag.IntVar(&logLevel, "logLevel", 1, "log Level: -1:trace, 0:debug, 1:info, 2:warning, 3:error, 4:fatal, 5:panic") 41 | flag.BoolVar(&prettyFlag, "pretty", false, "enable pretty printing") 42 | } 43 | 44 | var globalIB *IB // Global IB client for batch testing 45 | 46 | func getIB() *IB { 47 | if globalIB != nil { 48 | return globalIB 49 | } 50 | globalIB = NewIB(testConfig) 51 | if err := globalIB.Connect(); err != nil { 52 | panic("Failed to connect to IB: " + err.Error()) 53 | } 54 | 55 | if !globalIB.IsPaperAccount() { 56 | panic("Tests must run on a paper trading account") 57 | } 58 | return globalIB 59 | } 60 | 61 | // TestMain handles setup and teardown for the entire test suite. 62 | func TestMain(m *testing.M) { 63 | // Setup phase 64 | 65 | // Parse flags first 66 | flag.Parse() 67 | 68 | // Log level 69 | SetLogLevel(logLevel) 70 | 71 | // Pretty 72 | if prettyFlag { 73 | SetConsoleWriter() 74 | } 75 | 76 | // Run the tests 77 | code := m.Run() 78 | 79 | // Teardown phase 80 | if globalIB != nil { 81 | if err := globalIB.Disconnect(); err != nil { 82 | panic("Failed to disconnect IB client: " + err.Error()) 83 | } 84 | } 85 | 86 | // Exit with the test result code 87 | os.Exit(code) 88 | } 89 | 90 | func TestConnection(t *testing.T) { 91 | ib := getIB() 92 | 93 | if !ib.IsConnected() { 94 | t.Fatal("client not connected") 95 | } 96 | 97 | // Disconnect 98 | if err := ib.Disconnect(); err != nil { 99 | t.Errorf("Failed to disconnect IB client: %v", err) 100 | } 101 | time.Sleep(1 * time.Second) 102 | if ib.IsConnected() { 103 | t.Errorf("client should be disconnected") 104 | } 105 | 106 | // Reconnect 107 | if err := ib.Connect(); err != nil { 108 | panic("Failed to reconnect to IB: " + err.Error()) 109 | } 110 | 111 | if !ib.IsConnected() { 112 | t.Errorf("client should be disconnected") 113 | } 114 | 115 | // CurrentTime 116 | time.Sleep(1 * time.Second) 117 | currentTime, err := ib.ReqCurrentTime() 118 | if err != nil { 119 | t.Errorf("ReqCurrentTime: %v", err) 120 | return 121 | } 122 | lag := time.Since(currentTime) 123 | t.Logf("CurrentTime: %v, lag: %v.\n", currentTime, lag) 124 | if lag >= 3*time.Second { 125 | t.Error("CurrentTime lag is too high", lag) 126 | } 127 | 128 | // CurrentTimeInMillis 129 | time.Sleep(1 * time.Second) 130 | currentTimeInMillis, err := ib.ReqCurrentTimeInMillis() 131 | if err != nil { 132 | t.Errorf("ReqCurrentTimeInMillis: %v", err) 133 | return 134 | } 135 | lag = time.Since(time.UnixMilli(currentTimeInMillis)) 136 | t.Logf("CurrentTimeMillis: %v, lag: %v.\n", currentTimeInMillis, lag) 137 | if lag >= 3*time.Second { 138 | t.Error("CurrentTimeInMillis lag is too high", lag) 139 | } 140 | 141 | // Server version 142 | serverVersion := ib.ServerVersion() 143 | t.Log("Server version", serverVersion) 144 | 145 | // Managed accounts 146 | managedAccounts := ib.ManagedAccounts() 147 | if len(managedAccounts) < 1 { 148 | t.Fatal("no accounts") 149 | } 150 | if testing.Verbose() { 151 | for i, ma := range managedAccounts { 152 | t.Logf("Managed account %v: %v\n", i, ma) 153 | } 154 | } 155 | 156 | // Account values 157 | accountValues := ib.AccountValues() 158 | if len(accountValues) < 1 { 159 | t.Error("no account values") 160 | } 161 | if testing.Verbose() { 162 | for i, av := range accountValues { 163 | t.Logf("Account values %v: %v\n", i, av) 164 | } 165 | } 166 | 167 | // Account summary 168 | accountSummary := ib.AccountSummary() 169 | if len(accountSummary) < 1 { 170 | t.Error("no account summary") 171 | } 172 | if testing.Verbose() { 173 | for i, as := range accountSummary { 174 | t.Logf("Account summary %v: %v\n", i, as) 175 | } 176 | } 177 | 178 | // Portfolio 179 | portfolio := ib.Portfolio() 180 | if len(portfolio) < 1 { 181 | t.Error("no portfolio") 182 | } 183 | if testing.Verbose() { 184 | for i, p := range portfolio { 185 | t.Logf("Portfolio %v: %v\n", i, p) 186 | } 187 | } 188 | 189 | // TWS connection time 190 | ct := ib.TWSConnectionTime() 191 | if testing.Verbose() { 192 | t.Logf("Connection time: %v\n", ct) 193 | } 194 | } 195 | 196 | func TestMultipleConnections(t *testing.T) { 197 | // Client #1 198 | ib1 := NewIB(NewConfig( 199 | WithHost(testHost), 200 | WithPort(testPort), 201 | WithClientID(1001), 202 | )) 203 | if err := ib1.Connect(); err != nil { 204 | panic("Failed to connect to IB: " + err.Error()) 205 | } 206 | defer ib1.Disconnect() 207 | 208 | // Client #2 209 | ib2 := NewIB(NewConfig( 210 | WithHost(testHost), 211 | WithPort(testPort), 212 | WithClientID(1002), 213 | )) 214 | if err := ib2.Connect(); err != nil { 215 | panic("Failed to connect to IB: " + err.Error()) 216 | } 217 | defer ib2.Disconnect() 218 | 219 | // Client #3 220 | ib3 := NewIB(NewConfig( 221 | WithHost(testHost), 222 | WithPort(testPort), 223 | WithClientID(1003), 224 | )) 225 | if err := ib3.Connect(); err != nil { 226 | panic("Failed to connect to IB: " + err.Error()) 227 | } 228 | defer ib3.Disconnect() 229 | 230 | // Client #4 231 | ib4 := NewIB(NewConfig( 232 | WithHost(testHost), 233 | WithPort(testPort), 234 | WithClientID(1004), 235 | )) 236 | if err := ib4.Connect(); err != nil { 237 | panic("Failed to connect to IB: " + err.Error()) 238 | } 239 | defer ib4.Disconnect() 240 | 241 | if !ib1.IsConnected() { 242 | t.Fatal("client 1 not connected") 243 | } 244 | if !ib2.IsConnected() { 245 | t.Fatal("client 2 not connected") 246 | } 247 | if !ib3.IsConnected() { 248 | t.Fatal("client 3 not connected") 249 | } 250 | if !ib4.IsConnected() { 251 | t.Fatal("client 4 not connected") 252 | } 253 | } 254 | 255 | func TestPositions(t *testing.T) { 256 | ib := getIB() 257 | 258 | ib.ReqPositions() 259 | defer ib.CancelPositions() 260 | 261 | posChan := ib.PositionChan() 262 | go func() { 263 | for pos := range posChan { 264 | t.Log("Position from chan:", pos) 265 | } 266 | }() 267 | 268 | time.Sleep(1 * time.Second) 269 | 270 | positions := ib.Positions() 271 | t.Log("positions", positions) 272 | 273 | } 274 | 275 | func TestPnl(t *testing.T) { 276 | ib := getIB() 277 | 278 | ib.ReqPnL(account, modelCode) 279 | 280 | pnlChan := ib.PnlChan(account, modelCode) 281 | go func() { 282 | for pnl := range pnlChan { 283 | t.Log("pnl from chan", pnl) 284 | } 285 | }() 286 | 287 | time.Sleep(1 * time.Second) 288 | 289 | pnl := ib.Pnl(account, modelCode) 290 | t.Log("pnl", pnl) 291 | 292 | time.Sleep(3 * time.Second) 293 | 294 | ib.CancelPnL(account, modelCode) 295 | 296 | time.Sleep(1 * time.Second) 297 | 298 | pnl = ib.Pnl(account, modelCode) 299 | t.Log("pnl", pnl) 300 | } 301 | 302 | func TestPnlSingle(t *testing.T) { 303 | ib := getIB() 304 | 305 | ib.ReqPnLSingle(account, modelCode, contractID) 306 | 307 | pnlSingleChan := ib.PnlSingleChan(account, modelCode, contractID) 308 | go func() { 309 | for pnlSingle := range pnlSingleChan { 310 | t.Log("pnl from chan", pnlSingle) 311 | } 312 | }() 313 | 314 | time.Sleep(1 * time.Second) 315 | 316 | pnlSingle := ib.PnlSingle(account, modelCode, contractID) 317 | t.Log("pnlSingle", pnlSingle) 318 | 319 | time.Sleep(3 * time.Second) 320 | 321 | ib.CancelPnLSingle(account, modelCode, contractID) 322 | 323 | time.Sleep(2 * time.Second) 324 | 325 | pnlSingle = ib.PnlSingle(account, modelCode, contractID) 326 | t.Log("pnlSingle", pnlSingle) 327 | } 328 | 329 | func TestMidPoint(t *testing.T) { 330 | ib := getIB() 331 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 332 | 333 | midpoint, err := ib.MidPoint(eurusd) 334 | if err != nil { 335 | t.Fatalf("Failed to get midpoint: %v", err) 336 | } 337 | if testing.Verbose() { 338 | t.Logf("MidPoint: %v", midpoint) 339 | } 340 | } 341 | 342 | func TestCalculateOption(t *testing.T) { 343 | ib := getIB() 344 | 345 | ib.ReqMarketDataType(4) 346 | 347 | spx := NewIndex("SPX", "CBOE", "USD") 348 | 349 | err := ib.QualifyContract(spx) 350 | if err != nil { 351 | t.Fatal("Qualify contract") 352 | } 353 | 354 | ticker, err := ib.Snapshot(spx) 355 | if err != nil && err != WarnDelayedMarketData { 356 | t.Fatalf("Failed to get ticker: %v", err) 357 | } 358 | 359 | maturity := time.Now().AddDate(0, 3, 0).Format("200601") // three month from now 360 | strike := math.Round(ticker.MarketPrice()/250) * 250 361 | call := NewOption("SPX", maturity, strike, "C", "SMART", "100", "USD") 362 | call.TradingClass = "SPX" 363 | 364 | err = ib.QualifyContract(call) 365 | if err != nil { 366 | t.Fatal("Qualify options") 367 | } 368 | 369 | ticker, err = ib.Snapshot(call) 370 | greeks := ticker.Greeks() 371 | 372 | if testing.Verbose() { 373 | t.Log("err", err) 374 | t.Log("Greeks", greeks) 375 | } 376 | 377 | optionPrice, err := ib.CalculateOptionPrice(call, greeks.ImpliedVol+0.01, greeks.UndPrice) 378 | if err != nil { 379 | t.Errorf("CalculateOptionPrice: %v\n", err) 380 | return 381 | } 382 | 383 | if testing.Verbose() { 384 | t.Logf("Option Price: %v, was expecting: %v", optionPrice.OptPrice, greeks.OptPrice+greeks.Vega) 385 | } 386 | 387 | impliedVol, err := ib.CalculateImpliedVolatility(call, greeks.OptPrice+greeks.Vega, greeks.UndPrice) 388 | if err != nil { 389 | t.Errorf("CalculateImpliedVolatility: %v\n", err) 390 | return 391 | } 392 | 393 | if testing.Verbose() { 394 | t.Logf("Implied Volatility: %.2f%%, was expecting: %.2f%%", impliedVol.ImpliedVol*100, greeks.ImpliedVol*100+1) 395 | } 396 | } 397 | 398 | func TestPlaceOrder(t *testing.T) { 399 | ib := getIB() 400 | 401 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 402 | 403 | midpoint, err := ib.MidPoint(eurusd) 404 | if err != nil { 405 | t.Fatalf("Failed to get midpoint: %v", err) 406 | } 407 | price := math.Round(95*midpoint.MidPoint) / 100 408 | modifiedPrice := math.Round(105*midpoint.MidPoint) / 100 409 | 410 | // Place orders 411 | order1 := LimitOrder("SELL", StringToDecimal("20001"), price) 412 | trade1 := ib.PlaceOrder(eurusd, order1) 413 | 414 | order2 := LimitOrder("BUY", StringToDecimal("20002"), price) 415 | trade2 := ib.PlaceOrder(eurusd, order2) 416 | 417 | // Wait for order1 to be acknowledged 418 | select { 419 | case <-trade1.Done(): 420 | case <-time.After(5 * time.Second): 421 | t.Fatal("Order1 placement timed out") 422 | } 423 | 424 | // Trades 425 | trades := ib.Trades() 426 | for _, trade := range trades { 427 | if trade.Equal(trade1) { 428 | t.Log("found trade1 in Trades:", trade) 429 | } 430 | } 431 | 432 | // OpenTrades 433 | openTrades := ib.OpenTrades() 434 | for _, openTrade := range openTrades { 435 | if openTrade.Equal(trade2) { 436 | t.Log("found trade2 in openTrades:", openTrade) 437 | } 438 | } 439 | 440 | // Orders 441 | orders := ib.Orders() 442 | for _, order := range orders { 443 | if order.HasSameID(order1) { 444 | t.Log("found order1 in Orders:", order) 445 | } 446 | } 447 | 448 | // OpenOrders 449 | openOrders := ib.OpenOrders() 450 | for _, openOrder := range openOrders { 451 | if openOrder.HasSameID(order2) { 452 | t.Log("found order2 in openOrders:", openOrder) 453 | } 454 | } 455 | 456 | // Modify order 457 | order2.LmtPrice = modifiedPrice 458 | _ = ib.PlaceOrder(eurusd, order2) 459 | // Wait for order2 to be acknowledged 460 | select { 461 | case <-trade2.Done(): 462 | case <-time.After(5 * time.Second): 463 | t.Fatal("Order2 modification timed out") 464 | } 465 | 466 | // Request executions 467 | execs, err := ib.ReqExecutions() 468 | if err != nil { 469 | t.Error("Request Executions") 470 | return 471 | } 472 | if len(execs) < 2 { 473 | t.Errorf("not enough executions: %v", len(execs)) 474 | return 475 | } 476 | 477 | // Request fills 478 | fills, err := ib.ReqFills() 479 | if err != nil { 480 | t.Error("Fill Executions") 481 | return 482 | } 483 | if len(fills) < 2 { 484 | t.Errorf("not enough fills: %v", len(fills)) 485 | return 486 | } 487 | 488 | } 489 | 490 | func TestCancelOrder(t *testing.T) { 491 | ib := getIB() 492 | 493 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 494 | 495 | midpoint, err := ib.MidPoint(eurusd) 496 | if err != nil { 497 | t.Fatalf("Failed to get midpoint: %v", err) 498 | } 499 | price := math.Round(95*midpoint.MidPoint) / 100 500 | 501 | // Place order 502 | order := LimitOrder("BUY", StringToDecimal("20001"), price) 503 | trade := ib.PlaceOrder(eurusd, order) 504 | 505 | // Cancel the order 506 | ib.CancelOrder(order, NewOrderCancel()) 507 | 508 | // Wait for order to be cancelled 509 | select { 510 | case <-trade.Done(): 511 | case <-time.After(5 * time.Second): 512 | t.Fatal("Order cancel timed out") 513 | } 514 | 515 | if trade.OrderStatus.Status != Cancelled { 516 | t.Errorf("Expected Cancelled status, got %v", trade.OrderStatus.Status) 517 | } 518 | } 519 | 520 | func TestGlobalCancel(t *testing.T) { 521 | ib := getIB() 522 | 523 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 524 | 525 | midpoint, err := ib.MidPoint(eurusd) 526 | if err != nil { 527 | t.Fatalf("Failed to get midpoint: %v", err) 528 | } 529 | price := math.Round(95*midpoint.MidPoint) / 100 530 | 531 | // Place orders 532 | order1 := LimitOrder("BUY", StringToDecimal("20001"), price) 533 | trade1 := ib.PlaceOrder(eurusd, order1) 534 | 535 | order2 := LimitOrder("BUY", StringToDecimal("20002"), price) 536 | trade2 := ib.PlaceOrder(eurusd, order2) 537 | 538 | // Execute global cancel 539 | ib.ReqGlobalCancel() 540 | 541 | // Wait for order1 to be cancelled 542 | select { 543 | case <-trade1.Done(): 544 | case <-time.After(5 * time.Second): 545 | t.Fatal("Order cancel timed out") 546 | } 547 | // Wait for order2 to be cancelled 548 | select { 549 | case <-trade2.Done(): 550 | case <-time.After(5 * time.Second): 551 | t.Fatal("Order cancel timed out") 552 | } 553 | if !(trade1.OrderStatus.Status == Cancelled || trade1.OrderStatus.Status == ApiCancelled) { 554 | t.Errorf("Expected Cancelled status for trade1, got %v", trade1.OrderStatus.Status) 555 | } 556 | if !(trade2.OrderStatus.Status == Cancelled || trade2.OrderStatus.Status == ApiCancelled) { 557 | t.Errorf("Expected Cancelled status for trade2, got %v", trade2.OrderStatus.Status) 558 | } 559 | } 560 | 561 | func TestSnapshot(t *testing.T) { 562 | ib := getIB() 563 | 564 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 565 | 566 | ticker, err := ib.Snapshot(eurusd) 567 | if err != nil { 568 | t.Errorf("Unexpected error: %v", err) 569 | return 570 | } 571 | 572 | if testing.Verbose() { 573 | t.Logf("Market price: %v\n", ticker.MarketPrice()) 574 | t.Logf("Snapshot: %v\n", ticker) 575 | 576 | } 577 | 578 | } 579 | 580 | func TestReqSmartComponent(t *testing.T) { 581 | ib := getIB() 582 | 583 | smartComponents, err := ib.ReqSmartComponents("c70003") 584 | 585 | if err != nil { 586 | t.Errorf("Unexpected error: %v", err) 587 | return 588 | } 589 | 590 | if testing.Verbose() { 591 | for i, sc := range smartComponents { 592 | t.Logf("Smart component %v: %v\n", i, sc) 593 | } 594 | } 595 | 596 | } 597 | 598 | func TestReqMarketRule(t *testing.T) { 599 | ib := getIB() 600 | 601 | priceIncrement, err := ib.ReqMarketRule(26) 602 | 603 | if err != nil { 604 | t.Errorf("Unexpected error: %v", err) 605 | return 606 | } 607 | 608 | if testing.Verbose() { 609 | t.Logf("Price increment: %v\n", priceIncrement) 610 | } 611 | } 612 | 613 | func TestReqTickByTickData(t *testing.T) { 614 | ib := getIB() 615 | 616 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 617 | 618 | tickByTick := ib.ReqTickByTickData(eurusd, "BidAsk", 100, true) 619 | 620 | time.Sleep(5 * time.Second) 621 | 622 | err := ib.CancelTickByTickData(eurusd, "BidAsk") 623 | if err != nil { 624 | t.Errorf("Unexpected error: %v", err) 625 | return 626 | } 627 | 628 | if testing.Verbose() { 629 | t.Logf("Tick by tick: %v\n", tickByTick) 630 | } 631 | 632 | } 633 | 634 | func TestReqContractDetails(t *testing.T) { 635 | ib := getIB() 636 | 637 | amd := NewStock("XXX", "XXX", "XXX") 638 | _, err := ib.ReqContractDetails(amd) 639 | 640 | if err == nil { 641 | t.Error("Expected error, got nil") 642 | } 643 | 644 | amd = NewStock("AMD", "", "") 645 | cds, err := ib.ReqContractDetails(amd) 646 | if err != nil { 647 | t.Errorf("Unexpected error: %v", err) 648 | } 649 | if len(cds) < 2 { 650 | t.Errorf("Expected more contract details got: %v", len(cds)) 651 | } 652 | if testing.Verbose() { 653 | for i, cd := range cds { 654 | t.Logf("contract detail %v:, %v\n", i, cd) 655 | } 656 | } 657 | 658 | amd = NewStock("AMD", "SMART", "USD") 659 | cds, err = ib.ReqContractDetails(amd) 660 | if err != nil { 661 | t.Errorf("Unexpected error: %v", err) 662 | } 663 | if len(cds) > 1 { 664 | t.Errorf("Expected one contract details got: %v", len(cds)) 665 | } 666 | if testing.Verbose() { 667 | for i, cd := range cds { 668 | t.Logf("contract detail %v:, %v\n", i, cd) 669 | } 670 | } 671 | } 672 | 673 | func TestQualifyContract(t *testing.T) { 674 | ib := getIB() 675 | 676 | amd := NewStock("XXX", "XXX", "XXX") 677 | err := ib.QualifyContract(amd) 678 | if err == nil { 679 | t.Error("Expected error, got nil") 680 | } 681 | t.Logf("No security definition has been found for the request, %v", err) 682 | 683 | amd = NewStock("AMD", "", "") 684 | err = ib.QualifyContract(amd) 685 | if err == nil { 686 | t.Error("Expected error, got nil") 687 | } 688 | t.Logf("ambiguous contract, %v", err) 689 | 690 | amd = NewStock("AMD", "SMART", "USD") 691 | t.Logf("AMD before qualifiying:, %v", amd) 692 | err = ib.QualifyContract(amd) 693 | if err != nil { 694 | t.Errorf("Unexpected error: %v", err) 695 | } 696 | t.Logf("AMD after qualifiying:, %v", amd) 697 | } 698 | 699 | func TestJsonCOntract(t *testing.T) { 700 | ib := getIB() 701 | 702 | amd := NewStock("AMD", "SMART", "USD") 703 | err := ib.QualifyContract(amd) 704 | if err != nil { 705 | t.Errorf("Unexpected error: %v", err) 706 | } 707 | 708 | byteContract, err := json.Marshal(amd) 709 | if err != nil { 710 | t.Errorf("json Marshall error: %v", err) 711 | } 712 | if testing.Verbose() { 713 | t.Logf("json contract: %v\n", string(byteContract)) 714 | } 715 | 716 | var decodedContract Contract 717 | err = json.Unmarshal(byteContract, &decodedContract) 718 | if err != nil { 719 | t.Errorf("json Unmarshall error: %v", err) 720 | } 721 | if !decodedContract.Equal(amd) { 722 | t.Errorf("Decoded contract does not match original contract: %v", amd) 723 | } 724 | } 725 | 726 | func TestReqMktDepthExchanges(t *testing.T) { 727 | ib := getIB() 728 | 729 | mdes, err := ib.ReqMktDepthExchanges() 730 | 731 | if err != nil { 732 | t.Errorf("Unexpected error %v\n", err) 733 | } 734 | 735 | if len(mdes) < 1 { 736 | t.Error("no market depth exchanges") 737 | } 738 | 739 | t.Logf("No security definition has been found for the request, %v", err) 740 | if testing.Verbose() { 741 | for i, mde := range mdes { 742 | t.Logf("Depth market data description %v: %v\n", i, mde) 743 | } 744 | } 745 | } 746 | 747 | func TestReqMktDepth(t *testing.T) { 748 | ib := getIB() 749 | 750 | aapl := NewStock("AAPL", "NYSE", "USD") 751 | 752 | ticker, err := ib.ReqMktDepth(aapl, 5, false) 753 | 754 | if err == ErrAdditionalSubscriptionRequired { 755 | t.Log("no market data subscription for Market depth") 756 | return 757 | } 758 | 759 | if err != nil { 760 | t.Fatalf("Unexpected error %v\n", err) 761 | } 762 | 763 | time.Sleep(2 * time.Second) 764 | 765 | bids := ticker.DomBids() 766 | asks := ticker.DomAsks() 767 | 768 | if len(bids) == 0 || len(asks) == 0 { 769 | t.Error("no market depth data") 770 | } 771 | if testing.Verbose() { 772 | for level := range len(bids) { 773 | t.Logf("level %v, bid:%v, ask:%v", level, bids[level], asks[level]) 774 | } 775 | } 776 | 777 | ib.CancelMktDepth(aapl, false) 778 | } 779 | 780 | func TestNewsBulletins(t *testing.T) { 781 | ib := getIB() 782 | 783 | nbChan := ib.NewsBulletinsChan() 784 | ctx, cancel := context.WithCancel(ib.eClient.Ctx) 785 | defer cancel() 786 | go func() { 787 | var i int 788 | for { 789 | select { 790 | case <-ctx.Done(): 791 | return 792 | case bulletin, ok := <-nbChan: 793 | if !ok { 794 | return 795 | } 796 | t.Logf("News bulletin from channel %v: %v\n", i, bulletin) 797 | i++ 798 | } 799 | } 800 | }() 801 | 802 | ib.ReqNewsBulletins(true) 803 | defer ib.CancelNewsBulletins() 804 | 805 | time.Sleep(2 * time.Second) 806 | 807 | bulletins := ib.NewsBulletins() 808 | 809 | for i, bulletin := range bulletins { 810 | t.Logf("News bulletin %v: %v\n", i, bulletin) 811 | } 812 | 813 | } 814 | 815 | func TestRequestFA(t *testing.T) { 816 | ib := getIB() 817 | 818 | cxml, err := ib.RequestFA(FaDataType(1)) 819 | 820 | if err == ErrNotFinancialAdvisor { 821 | t.Log("RequestFA not allowed on non FA account") 822 | return 823 | } 824 | 825 | if err != nil { 826 | t.Errorf("Unexpected error %v\n", err) 827 | } 828 | 829 | if testing.Verbose() { 830 | t.Logf("FA cxml:\n %v\n", cxml) 831 | } 832 | } 833 | 834 | func TestReplaceFA(t *testing.T) { 835 | ib := getIB() 836 | 837 | cxml, err := ib.RequestFA(FaDataType(1)) 838 | 839 | if err == ErrNotFinancialAdvisor { 840 | t.Log("ReplaceFA not allowed on non FA account") 841 | return 842 | } 843 | 844 | cxml, err = ib.ReplaceFA(FaDataType(1), cxml) 845 | 846 | if err != nil { 847 | t.Errorf("Unexpected error %v\n", err) 848 | } 849 | 850 | if testing.Verbose() { 851 | t.Logf("FA cxml:\n %v\n", cxml) 852 | } 853 | } 854 | 855 | func TestReqHistoricalData(t *testing.T) { 856 | ib := getIB() 857 | 858 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 859 | 860 | lastWednesday12ESTString := FormatIBTimeUSEastern(LastWednesday12EST()) 861 | 862 | // Request Historical Data 863 | endDateTime := lastWednesday12ESTString // format "yyyymmdd HH:mm:ss ttt", where "ttt" is an optional time zone 864 | duration := "1 D" // "60 S", "30 D", "13 W", "6 M", "10 Y". The unit must be specified (S for seconds, D for days, W for weeks, etc.). 865 | barSize := "15 mins" // "1 secs", "5 secs", "10 secs", "15 secs", "30 secs", "1 min", "2 mins", "5 mins", etc. 866 | whatToShow := "MIDPOINT" // "TRADES", "MIDPOINT", "BID", "ASK", "BID_ASK", "HISTORICAL_VOLATILITY", etc. 867 | useRTH := true // `true` limits data to regular trading hours (RTH), `false` includes all data. 868 | formatDate := 1 // `1` for the "yyyymmdd HH:mm:ss ttt" format, or `2` for Unix timestamps. 869 | 870 | barChan, cancel := ib.ReqHistoricalData(eurusd, endDateTime, duration, barSize, whatToShow, useRTH, formatDate) 871 | cancel() 872 | <-barChan 873 | t.Log("Historical request cancelled") 874 | 875 | barChan, _ = ib.ReqHistoricalData(eurusd, endDateTime, duration, barSize, whatToShow, useRTH, formatDate) 876 | var bars []Bar 877 | var i int 878 | for bar := range barChan { 879 | bars = append(bars, bar) 880 | if testing.Verbose() { 881 | t.Logf("bar %v: %v\n", i, bar) 882 | i++ 883 | } 884 | } 885 | 886 | if len(bars) < 1 { 887 | t.Error("No bars retreived") 888 | return 889 | } 890 | 891 | t.Log("Number of bars:", len(bars)) 892 | t.Log("FirstBar:", bars[0]) 893 | t.Log("LastBar", bars[len(bars)-1]) 894 | } 895 | 896 | func TestReqHistoricalDataUpToDate(t *testing.T) { 897 | ib := getIB() 898 | 899 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 900 | duration := "60 S" 901 | barSize := "5 secs" 902 | whatToShow := "MIDPOINT" 903 | useRTH := true 904 | formatDate := 1 905 | 906 | barChan, cancel := ib.ReqHistoricalDataUpToDate(eurusd, duration, barSize, whatToShow, useRTH, formatDate) 907 | 908 | var bars []Bar 909 | go func() { 910 | var i int 911 | for bar := range barChan { 912 | bars = append(bars, bar) 913 | if testing.Verbose() { 914 | t.Logf("bar %v: %v\n", i, bar) 915 | i++ 916 | } 917 | } 918 | }() 919 | 920 | time.Sleep(30 * time.Second) 921 | cancel() 922 | 923 | if len(bars) < 1 { 924 | t.Error("No bars retreived") 925 | return 926 | } 927 | 928 | t.Log("Number of bars:", len(bars)) 929 | t.Log("FirstBar:", bars[0]) 930 | t.Log("LastBar", bars[len(bars)-1]) 931 | } 932 | 933 | func TestReqHistoricalSchedule(t *testing.T) { 934 | ib := getIB() 935 | 936 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 937 | 938 | historicalSchedule, err := ib.ReqHistoricalSchedule(eurusd, "", "1 W", true) 939 | if err != nil { 940 | t.Errorf("Unexpected error: %v", err) 941 | return 942 | } 943 | t.Logf("Historical schedule start date:, %v", historicalSchedule.StartDateTime) 944 | t.Logf("Historical schedule end date:, %v", historicalSchedule.EndDateTime) 945 | t.Logf("Historical schedule time zone:, %v", historicalSchedule.TimeZone) 946 | if testing.Verbose() { 947 | for i, session := range historicalSchedule.Sessions { 948 | t.Logf("Session %v:, %v", i, session) 949 | } 950 | } 951 | } 952 | 953 | func TestReqHeadTimeStamp(t *testing.T) { 954 | ib := getIB() 955 | 956 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 957 | 958 | headStamp, err := ib.ReqHeadTimeStamp(eurusd, "MIDPOINT", true, 1) 959 | if err != nil { 960 | t.Errorf("Unexpected error: %v", err) 961 | return 962 | } 963 | if !(headStamp.Before(time.Now()) && headStamp.After(time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))) { 964 | t.Errorf("Unexpected error: %v", err) 965 | } 966 | t.Logf("headStamp:, %v", headStamp) 967 | } 968 | 969 | func TestReqHistogramData(t *testing.T) { 970 | ib := getIB() 971 | 972 | aapl := NewStock("AAPL", "SMART", "USD") 973 | 974 | histogramDatas, err := ib.ReqHistogramData(aapl, true, "1 day") 975 | 976 | if err != nil { 977 | t.Errorf("Unexpected error: %v", err) 978 | return 979 | } 980 | 981 | if testing.Verbose() { 982 | for i, hd := range histogramDatas { 983 | t.Logf("Histogram Data %v: %v\n", i, hd) 984 | } 985 | } 986 | 987 | } 988 | 989 | func TestReqHistoricalTicks(t *testing.T) { 990 | ib := getIB() 991 | 992 | aapl := NewStock("AAPL", "SMART", "USD") 993 | 994 | ticks, err, done := ib.ReqHistoricalTicks(aapl, LastWednesday12EST(), time.Time{}, 100, true, true) 995 | 996 | if err != nil { 997 | t.Errorf("Unexpected error: %v", err) 998 | return 999 | } 1000 | 1001 | if testing.Verbose() { 1002 | t.Logf("Historical Ticks number %v, is done? %v\n", len(ticks), done) 1003 | for i, hd := range ticks { 1004 | t.Logf("%v: %v\n", i, hd) 1005 | } 1006 | } 1007 | 1008 | } 1009 | 1010 | func TestReqHistoricalTickLast(t *testing.T) { 1011 | ib := getIB() 1012 | 1013 | aapl := NewStock("AAPL", "SMART", "USD") 1014 | 1015 | ticks, err, done := ib.ReqHistoricalTickLast(aapl, LastWednesday12EST(), time.Time{}, 100, true, true) 1016 | 1017 | if err != nil { 1018 | t.Errorf("Unexpected error: %v", err) 1019 | return 1020 | } 1021 | 1022 | if testing.Verbose() { 1023 | t.Logf("Historical Last Ticks number %v, is done? %v\n", len(ticks), done) 1024 | for i, hd := range ticks { 1025 | t.Logf("%v: %v\n", i, hd) 1026 | } 1027 | } 1028 | 1029 | } 1030 | 1031 | func TestReqHistoricalTickBidAsk(t *testing.T) { 1032 | ib := getIB() 1033 | 1034 | aapl := NewStock("AAPL", "SMART", "USD") 1035 | 1036 | ticks, err, done := ib.ReqHistoricalTickBidAsk(aapl, LastWednesday12EST(), time.Time{}, 100, true, true) 1037 | 1038 | if err != nil { 1039 | t.Errorf("Unexpected error: %v", err) 1040 | return 1041 | } 1042 | 1043 | if testing.Verbose() { 1044 | t.Logf("Historical Bid Ask Ticks number %v, is done? %v\n", len(ticks), done) 1045 | for i, hd := range ticks { 1046 | t.Logf("%v: %v\n", i, hd) 1047 | } 1048 | } 1049 | 1050 | } 1051 | 1052 | func TestReqScannerParameters(t *testing.T) { 1053 | ib := getIB() 1054 | 1055 | xml, err := ib.ReqScannerParameters() 1056 | 1057 | if err != nil { 1058 | t.Errorf("Unexpected error: %v", err) 1059 | return 1060 | } 1061 | 1062 | if testing.Verbose() { 1063 | fmt.Println(xml) 1064 | } 1065 | 1066 | } 1067 | 1068 | func TestReqScannerSubscription(t *testing.T) { 1069 | ib := getIB() 1070 | 1071 | ss := NewScannerSubscription() 1072 | ss.Instrument = "STK" 1073 | ss.LocationCode = "STK.US.MAJOR" 1074 | ss.ScanCode = "HOT_BY_VOLUME" 1075 | 1076 | scanDatas, err := ib.ReqScannerSubscription(ss) 1077 | 1078 | if err != nil { 1079 | t.Errorf("Unexpected error: %v", err) 1080 | return 1081 | } 1082 | 1083 | if testing.Verbose() { 1084 | for i, sd := range scanDatas { 1085 | t.Logf("Scan data %v: %v\n", i, sd) 1086 | } 1087 | } 1088 | 1089 | } 1090 | 1091 | func TestReqRealTimeBars(t *testing.T) { 1092 | ib := getIB() 1093 | 1094 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 1095 | 1096 | useRTH := true 1097 | whatToShow := "MIDPOINT" // "TRADES", "MIDPOINT", "BID" or "ASK" 1098 | rtBarChan, cancel := ib.ReqRealTimeBars(eurusd, 5, whatToShow, useRTH) 1099 | 1100 | var rtBars []RealTimeBar 1101 | go func() { 1102 | var i int 1103 | for rtBar := range rtBarChan { 1104 | rtBars = append(rtBars, rtBar) 1105 | if testing.Verbose() { 1106 | t.Logf("real time bar %v: %v\n", i, rtBar) 1107 | i++ 1108 | } 1109 | } 1110 | }() 1111 | 1112 | time.Sleep(10 * time.Second) 1113 | cancel() 1114 | 1115 | if len(rtBars) < 1 { 1116 | t.Error("No real time bars retreived") 1117 | return 1118 | } 1119 | 1120 | t.Log("Number of bars:", len(rtBars)) 1121 | t.Log("FirstBar:", rtBars[0]) 1122 | t.Log("LastBar", rtBars[len(rtBars)-1]) 1123 | } 1124 | 1125 | func TestReqFundamentalData(t *testing.T) { 1126 | ib := getIB() 1127 | 1128 | aapl := NewStock("AAPL", "SMART", "USD") 1129 | 1130 | // "ReportSnapshot" 1131 | data, err := ib.ReqFundamentalData(aapl, "ReportSnapshot") 1132 | 1133 | if err != nil { 1134 | switch err { 1135 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1136 | t.Log(err) 1137 | default: 1138 | t.Errorf("Unexpected error: %v", err) 1139 | } 1140 | } 1141 | 1142 | if testing.Verbose() && data != "" { 1143 | t.Logf("fundamental data: ReportSnapshot. \n%v", data) 1144 | } 1145 | 1146 | // "ReportsFinSummary" 1147 | data, err = ib.ReqFundamentalData(aapl, "ReportsFinSummary") 1148 | 1149 | if err != nil { 1150 | switch err { 1151 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1152 | t.Log(err) 1153 | default: 1154 | t.Errorf("Unexpected error: %v", err) 1155 | } 1156 | } 1157 | 1158 | if testing.Verbose() && data != "" { 1159 | t.Logf("fundamental data: Financial Summary. \n%v", data) 1160 | } 1161 | 1162 | // "ReportRatios" 1163 | data, err = ib.ReqFundamentalData(aapl, "ReportRatios") 1164 | 1165 | if err != nil { 1166 | switch err { 1167 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1168 | t.Log(err) 1169 | default: 1170 | t.Errorf("Unexpected error: %v", err) 1171 | } 1172 | } 1173 | 1174 | if testing.Verbose() && data != "" { 1175 | t.Logf("fundamental data: Financial Ratio. \n%v", data) 1176 | } 1177 | 1178 | // "ReportsFinStatements" 1179 | data, err = ib.ReqFundamentalData(aapl, "ReportsFinStatements") 1180 | 1181 | if err != nil { 1182 | switch err { 1183 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1184 | t.Log(err) 1185 | default: 1186 | t.Errorf("Unexpected error: %v", err) 1187 | } 1188 | } 1189 | 1190 | if testing.Verbose() && data != "" { 1191 | t.Logf("fundamental data: Financial Statement. \n%v", data) 1192 | } 1193 | 1194 | // "RESC 1195 | data, err = ib.ReqFundamentalData(aapl, "RESC") 1196 | 1197 | if err != nil { 1198 | switch err { 1199 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1200 | t.Log(err) 1201 | default: 1202 | t.Errorf("Unexpected error: %v", err) 1203 | } 1204 | } 1205 | 1206 | if testing.Verbose() && data != "" { 1207 | t.Logf("fundamental data: Analyst estimates. \n%v", data) 1208 | } 1209 | 1210 | // "CalendarReport" 1211 | data, err = ib.ReqFundamentalData(aapl, "CalendarReport") 1212 | 1213 | if err != nil { 1214 | switch err { 1215 | case ErrMissingReportType, ErrNewsFeedNotAllowed: 1216 | t.Log(err) 1217 | default: 1218 | t.Errorf("Unexpected error: %v", err) 1219 | } 1220 | } 1221 | 1222 | if testing.Verbose() && data != "" { 1223 | t.Logf("fundamental data: Calendar Report. \n%v", data) 1224 | } 1225 | } 1226 | 1227 | func TestReqNewsProviders(t *testing.T) { 1228 | ib := getIB() 1229 | 1230 | newsProvider, err := ib.ReqNewsProviders() 1231 | 1232 | if err != nil { 1233 | t.Errorf("Unexpected error: %v", err) 1234 | } 1235 | 1236 | if testing.Verbose() { 1237 | for i, np := range newsProvider { 1238 | t.Logf("News provider %v: %v.\n", i, np) 1239 | } 1240 | } 1241 | } 1242 | 1243 | func TestReqHistoricalNews(t *testing.T) { 1244 | ib := getIB() 1245 | 1246 | aapl := NewStock("AAPL", "SMART", "USD") 1247 | err := ib.QualifyContract(aapl) 1248 | if err != nil { 1249 | t.Errorf("Unexpected error: %v", err) 1250 | } 1251 | 1252 | // Briefing.com General Market Columns -> BRFG. 1253 | // Briefing.com Analyst Actions -> BRFUPDN. 1254 | // Dow Jones News Service -> DJ-N. 1255 | // Dow Jones Real-Time News Asia Pacific -> DJ-RTA. 1256 | // Dow Jones Real-Time News Europe -> DJ-RTE. 1257 | // Dow Jones Real-Time News Global -> DJ-RTG. 1258 | // Dow Jones Real-Time News Pro -> DJ-RTPRO. 1259 | // Dow Jones Newsletters -> DJNL. 1260 | 1261 | provider := "BRFG" 1262 | startDateTime := time.Now().AddDate(0, 0, -7) 1263 | endDateTime := time.Now() 1264 | 1265 | historicalNews, err, hasMore := ib.ReqHistoricalNews(aapl.ConID, provider, startDateTime, endDateTime, 50) 1266 | 1267 | if err != nil { 1268 | t.Errorf("Unexpected error: %v", err) 1269 | } 1270 | 1271 | if testing.Verbose() { 1272 | t.Logf("Number of headlines: %v, has more? %v", len(historicalNews), hasMore) 1273 | for i, hn := range historicalNews { 1274 | t.Logf("News headline %v: %v.\n", i, hn) 1275 | } 1276 | } 1277 | if len(historicalNews) > 0 { 1278 | 1279 | article, err := ib.ReqNewsArticle(provider, historicalNews[0].ArticleID) 1280 | 1281 | if err != nil { 1282 | t.Errorf("Unexpected error: %v", err) 1283 | } 1284 | 1285 | if testing.Verbose() { 1286 | t.Log(article) 1287 | } 1288 | 1289 | } 1290 | 1291 | } 1292 | 1293 | func TestQueryDisplayGroups(t *testing.T) { 1294 | ib := getIB() 1295 | 1296 | groups, err := ib.QueryDisplayGroups() 1297 | 1298 | if err != nil { 1299 | t.Errorf("Unexpected error: %v", err) 1300 | } 1301 | 1302 | t.Logf("display groups:, %v", groups) 1303 | } 1304 | 1305 | func TestReqSecDefOptParams(t *testing.T) { 1306 | ib := getIB() 1307 | 1308 | optionChains, err := ib.ReqSecDefOptParams("IBM", "", "STK", 8314) 1309 | 1310 | if err != nil { 1311 | t.Errorf("Unexpected error: %v", err) 1312 | } 1313 | 1314 | t.Logf("option chains:, %v", optionChains) 1315 | } 1316 | 1317 | func TestReqSoftDollarTiers(t *testing.T) { 1318 | ib := getIB() 1319 | 1320 | sdts, err := ib.ReqSoftDollarTiers() 1321 | 1322 | if err != nil { 1323 | t.Errorf("Unexpected error: %v", err) 1324 | } 1325 | 1326 | t.Logf("Soft Dollar Tiers:, %v", sdts) 1327 | } 1328 | 1329 | func TestReqFamilyCodes(t *testing.T) { 1330 | ib := getIB() 1331 | 1332 | familyCodes, err := ib.ReqFamilyCodes() 1333 | 1334 | if err != nil { 1335 | t.Errorf("Unexpected error: %v", err) 1336 | } 1337 | 1338 | t.Logf("Family codes:, %v", familyCodes) 1339 | } 1340 | 1341 | func TestReqMatchingSymbols(t *testing.T) { 1342 | ib := getIB() 1343 | 1344 | cds, err := ib.ReqMatchingSymbols("aapl") 1345 | 1346 | if err != nil { 1347 | t.Errorf("Unexpected error: %v", err) 1348 | } 1349 | 1350 | for i, cd := range cds { 1351 | t.Logf("Contract descriptions %v:, %v\n", i, cd) 1352 | } 1353 | 1354 | } 1355 | 1356 | func TestReqWshMetaData(t *testing.T) { 1357 | ib := getIB() 1358 | 1359 | dataJson, err := ib.ReqWshMetaData() 1360 | 1361 | if err == ErrNewsFeedNotAllowed { 1362 | t.Log(err) 1363 | return 1364 | } 1365 | 1366 | if err != nil { 1367 | t.Errorf("Unexpected error: %v", err) 1368 | } 1369 | 1370 | t.Logf("Wall Street Horizon Meta data:, %v", dataJson) 1371 | } 1372 | 1373 | func TestReqWshEventData(t *testing.T) { 1374 | ib := getIB() 1375 | 1376 | dataJson, err := ib.ReqWshEventData(NewWshEventData()) 1377 | 1378 | if err == ErrNewsFeedNotAllowed { 1379 | t.Log(err) 1380 | return 1381 | } 1382 | 1383 | if err != nil { 1384 | t.Errorf("Unexpected error: %v", err) 1385 | } 1386 | 1387 | t.Logf("Wall Street Horizon Event data:, %v", dataJson) 1388 | } 1389 | 1390 | func TestReqUserInfo(t *testing.T) { 1391 | ib := getIB() 1392 | 1393 | whiteBrandingId, err := ib.ReqUserInfo() 1394 | 1395 | if err != nil { 1396 | t.Errorf("Unexpected error: %v", err) 1397 | } 1398 | 1399 | t.Logf("White Branding ID:, %v", whiteBrandingId) 1400 | } 1401 | -------------------------------------------------------------------------------- /ibapi.go: -------------------------------------------------------------------------------- 1 | /* 2 | The primary aim of this file is to re-export all structs, constants, and functions from the 3 | ibapi package, allowing developers to access them through a single cohesive package. This 4 | simplifies the process of integrating with the Interactive Brokers API by reducing the need 5 | to manage multiple imports and providing a more straightforward API surface for developers. 6 | */ 7 | package ibsync 8 | 9 | import "github.com/scmhub/ibapi" 10 | 11 | const ( 12 | UNSET_INT = ibapi.UNSET_INT 13 | UNSET_LONG = ibapi.UNSET_LONG 14 | UNSET_FLOAT = ibapi.UNSET_FLOAT 15 | INFINITY_STRING = ibapi.INFINITY_STRING 16 | ) 17 | 18 | var ( 19 | UNSET_DECIMAL = ibapi.UNSET_DECIMAL 20 | ZERO = ibapi.ZERO 21 | StringToDecimal = ibapi.StringToDecimal 22 | DecimalToString = ibapi.DecimalToString 23 | IntMaxString = ibapi.IntMaxString 24 | LongMaxString = ibapi.LongMaxString 25 | FloatMaxString = ibapi.FloatMaxString 26 | DecimalMaxString = ibapi.DecimalMaxString 27 | Logger = ibapi.Logger 28 | SetLogLevel = ibapi.SetLogLevel 29 | SetConsoleWriter = ibapi.SetConsoleWriter 30 | TickName = ibapi.TickName 31 | ) 32 | 33 | type ( 34 | Bar = ibapi.Bar 35 | RealTimeBar = ibapi.RealTimeBar 36 | CommissionAndFeesReport = ibapi.CommissionAndFeesReport 37 | ComboLeg = ibapi.ComboLeg 38 | Contract = ibapi.Contract 39 | ContractDescription = ibapi.ContractDescription 40 | ContractDetails = ibapi.ContractDetails 41 | DeltaNeutralContract = ibapi.DeltaNeutralContract 42 | Decimal = ibapi.Decimal 43 | DepthMktDataDescription = ibapi.DepthMktDataDescription 44 | Execution = ibapi.Execution 45 | ExecutionFilter = ibapi.ExecutionFilter 46 | FaDataType = ibapi.FaDataType 47 | FamilyCode = ibapi.FamilyCode 48 | FundDistributionPolicyIndicator = ibapi.FundDistributionPolicyIndicator 49 | HistogramData = ibapi.HistogramData 50 | HistoricalSession = ibapi.HistoricalSession 51 | HistoricalTick = ibapi.HistoricalTick 52 | HistoricalTickBidAsk = ibapi.HistoricalTickBidAsk 53 | HistoricalTickLast = ibapi.HistoricalTickLast 54 | IneligibilityReason = ibapi.IneligibilityReason 55 | NewsProvider = ibapi.NewsProvider 56 | Order = ibapi.Order 57 | OrderCancel = ibapi.OrderCancel 58 | OrderID = ibapi.OrderID 59 | OrderState = ibapi.OrderState 60 | PriceIncrement = ibapi.PriceIncrement 61 | ScanData = ibapi.ScanData 62 | ScannerSubscription = ibapi.ScannerSubscription 63 | SmartComponent = ibapi.SmartComponent 64 | SoftDollarTier = ibapi.SoftDollarTier 65 | TagValue = ibapi.TagValue 66 | TickerID = ibapi.TickerID 67 | TickType = ibapi.TickType 68 | TickAttrib = ibapi.TickAttrib 69 | TickAttribLast = ibapi.TickAttribLast 70 | TickAttribBidAsk = ibapi.TickAttribBidAsk 71 | WshEventData = ibapi.WshEventData 72 | ) 73 | 74 | var ( 75 | NewBar = ibapi.NewBar 76 | NewRealTimeBar = ibapi.NewRealTimeBar 77 | NewCommissionAndFeesReport = ibapi.NewCommissionAndFeesReport 78 | NewComboLeg = ibapi.NewComboLeg 79 | NewContract = ibapi.NewContract 80 | NewContractDescription = ibapi.NewContractDescription 81 | NewContractDetails = ibapi.NewContractDetails 82 | NewDeltaNeutralContract = ibapi.NewDeltaNeutralContract 83 | NewDepthMktDataDescription = ibapi.NewDepthMktDataDescription 84 | NewExecution = ibapi.NewExecution 85 | NewExecutionFilter = ibapi.NewExecutionFilter 86 | NewFamilyCode = ibapi.NewFamilyCode 87 | NewHistogramData = ibapi.NewHistogramData 88 | NewHistoricalSession = ibapi.NewHistoricalSession 89 | NewHistoricalTick = ibapi.NewHistoricalTick 90 | NewHistoricalTickBidAsk = ibapi.NewHistoricalTickBidAsk 91 | NewHistoricalTickLast = ibapi.NewHistoricalTickLast 92 | NewNewsProvider = ibapi.NewNewsProvider 93 | NewOrder = ibapi.NewOrder 94 | NewOrderCancel = ibapi.NewOrderCancel 95 | NewOrderState = ibapi.NewOrderState 96 | NewPriceIncrement = ibapi.NewPriceIncrement 97 | NewScannerSubscription = ibapi.NewScannerSubscription 98 | NewSmartComponent = ibapi.NewSmartComponent 99 | NewSoftDollarTier = ibapi.NewSoftDollarTier 100 | NewTagValue = ibapi.NewTagValue 101 | NewTickAttrib = ibapi.NewTickAttrib 102 | NewTickAttribLast = ibapi.NewTickAttribLast 103 | NewTickAttribBidAsk = ibapi.NewTickAttribBidAsk 104 | NewWshEventData = ibapi.NewWshEventData 105 | ) 106 | 107 | var ( 108 | // ibapi custom contracts 109 | AtAuction = ibapi.AtAuction 110 | Discretionary = ibapi.Discretionary 111 | MarketOrder = ibapi.MarketOrder 112 | MarketIfTouched = ibapi.MarketIfTouched 113 | MarketOnClose = ibapi.MarketOnClose 114 | MarketOnOpen = ibapi.MarketOnOpen 115 | MidpointMatch = ibapi.MidpointMatch 116 | Midprice = ibapi.Midprice 117 | PeggedToMarket = ibapi.PeggedToMarket 118 | PeggedToStock = ibapi.PeggedToStock 119 | RelativePeggedToPrimary = ibapi.RelativePeggedToPrimary 120 | SweepToFill = ibapi.SweepToFill 121 | AuctionLimit = ibapi.AuctionLimit 122 | AuctionPeggedToStock = ibapi.AuctionPeggedToStock 123 | AuctionRelative = ibapi.AuctionRelative 124 | Block = ibapi.Block 125 | BoxTop = ibapi.BoxTop 126 | LimitOrder = ibapi.LimitOrder 127 | LimitOrderWithCashQty = ibapi.LimitOrderWithCashQty 128 | LimitIfTouched = ibapi.LimitIfTouched 129 | LimitOnClose = ibapi.LimitOnClose 130 | LimitOnOpen = ibapi.LimitOnOpen 131 | PassiveRelative = ibapi.PassiveRelative 132 | PeggedToMidpoint = ibapi.PeggedToMidpoint 133 | BracketOrder = ibapi.BracketOrder 134 | MarketToLimit = ibapi.MarketToLimit 135 | MarketWithProtection = ibapi.MarketWithProtection 136 | Stop = ibapi.Stop 137 | StopLimit = ibapi.StopLimit 138 | StopWithProtection = ibapi.StopWithProtection 139 | TrailingStop = ibapi.TrailingStop 140 | TrailingStopLimit = ibapi.TrailingStopLimit 141 | ComboLimitOrder = ibapi.ComboLimitOrder 142 | ComboMarketOrder = ibapi.ComboMarketOrder 143 | LimitOrderForComboWithLegPrices = ibapi.LimitOrderForComboWithLegPrices 144 | RelativeLimitCombo = ibapi.RelativeLimitCombo 145 | RelativeMarketCombo = ibapi.RelativeMarketCombo 146 | OneCancelsAll = ibapi.OneCancelsAll 147 | Volatility = ibapi.Volatility 148 | MarketFHedge = ibapi.MarketFHedge 149 | PeggedToBenchmark = ibapi.PeggedToBenchmark 150 | AttachAdjustableToStop = ibapi.AttachAdjustableToStop 151 | AttachAdjustableToStopLimit = ibapi.AttachAdjustableToStopLimit 152 | AttachAdjustableToTrail = ibapi.AttachAdjustableToTrail 153 | WhatIfLimitOrder = ibapi.WhatIfLimitOrder 154 | NewPriceCondition = ibapi.NewPriceCondition 155 | NewExecutionCondition = ibapi.NewExecutionCondition 156 | NewMarginCondition = ibapi.NewMarginCondition 157 | NewPercentageChangeCondition = ibapi.NewPercentageChangeCondition 158 | NewTimeCondition = ibapi.NewTimeCondition 159 | NewVolumeCondition = ibapi.NewVolumeCondition 160 | LimitIBKRATS = ibapi.LimitIBKRATS 161 | LimitOrderWithManualOrderTime = ibapi.LimitOrderWithManualOrderTime 162 | PegBestUpToMidOrder = ibapi.PegBestUpToMidOrder 163 | PegBestOrder = ibapi.PegBestOrder 164 | PegMidOrder = ibapi.PegMidOrder 165 | LimitOrderWithCustomerAccount = ibapi.LimitOrderWithCustomerAccount 166 | LimitOrderWithIncludeOvernight = ibapi.LimitOrderWithIncludeOvernight 167 | CancelOrderEmpty = ibapi.CancelOrderEmpty 168 | CancelOrderWithManualTime = ibapi.CancelOrderWithManualTime 169 | LimitOrderWithCmeTaggingFields = ibapi.LimitOrderWithCmeTaggingFields 170 | OrderCancelWithCmeTaggingFields = ibapi.OrderCancelWithCmeTaggingFields 171 | ) 172 | 173 | const ( 174 | // TickType 175 | BID_SIZE = ibapi.BID_SIZE 176 | BID = ibapi.BID 177 | ASK = ibapi.ASK 178 | ASK_SIZE = ibapi.ASK_SIZE 179 | LAST = ibapi.LAST 180 | LAST_SIZE = ibapi.LAST_SIZE 181 | HIGH = ibapi.HIGH 182 | LOW = ibapi.LOW 183 | VOLUME = ibapi.VOLUME 184 | CLOSE = ibapi.CLOSE 185 | BID_OPTION_COMPUTATION = ibapi.BID_OPTION_COMPUTATION 186 | ASK_OPTION_COMPUTATION = ibapi.ASK_OPTION_COMPUTATION 187 | LAST_OPTION_COMPUTATION = ibapi.LAST_OPTION_COMPUTATION 188 | MODEL_OPTION = ibapi.MODEL_OPTION 189 | OPEN = ibapi.OPEN 190 | LOW_13_WEEK = ibapi.LOW_13_WEEK 191 | HIGH_13_WEEK = ibapi.HIGH_13_WEEK 192 | LOW_26_WEEK = ibapi.LOW_26_WEEK 193 | HIGH_26_WEEK = ibapi.HIGH_26_WEEK 194 | LOW_52_WEEK = ibapi.LOW_52_WEEK 195 | HIGH_52_WEEK = ibapi.HIGH_52_WEEK 196 | AVG_VOLUME = ibapi.AVG_VOLUME 197 | OPEN_INTEREST = ibapi.OPEN_INTEREST 198 | OPTION_HISTORICAL_VOL = ibapi.OPTION_HISTORICAL_VOL 199 | OPTION_IMPLIED_VOL = ibapi.OPTION_IMPLIED_VOL 200 | OPTION_BID_EXCH = ibapi.OPTION_BID_EXCH 201 | OPTION_ASK_EXCH = ibapi.OPTION_ASK_EXCH 202 | OPTION_CALL_OPEN_INTEREST = ibapi.OPTION_CALL_OPEN_INTEREST 203 | OPTION_PUT_OPEN_INTEREST = ibapi.OPTION_PUT_OPEN_INTEREST 204 | OPTION_CALL_VOLUME = ibapi.OPTION_CALL_VOLUME 205 | OPTION_PUT_VOLUME = ibapi.OPTION_PUT_VOLUME 206 | INDEX_FUTURE_PREMIUM = ibapi.INDEX_FUTURE_PREMIUM 207 | BID_EXCH = ibapi.BID_EXCH 208 | ASK_EXCH = ibapi.ASK_EXCH 209 | AUCTION_VOLUME = ibapi.AUCTION_VOLUME 210 | AUCTION_PRICE = ibapi.AUCTION_PRICE 211 | AUCTION_IMBALANCE = ibapi.AUCTION_IMBALANCE 212 | MARK_PRICE = ibapi.MARK_PRICE 213 | BID_EFP_COMPUTATION = ibapi.BID_EFP_COMPUTATION 214 | ASK_EFP_COMPUTATION = ibapi.ASK_EFP_COMPUTATION 215 | LAST_EFP_COMPUTATION = ibapi.LAST_EFP_COMPUTATION 216 | OPEN_EFP_COMPUTATION = ibapi.OPEN_EFP_COMPUTATION 217 | HIGH_EFP_COMPUTATION = ibapi.HIGH_EFP_COMPUTATION 218 | LOW_EFP_COMPUTATION = ibapi.LOW_EFP_COMPUTATION 219 | CLOSE_EFP_COMPUTATION = ibapi.CLOSE_EFP_COMPUTATION 220 | LAST_TIMESTAMP = ibapi.LAST_TIMESTAMP 221 | SHORTABLE = ibapi.SHORTABLE 222 | FUNDAMENTAL_RATIOS = ibapi.FUNDAMENTAL_RATIOS 223 | RT_VOLUME = ibapi.RT_VOLUME 224 | HALTED = ibapi.HALTED 225 | BID_YIELD = ibapi.BID_YIELD 226 | ASK_YIELD = ibapi.ASK_YIELD 227 | LAST_YIELD = ibapi.LAST_YIELD 228 | CUST_OPTION_COMPUTATION = ibapi.CUST_OPTION_COMPUTATION 229 | TRADE_COUNT = ibapi.TRADE_COUNT 230 | TRADE_RATE = ibapi.TRADE_RATE 231 | VOLUME_RATE = ibapi.VOLUME_RATE 232 | LAST_RTH_TRADE = ibapi.LAST_RTH_TRADE 233 | RT_HISTORICAL_VOL = ibapi.RT_HISTORICAL_VOL 234 | IB_DIVIDENDS = ibapi.IB_DIVIDENDS 235 | BOND_FACTOR_MULTIPLIER = ibapi.BOND_FACTOR_MULTIPLIER 236 | REGULATORY_IMBALANCE = ibapi.REGULATORY_IMBALANCE 237 | NEWS_TICK = ibapi.NEWS_TICK 238 | SHORT_TERM_VOLUME_3_MIN = ibapi.SHORT_TERM_VOLUME_3_MIN 239 | SHORT_TERM_VOLUME_5_MIN = ibapi.SHORT_TERM_VOLUME_5_MIN 240 | SHORT_TERM_VOLUME_10_MIN = ibapi.SHORT_TERM_VOLUME_10_MIN 241 | DELAYED_BID = ibapi.DELAYED_BID 242 | DELAYED_ASK = ibapi.DELAYED_ASK 243 | DELAYED_LAST = ibapi.DELAYED_LAST 244 | DELAYED_BID_SIZE = ibapi.DELAYED_BID_SIZE 245 | DELAYED_ASK_SIZE = ibapi.DELAYED_ASK_SIZE 246 | DELAYED_LAST_SIZE = ibapi.DELAYED_LAST_SIZE 247 | DELAYED_HIGH = ibapi.DELAYED_HIGH 248 | DELAYED_LOW = ibapi.DELAYED_LOW 249 | DELAYED_VOLUME = ibapi.DELAYED_VOLUME 250 | DELAYED_CLOSE = ibapi.DELAYED_CLOSE 251 | DELAYED_OPEN = ibapi.DELAYED_OPEN 252 | RT_TRD_VOLUME = ibapi.RT_TRD_VOLUME 253 | CREDITMAN_MARK_PRICE = ibapi.CREDITMAN_MARK_PRICE 254 | CREDITMAN_SLOW_MARK_PRICE = ibapi.CREDITMAN_SLOW_MARK_PRICE 255 | DELAYED_BID_OPTION = ibapi.DELAYED_BID_OPTION 256 | DELAYED_ASK_OPTION = ibapi.DELAYED_ASK_OPTION 257 | DELAYED_LAST_OPTION = ibapi.DELAYED_LAST_OPTION 258 | DELAYED_MODEL_OPTION = ibapi.DELAYED_MODEL_OPTION 259 | LAST_EXCH = ibapi.LAST_EXCH 260 | LAST_REG_TIME = ibapi.LAST_REG_TIME 261 | FUTURES_OPEN_INTEREST = ibapi.FUTURES_OPEN_INTEREST 262 | AVG_OPT_VOLUME = ibapi.AVG_OPT_VOLUME 263 | DELAYED_LAST_TIMESTAMP = ibapi.DELAYED_LAST_TIMESTAMP 264 | SHORTABLE_SHARES = ibapi.SHORTABLE_SHARES 265 | DELAYED_HALTED = ibapi.DELAYED_HALTED 266 | REUTERS_2_MUTUAL_FUNDS = ibapi.REUTERS_2_MUTUAL_FUNDS 267 | ETF_NAV_CLOSE = ibapi.ETF_NAV_CLOSE 268 | ETF_NAV_PRIOR_CLOSE = ibapi.ETF_NAV_PRIOR_CLOSE 269 | ETF_NAV_BID = ibapi.ETF_NAV_BID 270 | ETF_NAV_ASK = ibapi.ETF_NAV_ASK 271 | ETF_NAV_LAST = ibapi.ETF_NAV_LAST 272 | ETF_FROZEN_NAV_LAST = ibapi.ETF_FROZEN_NAV_LAST 273 | ETF_NAV_HIGH = ibapi.ETF_NAV_HIGH 274 | ETF_NAV_LOW = ibapi.ETF_NAV_LOW 275 | SOCIAL_MARKET_ANALYTICS = ibapi.SOCIAL_MARKET_ANALYTICS 276 | ESTIMATED_IPO_MIDPOINT = ibapi.ESTIMATED_IPO_MIDPOINT 277 | FINAL_IPO_LAST = ibapi.FINAL_IPO_LAST 278 | DELAYED_YIELD_BID = ibapi.DELAYED_YIELD_BID 279 | DELAYED_YIELD_ASK = ibapi.DELAYED_YIELD_ASK 280 | NOT_SET = ibapi.NOT_SET 281 | ) 282 | -------------------------------------------------------------------------------- /pubsub.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | "sync" 7 | ) 8 | 9 | const defaultBufferSize = 5 10 | 11 | // UnsubscribeFunc is a function type that can be used to unsubscribe from a topic. 12 | type UnsubscribeFunc func() 13 | 14 | // PubSub is a thread-safe publish-subscribe implementation. 15 | // It manages topic subscriptions and message distribution. 16 | type PubSub struct { 17 | mu sync.RWMutex 18 | topics map[string][]chan string // Map of topics with a list of subscriber channels 19 | } 20 | 21 | // NewPubSub creates and initializes a new PubSub instance. 22 | func NewPubSub() *PubSub { 23 | return &PubSub{ 24 | topics: make(map[string][]chan string), 25 | } 26 | } 27 | 28 | // Subscribe creates a new subscriber for a topic and returns a channel to receive messages. 29 | // It supports optional buffer size specification. 30 | func (ps *PubSub) Subscribe(topic any, size ...int) (<-chan string, UnsubscribeFunc) { 31 | ps.mu.Lock() 32 | defer ps.mu.Unlock() 33 | 34 | t := fmt.Sprint(topic) 35 | 36 | buffSize := defaultBufferSize 37 | if len(size) > 0 { 38 | buffSize = size[0] 39 | } 40 | ch := make(chan string, buffSize) 41 | 42 | ps.topics[t] = append(ps.topics[t], ch) 43 | 44 | return ch, func() { ps.Unsubscribe(topic, ch) } 45 | } 46 | 47 | // Unsubscribe removes a specific subscriber channel from a topic. 48 | // It closes the channel and removes the topic if no subscribers remain. 49 | func (ps *PubSub) Unsubscribe(topic any, subscriberChan <-chan string) { 50 | ps.mu.Lock() 51 | defer ps.mu.Unlock() 52 | 53 | t := fmt.Sprint(topic) 54 | 55 | subscribers, exists := ps.topics[t] 56 | if !exists { 57 | return 58 | } 59 | 60 | for i, ch := range subscribers { 61 | if ch == subscriberChan { 62 | ps.topics[t] = slices.Delete(subscribers, i, i+1) 63 | close(ch) 64 | if len(ps.topics[t]) == 0 { 65 | delete(ps.topics, t) 66 | } 67 | return 68 | } 69 | } 70 | } 71 | 72 | // UnsubscribeAll removes all subscribers from a topic. 73 | // It closes all subscriber channels and deletes the topic from the topics map. 74 | func (ps *PubSub) UnsubscribeAll(topic any) { 75 | ps.mu.Lock() 76 | defer ps.mu.Unlock() 77 | 78 | t := fmt.Sprint(topic) 79 | 80 | // If the topic exists, close all subscriber channels 81 | if subscribers, exists := ps.topics[t]; exists { 82 | for _, ch := range subscribers { 83 | close(ch) // Close each subscriber channel 84 | } 85 | delete(ps.topics, t) // Remove the topic from the map 86 | } 87 | } 88 | 89 | // Publish sends a message to all subscribers of a topic. 90 | func (ps *PubSub) Publish(topic any, msg string) { 91 | ps.mu.RLock() 92 | defer ps.mu.RUnlock() 93 | 94 | t := fmt.Sprint(topic) 95 | 96 | subscribers, exists := ps.topics[t] 97 | if !exists { 98 | return 99 | } 100 | 101 | for _, ch := range subscribers { 102 | ch <- msg // must be blocking or "end" msgs can get through before msgs and will close the channel too early 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pubsub_test.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | // Test basic Publish and Subscribe 10 | func TestPublishSubscribe(t *testing.T) { 11 | pubSub := NewPubSub() 12 | topic := "test_topic" 13 | msg := "test message" 14 | 15 | // Subscribe to a topic 16 | ch, unsubscribe := pubSub.Subscribe(topic) 17 | defer unsubscribe() 18 | 19 | // Publish a message to the topic 20 | pubSub.Publish(topic, msg) 21 | 22 | // Verify that the subscriber receives the message 23 | select { 24 | case received := <-ch: 25 | if received != msg { 26 | t.Errorf("Expected message %s, but got %s", msg, received) 27 | } 28 | case <-time.After(1 * time.Second): 29 | t.Error("Did not receive message on subscribed channel") 30 | } 31 | } 32 | 33 | // Test multiple subscribers 34 | func TestMultipleSubscribers(t *testing.T) { 35 | pubSub := NewPubSub() 36 | topic := "multi_subscribers_topic" 37 | msg := "hello, subscribers!" 38 | 39 | // Subscribe multiple channels to the same topic 40 | ch1, unsubscribeCh1 := pubSub.Subscribe(topic) 41 | defer unsubscribeCh1() 42 | ch2, unsubscribeCh2 := pubSub.Subscribe(topic) 43 | defer unsubscribeCh2() 44 | 45 | // Publish a message to the topic 46 | pubSub.Publish(topic, msg) 47 | 48 | // Verify that both subscribers receive the message 49 | for _, ch := range []<-chan string{ch1, ch2} { 50 | select { 51 | case received := <-ch: 52 | 53 | if received != msg { 54 | t.Errorf("Expected message %s, but got %s", msg, received) 55 | } 56 | case <-time.After(1 * time.Second): 57 | 58 | t.Error("Did not receive message on subscribed channel") 59 | } 60 | } 61 | } 62 | 63 | // Test Unsubscribe 64 | func TestUnsubscribe(t *testing.T) { 65 | pubSub := NewPubSub() 66 | topic := "unsubscribe_test" 67 | ch, _ := pubSub.Subscribe(topic) 68 | pubSub.Unsubscribe(topic, ch) 69 | 70 | select { 71 | case _, open := <-ch: 72 | if open { 73 | t.Error("Expected channel to be closed after unsubscribe") 74 | } 75 | default: 76 | // Success, channel was properly closed 77 | } 78 | } 79 | 80 | // Test UnsubscribeAll 81 | func TestUnsubscribeAll(t *testing.T) { 82 | pubSub := NewPubSub() 83 | topic := "unsubscribe_all_test" 84 | ch1, _ := pubSub.Subscribe(topic) 85 | ch2, _ := pubSub.Subscribe(topic) 86 | 87 | pubSub.UnsubscribeAll(topic) 88 | 89 | // Verify that both channels are closed 90 | for _, ch := range []<-chan string{ch1, ch2} { 91 | select { 92 | case _, open := <-ch: 93 | if open { 94 | t.Error("Expected channel to be closed after UnsubscribeAll") 95 | } 96 | default: 97 | // Success, channel was properly closed 98 | } 99 | } 100 | } 101 | 102 | // Test Publish without subscribers 103 | func TestPublishWithoutSubscribers(t *testing.T) { 104 | pubSub := NewPubSub() 105 | topic := "no_subscriber_topic" 106 | pubSub.Publish(topic, "no subscribers") // No channels subscribed, should proceed without errors 107 | } 108 | 109 | // Test Publish while unsubscribing in parallel 110 | func TestPublishUnsubscribeParallel(t *testing.T) { 111 | pubSub := NewPubSub() 112 | topic := "parallel_publish_unsubscribe" 113 | msg := "parallel message" 114 | 115 | var wg sync.WaitGroup 116 | wg.Add(2) 117 | 118 | _, unsubscribe := pubSub.Subscribe(topic) 119 | 120 | go func() { 121 | defer wg.Done() 122 | pubSub.Publish(topic, msg) 123 | }() 124 | 125 | go func() { 126 | defer wg.Done() 127 | unsubscribe() 128 | }() 129 | 130 | wg.Wait() 131 | } 132 | 133 | func BenchmarkPubSub(b *testing.B) { 134 | pubSub := NewPubSub() 135 | reqID := 1 136 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 137 | contractDetails := NewContractDetails() 138 | contractDetails.Contract = *eurusd 139 | 140 | b.ResetTimer() 141 | 142 | for i := 0; i < b.N; i++ { 143 | ch, cancel := pubSub.Subscribe(reqID) 144 | pubSub.Publish(reqID, Encode(contractDetails)) 145 | msg := <-ch 146 | var cd ContractDetails 147 | if err := Decode(&cd, msg); err != nil { 148 | return 149 | } 150 | cancel() 151 | } 152 | } 153 | 154 | func BenchmarkPubSubBuffered(b *testing.B) { 155 | pubSub := NewPubSub() 156 | reqID := 1 157 | eurusd := NewForex("EUR", "IDEALPRO", "USD") 158 | contractDetails := NewContractDetails() 159 | contractDetails.Contract = *eurusd 160 | 161 | b.ResetTimer() 162 | 163 | for i := 0; i < b.N; i++ { 164 | ch, cancel := pubSub.Subscribe(reqID, 100) 165 | pubSub.Publish(reqID, Encode(contractDetails)) 166 | msg := <-ch 167 | var cd ContractDetails 168 | if err := Decode(&cd, msg); err != nil { 169 | return 170 | } 171 | cancel() 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // ibState holds the data to keep in sync with IB server. 9 | // Note: It is the responsibility of the user to lock and unlock this state! 10 | type ibState struct { 11 | mu sync.Mutex 12 | accounts []string 13 | nextValidID int64 14 | updateAccountTime time.Time 15 | updateAccountValues map[string]AccountValue // Key(account, tag, currency, modelCode) -> AccountValue 16 | accountSummary map[string]AccountValue // Key(account, tag, currency) -> AccountValue 17 | portfolio map[string]map[int64]PortfolioItem // account -> conId -> PortfolioItem 18 | positions map[string]map[int64]Position // account -> conId -> Position 19 | trades map[string]*Trade // permId -> Trade 20 | permID2Trade map[int64]*Trade // permId -> Trade 21 | fills map[string]*Fill // execID -> Fill 22 | msgID2NewsBulletin map[int64]NewsBulletin // msgID -> NewsBulletin 23 | tickers map[*Contract]*Ticker // *Contract -> Ticker 24 | reqID2Ticker map[int64]*Ticker // reqId -> Ticker 25 | ticker2ReqID map[string]map[*Ticker]int64 // Ticker -> reqId 26 | reqID2Pnl map[int64]*Pnl // reqId -> Pnl 27 | reqID2PnlSingle map[int64]*PnlSingle // reqId -> PnlSingle 28 | pnlKey2ReqID map[string]int64 // Key(account, modelCode) -> reqID 29 | pnlSingleKey2ReqID map[string]int64 // Key(account, modelCode, conID) -> reqID 30 | newsTicks []NewsTick 31 | } 32 | 33 | // NewState creates and initializes a new ibState instance. 34 | func NewState() *ibState { 35 | s := &ibState{} 36 | s.reset() 37 | return s 38 | } 39 | 40 | // reset reinitializes all maps and slices in the state to their zero values. 41 | func (s *ibState) reset() { 42 | s.accounts = nil 43 | s.nextValidID = -1 44 | s.updateAccountTime = time.Time{} 45 | s.updateAccountValues = make(map[string]AccountValue) 46 | s.accountSummary = make(map[string]AccountValue) 47 | s.portfolio = make(map[string]map[int64]PortfolioItem) 48 | s.positions = make(map[string]map[int64]Position) 49 | s.trades = make(map[string]*Trade) 50 | s.permID2Trade = make(map[int64]*Trade) 51 | s.fills = make(map[string]*Fill) 52 | s.msgID2NewsBulletin = make(map[int64]NewsBulletin) 53 | s.tickers = make(map[*Contract]*Ticker) 54 | s.reqID2Ticker = make(map[int64]*Ticker) 55 | s.ticker2ReqID = make(map[string]map[*Ticker]int64) 56 | s.reqID2Pnl = make(map[int64]*Pnl) 57 | s.reqID2PnlSingle = make(map[int64]*PnlSingle) 58 | s.pnlKey2ReqID = make(map[string]int64) 59 | s.pnlSingleKey2ReqID = make(map[string]int64) 60 | s.newsTicks = nil 61 | } 62 | 63 | // startTicker registers a new ticker with the state for a specific request ID and contract. 64 | func (s *ibState) startTicker(reqID int64, contract *Contract, tickerType string) *Ticker { 65 | ticker := NewTicker(contract) 66 | s.tickers[contract] = ticker 67 | s.reqID2Ticker[reqID] = ticker 68 | _, ok := s.ticker2ReqID[tickerType] 69 | if !ok { 70 | s.ticker2ReqID[tickerType] = make(map[*Ticker]int64) 71 | } 72 | s.ticker2ReqID[tickerType][ticker] = reqID 73 | return ticker 74 | } 75 | 76 | // endTicker removes a ticker from the state for a specific ticker type. 77 | func (s *ibState) endTicker(ticker *Ticker, tickerType string) (int64, bool) { 78 | reqID, ok := s.ticker2ReqID[tickerType][ticker] 79 | if !ok { 80 | return 0, false 81 | } 82 | delete(s.ticker2ReqID[tickerType], ticker) 83 | return reqID, true 84 | } 85 | 86 | // updateID updates the next requested ID to be at least the specified minimum ID. 87 | func (s *ibState) updateID(minID int64) { 88 | s.nextValidID = max(s.nextValidID, minID) 89 | } 90 | -------------------------------------------------------------------------------- /ticker.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "errors" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Ticker represents real-time market data for a financial contract. 13 | // 14 | // The Ticker struct captures comprehensive market information including: 15 | // - Current price data (bid, ask, last price) 16 | // - Historical price metrics (open, high, low, close) 17 | // - Trading volume and statistics 18 | // - Volatility indicators 19 | // - Options-specific data like greeks 20 | // 21 | // Thread-safe access is provided through mutex-protected getter methods. 22 | // 23 | // Market Data Types: 24 | // - Level 1 streaming ticks stored in 'ticks' 25 | // - Level 2 market depth ticks stored in 'domTicks' 26 | // - Order book (DOM) available in 'domBids' and 'domAsks' 27 | // - Tick-by-tick data stored in 'tickByTicks' 28 | // 29 | // Options Greeks: 30 | // - Bid, ask, and last greeks stored in 'bidGreeks', 'askGreeks', and 'lastGreeks' 31 | // - Model-calculated greeks available in 'modelGreeks' 32 | type Ticker struct { 33 | mu sync.Mutex 34 | contract *Contract 35 | time time.Time 36 | marketDataType int64 37 | minTick float64 38 | bid float64 39 | bidSize Decimal 40 | bidExchange string 41 | ask float64 42 | askSize Decimal 43 | askExchange string 44 | last float64 45 | lastSize Decimal 46 | lastExchange string 47 | lastTimestamp string 48 | prevBid float64 49 | prevBidSize Decimal 50 | prevAsk float64 51 | prevAskSize Decimal 52 | prevLast float64 53 | prevLastSize Decimal 54 | volume Decimal 55 | open float64 56 | high float64 57 | low float64 58 | close float64 59 | vwap float64 60 | low13Week float64 61 | high13Week float64 62 | low26Week float64 63 | high26Week float64 64 | low52Week float64 65 | high52Week float64 66 | bidYield float64 67 | askYield float64 68 | lastYield float64 69 | markPrice float64 70 | halted float64 71 | rtHistVolatility float64 72 | rtVolume float64 73 | rtTradeVolume float64 74 | rtTime time.Time 75 | avVolume Decimal 76 | tradeCount float64 77 | tradeRate float64 78 | volumeRate float64 79 | shortableShares Decimal 80 | indexFuturePremium float64 81 | futuresOpenInterest Decimal 82 | putOpenInterest Decimal 83 | callOpenInterest Decimal 84 | putVolume Decimal 85 | callVolume Decimal 86 | avOptionVolume Decimal 87 | histVolatility float64 88 | impliedVolatility float64 89 | dividends Dividends 90 | fundamentalRatios FundamentalRatios 91 | ticks []TickData 92 | tickByTicks []TickByTick 93 | domBids map[int64]DOMLevel 94 | domAsks map[int64]DOMLevel 95 | domTicks []MktDepthData 96 | bidGreeks TickOptionComputation 97 | askGreeks TickOptionComputation 98 | lastGreeks TickOptionComputation 99 | modelGreeks TickOptionComputation 100 | auctionVolume Decimal 101 | auctionPrice float64 102 | auctionImbalance Decimal 103 | regulatoryImbalance Decimal 104 | bboExchange string 105 | snapshotPermissions int64 106 | } 107 | 108 | // NewTicker creates a new Ticker instance for the given contract. 109 | func NewTicker(contract *Contract) *Ticker { 110 | return &Ticker{ 111 | contract: contract, 112 | domBids: make(map[int64]DOMLevel), 113 | domAsks: make(map[int64]DOMLevel), 114 | } 115 | } 116 | 117 | // Contract returns the financial contract associated with this Ticker. 118 | func (t *Ticker) Contract() *Contract { 119 | t.mu.Lock() 120 | defer t.mu.Unlock() 121 | return t.contract 122 | } 123 | 124 | func (t *Ticker) Time() time.Time { 125 | t.mu.Lock() 126 | defer t.mu.Unlock() 127 | return t.time 128 | } 129 | 130 | func (t *Ticker) MarketDataType() int64 { 131 | t.mu.Lock() 132 | defer t.mu.Unlock() 133 | return t.marketDataType 134 | } 135 | 136 | func (t *Ticker) setMarketDataType(marketDataType int64) { 137 | t.mu.Lock() 138 | defer t.mu.Unlock() 139 | t.marketDataType = marketDataType 140 | } 141 | 142 | func (t *Ticker) MinTick() float64 { 143 | t.mu.Lock() 144 | defer t.mu.Unlock() 145 | return t.minTick 146 | } 147 | 148 | func (t *Ticker) Bid() float64 { 149 | t.mu.Lock() 150 | defer t.mu.Unlock() 151 | return t.bid 152 | } 153 | 154 | func (t *Ticker) BidSize() Decimal { 155 | t.mu.Lock() 156 | defer t.mu.Unlock() 157 | return t.bidSize 158 | } 159 | 160 | func (t *Ticker) BidExchange() string { 161 | t.mu.Lock() 162 | defer t.mu.Unlock() 163 | return t.bidExchange 164 | } 165 | 166 | func (t *Ticker) Ask() float64 { 167 | t.mu.Lock() 168 | defer t.mu.Unlock() 169 | return t.ask 170 | } 171 | 172 | func (t *Ticker) AskSize() Decimal { 173 | t.mu.Lock() 174 | defer t.mu.Unlock() 175 | return t.askSize 176 | } 177 | 178 | func (t *Ticker) AskExchange() string { 179 | t.mu.Lock() 180 | defer t.mu.Unlock() 181 | return t.askExchange 182 | } 183 | 184 | func (t *Ticker) Last() float64 { 185 | t.mu.Lock() 186 | defer t.mu.Unlock() 187 | return t.last 188 | } 189 | 190 | // Getters pour les autres champs 191 | func (t *Ticker) LastSize() Decimal { 192 | t.mu.Lock() 193 | defer t.mu.Unlock() 194 | return t.lastSize 195 | } 196 | 197 | func (t *Ticker) LastExchange() string { 198 | t.mu.Lock() 199 | defer t.mu.Unlock() 200 | return t.lastExchange 201 | } 202 | 203 | func (t *Ticker) PrevBid() float64 { 204 | t.mu.Lock() 205 | defer t.mu.Unlock() 206 | return t.prevBid 207 | } 208 | 209 | func (t *Ticker) PrevBidSize() Decimal { 210 | t.mu.Lock() 211 | defer t.mu.Unlock() 212 | return t.prevBidSize 213 | } 214 | 215 | func (t *Ticker) PrevAsk() float64 { 216 | t.mu.Lock() 217 | defer t.mu.Unlock() 218 | return t.prevAsk 219 | } 220 | 221 | func (t *Ticker) PrevAskSize() Decimal { 222 | t.mu.Lock() 223 | defer t.mu.Unlock() 224 | return t.prevAskSize 225 | } 226 | 227 | func (t *Ticker) PrevLast() float64 { 228 | t.mu.Lock() 229 | defer t.mu.Unlock() 230 | return t.prevLast 231 | } 232 | 233 | func (t *Ticker) PrevLastSize() Decimal { 234 | t.mu.Lock() 235 | defer t.mu.Unlock() 236 | return t.prevLastSize 237 | } 238 | 239 | func (t *Ticker) Volume() Decimal { 240 | t.mu.Lock() 241 | defer t.mu.Unlock() 242 | return t.volume 243 | } 244 | 245 | func (t *Ticker) Open() float64 { 246 | t.mu.Lock() 247 | defer t.mu.Unlock() 248 | return t.open 249 | } 250 | 251 | func (t *Ticker) High() float64 { 252 | t.mu.Lock() 253 | defer t.mu.Unlock() 254 | return t.high 255 | } 256 | 257 | func (t *Ticker) Low() float64 { 258 | t.mu.Lock() 259 | defer t.mu.Unlock() 260 | return t.low 261 | } 262 | 263 | func (t *Ticker) Close() float64 { 264 | t.mu.Lock() 265 | defer t.mu.Unlock() 266 | return t.close 267 | } 268 | 269 | func (t *Ticker) Vwap() float64 { 270 | t.mu.Lock() 271 | defer t.mu.Unlock() 272 | return t.vwap 273 | } 274 | 275 | func (t *Ticker) Low13Week() float64 { 276 | t.mu.Lock() 277 | defer t.mu.Unlock() 278 | return t.low13Week 279 | } 280 | 281 | func (t *Ticker) High13Week() float64 { 282 | t.mu.Lock() 283 | defer t.mu.Unlock() 284 | return t.high13Week 285 | } 286 | 287 | func (t *Ticker) Low26Week() float64 { 288 | t.mu.Lock() 289 | defer t.mu.Unlock() 290 | return t.low26Week 291 | } 292 | 293 | func (t *Ticker) High26Week() float64 { 294 | t.mu.Lock() 295 | defer t.mu.Unlock() 296 | return t.high26Week 297 | } 298 | 299 | func (t *Ticker) Low52Week() float64 { 300 | t.mu.Lock() 301 | defer t.mu.Unlock() 302 | return t.low52Week 303 | } 304 | 305 | func (t *Ticker) High52Week() float64 { 306 | t.mu.Lock() 307 | defer t.mu.Unlock() 308 | return t.high52Week 309 | } 310 | 311 | func (t *Ticker) BidYield() float64 { 312 | t.mu.Lock() 313 | defer t.mu.Unlock() 314 | return t.bidYield 315 | } 316 | 317 | func (t *Ticker) AskYield() float64 { 318 | t.mu.Lock() 319 | defer t.mu.Unlock() 320 | return t.askYield 321 | } 322 | 323 | func (t *Ticker) LastYield() float64 { 324 | t.mu.Lock() 325 | defer t.mu.Unlock() 326 | return t.lastYield 327 | } 328 | 329 | func (t *Ticker) MarkPrice() float64 { 330 | t.mu.Lock() 331 | defer t.mu.Unlock() 332 | return t.markPrice 333 | } 334 | 335 | func (t *Ticker) Halted() float64 { 336 | t.mu.Lock() 337 | defer t.mu.Unlock() 338 | return t.halted 339 | } 340 | 341 | func (t *Ticker) RtHistVolatility() float64 { 342 | t.mu.Lock() 343 | defer t.mu.Unlock() 344 | return t.rtHistVolatility 345 | } 346 | 347 | func (t *Ticker) RtVolume() float64 { 348 | t.mu.Lock() 349 | defer t.mu.Unlock() 350 | return t.rtVolume 351 | } 352 | 353 | func (t *Ticker) RtTradeVolume() float64 { 354 | t.mu.Lock() 355 | defer t.mu.Unlock() 356 | return t.rtTradeVolume 357 | } 358 | 359 | func (t *Ticker) RtTime() time.Time { 360 | t.mu.Lock() 361 | defer t.mu.Unlock() 362 | return t.rtTime 363 | } 364 | 365 | func (t *Ticker) AvVolume() Decimal { 366 | t.mu.Lock() 367 | defer t.mu.Unlock() 368 | return t.avVolume 369 | } 370 | 371 | func (t *Ticker) TradeCount() float64 { 372 | t.mu.Lock() 373 | defer t.mu.Unlock() 374 | return t.tradeCount 375 | } 376 | 377 | func (t *Ticker) TradeRate() float64 { 378 | t.mu.Lock() 379 | defer t.mu.Unlock() 380 | return t.tradeRate 381 | } 382 | 383 | func (t *Ticker) VolumeRate() float64 { 384 | t.mu.Lock() 385 | defer t.mu.Unlock() 386 | return t.volumeRate 387 | } 388 | 389 | func (t *Ticker) ShortableShares() Decimal { 390 | t.mu.Lock() 391 | defer t.mu.Unlock() 392 | return t.shortableShares 393 | } 394 | 395 | func (t *Ticker) IndexFuturePremium() float64 { 396 | t.mu.Lock() 397 | defer t.mu.Unlock() 398 | return t.indexFuturePremium 399 | } 400 | 401 | func (t *Ticker) FuturesOpenInterest() Decimal { 402 | t.mu.Lock() 403 | defer t.mu.Unlock() 404 | return t.futuresOpenInterest 405 | } 406 | 407 | func (t *Ticker) PutOpenInterest() Decimal { 408 | t.mu.Lock() 409 | defer t.mu.Unlock() 410 | return t.putOpenInterest 411 | } 412 | 413 | func (t *Ticker) CallOpenInterest() Decimal { 414 | t.mu.Lock() 415 | defer t.mu.Unlock() 416 | return t.callOpenInterest 417 | } 418 | 419 | func (t *Ticker) PutVolume() Decimal { 420 | t.mu.Lock() 421 | defer t.mu.Unlock() 422 | return t.putVolume 423 | } 424 | 425 | func (t *Ticker) CallVolume() Decimal { 426 | t.mu.Lock() 427 | defer t.mu.Unlock() 428 | return t.callVolume 429 | } 430 | 431 | func (t *Ticker) AvOptionVolume() Decimal { 432 | t.mu.Lock() 433 | defer t.mu.Unlock() 434 | return t.avOptionVolume 435 | } 436 | 437 | func (t *Ticker) HistVolatility() float64 { 438 | t.mu.Lock() 439 | defer t.mu.Unlock() 440 | return t.histVolatility 441 | } 442 | 443 | func (t *Ticker) ImpliedVolatility() float64 { 444 | t.mu.Lock() 445 | defer t.mu.Unlock() 446 | return t.impliedVolatility 447 | } 448 | 449 | func (t *Ticker) Dividends() Dividends { 450 | t.mu.Lock() 451 | defer t.mu.Unlock() 452 | return t.dividends 453 | } 454 | 455 | func (t *Ticker) FundamentalRatios() FundamentalRatios { 456 | t.mu.Lock() 457 | defer t.mu.Unlock() 458 | return t.fundamentalRatios 459 | } 460 | 461 | func (t *Ticker) Ticks() []TickData { 462 | t.mu.Lock() 463 | defer t.mu.Unlock() 464 | return t.ticks 465 | } 466 | 467 | func (t *Ticker) TickByTicks() []TickByTick { 468 | t.mu.Lock() 469 | defer t.mu.Unlock() 470 | return t.tickByTicks 471 | } 472 | 473 | func (t *Ticker) DomBids() []DOMLevel { 474 | t.mu.Lock() 475 | defer t.mu.Unlock() 476 | var dls []DOMLevel 477 | for _, dl := range t.domBids { 478 | dls = append(dls, dl) 479 | } 480 | return dls 481 | } 482 | 483 | func (t *Ticker) DomAsks() []DOMLevel { 484 | t.mu.Lock() 485 | defer t.mu.Unlock() 486 | var dls []DOMLevel 487 | for _, dl := range t.domAsks { 488 | dls = append(dls, dl) 489 | } 490 | return dls 491 | } 492 | 493 | func (t *Ticker) DomTicks() []MktDepthData { 494 | t.mu.Lock() 495 | defer t.mu.Unlock() 496 | return t.domTicks 497 | } 498 | 499 | func (t *Ticker) BidGreeks() TickOptionComputation { 500 | t.mu.Lock() 501 | defer t.mu.Unlock() 502 | return t.bidGreeks 503 | } 504 | 505 | func (t *Ticker) AskGreeks() TickOptionComputation { 506 | t.mu.Lock() 507 | defer t.mu.Unlock() 508 | return t.askGreeks 509 | } 510 | 511 | func (t *Ticker) midGreeks() TickOptionComputation { 512 | if t.bidGreeks.Type() == 0 || t.askGreeks.Type() == 0 { 513 | return TickOptionComputation{} 514 | } 515 | mg := TickOptionComputation{ 516 | ImpliedVol: (t.bidGreeks.ImpliedVol + t.askGreeks.ImpliedVol) / 2, 517 | Delta: (t.bidGreeks.Delta + t.askGreeks.Delta) / 2, 518 | OptPrice: (t.bidGreeks.OptPrice + t.askGreeks.OptPrice) / 2, 519 | PvDividend: (t.bidGreeks.PvDividend + t.askGreeks.PvDividend) / 2, 520 | Gamma: (t.bidGreeks.Gamma + t.askGreeks.Gamma) / 2, 521 | Vega: (t.bidGreeks.Vega + t.askGreeks.Vega) / 2, 522 | Theta: (t.bidGreeks.Theta + t.askGreeks.Theta) / 2, 523 | UndPrice: (t.bidGreeks.UndPrice + t.askGreeks.UndPrice) / 2, 524 | } 525 | return mg 526 | } 527 | 528 | func (t *Ticker) MidGreeks() TickOptionComputation { 529 | t.mu.Lock() 530 | defer t.mu.Unlock() 531 | return t.midGreeks() 532 | } 533 | 534 | func (t *Ticker) LastGreeks() TickOptionComputation { 535 | t.mu.Lock() 536 | defer t.mu.Unlock() 537 | return t.lastGreeks 538 | } 539 | 540 | func (t *Ticker) ModelGreeks() TickOptionComputation { 541 | t.mu.Lock() 542 | defer t.mu.Unlock() 543 | return t.modelGreeks 544 | } 545 | 546 | // Greeks returns the most representative option Greeks. 547 | // 548 | // Selection priority: 549 | // 1. Midpoint Greeks (average of bid and ask Greeks) 550 | // 2. Last trade Greeks 551 | // 3. Model-calculated Greeks 552 | func (t *Ticker) Greeks() TickOptionComputation { 553 | t.mu.Lock() 554 | defer t.mu.Unlock() 555 | greeks := t.midGreeks() 556 | if greeks != (TickOptionComputation{}) { 557 | return greeks 558 | } 559 | if t.lastGreeks != (TickOptionComputation{}) { 560 | return t.lastGreeks 561 | } 562 | if t.modelGreeks != (TickOptionComputation{}) { 563 | return t.modelGreeks 564 | } 565 | return TickOptionComputation{} 566 | } 567 | 568 | func (t *Ticker) AuctionVolume() Decimal { 569 | t.mu.Lock() 570 | defer t.mu.Unlock() 571 | return t.auctionVolume 572 | } 573 | 574 | func (t *Ticker) AuctionPrice() float64 { 575 | t.mu.Lock() 576 | defer t.mu.Unlock() 577 | return t.auctionPrice 578 | } 579 | 580 | func (t *Ticker) AuctionImbalance() Decimal { 581 | t.mu.Lock() 582 | defer t.mu.Unlock() 583 | return t.auctionImbalance 584 | } 585 | 586 | func (t *Ticker) RegulatoryImbalance() Decimal { 587 | t.mu.Lock() 588 | defer t.mu.Unlock() 589 | return t.regulatoryImbalance 590 | } 591 | 592 | func (t *Ticker) BboExchange() string { 593 | t.mu.Lock() 594 | defer t.mu.Unlock() 595 | return t.bboExchange 596 | } 597 | 598 | func (t *Ticker) SnapshotPermissions() int64 { 599 | t.mu.Lock() 600 | defer t.mu.Unlock() 601 | return t.snapshotPermissions 602 | } 603 | 604 | func (t *Ticker) hasBidAsk() bool { 605 | return t.bid > 0 && t.ask > 0 606 | } 607 | 608 | func (t *Ticker) HasBidAsk() bool { 609 | t.mu.Lock() 610 | defer t.mu.Unlock() 611 | return t.hasBidAsk() 612 | } 613 | 614 | // MidPoint calculates the average of the current bid and ask prices. 615 | func (t *Ticker) MidPoint() float64 { 616 | t.mu.Lock() 617 | defer t.mu.Unlock() 618 | if t.hasBidAsk() { 619 | return (t.bid + t.ask) * 0.5 620 | } 621 | return math.NaN() 622 | } 623 | 624 | // MarketPrice determines the most appropriate current market price. 625 | // 626 | // Price selection priority: 627 | // 1. If last price is within the bid-ask spread, use last price. 628 | // 2. If no last price fits the spread, use midpoint (average of bid and ask). 629 | // 3. If no bid-ask available, return last price. 630 | func (t *Ticker) MarketPrice() float64 { 631 | t.mu.Lock() 632 | defer t.mu.Unlock() 633 | if t.hasBidAsk() { 634 | if t.bid <= t.last && t.last <= t.ask { 635 | return t.last 636 | } 637 | return (t.bid + t.ask) * 0.5 638 | } 639 | return t.last 640 | } 641 | 642 | func (t *Ticker) SetTickPrice(tp TickPrice) { 643 | t.mu.Lock() 644 | defer t.mu.Unlock() 645 | var size Decimal 646 | switch tp.TickType { 647 | case BID, DELAYED_BID: 648 | if tp.Price == t.bid { 649 | return 650 | } 651 | t.prevBid = t.bid 652 | t.bid = tp.Price 653 | case ASK, DELAYED_ASK: 654 | if tp.Price == t.ask { 655 | return 656 | } 657 | t.prevAsk = t.ask 658 | t.ask = tp.Price 659 | case LAST, DELAYED_LAST: 660 | if tp.Price == t.last { 661 | return 662 | } 663 | t.prevLast = t.last 664 | t.last = tp.Price 665 | case HIGH, DELAYED_HIGH: 666 | t.high = tp.Price 667 | case LOW, DELAYED_LOW: 668 | t.low = tp.Price 669 | case CLOSE, DELAYED_CLOSE: 670 | t.close = tp.Price 671 | case OPEN, DELAYED_OPEN: 672 | t.open = tp.Price 673 | case LOW_13_WEEK: 674 | t.low13Week = tp.Price 675 | case HIGH_13_WEEK: 676 | t.high13Week = tp.Price 677 | case LOW_26_WEEK: 678 | t.low26Week = tp.Price 679 | case HIGH_26_WEEK: 680 | t.high26Week = tp.Price 681 | case LOW_52_WEEK: 682 | t.low52Week = tp.Price 683 | case HIGH_52_WEEK: 684 | t.high52Week = tp.Price 685 | case AUCTION_PRICE: 686 | t.auctionPrice = tp.Price 687 | case MARK_PRICE: 688 | t.markPrice = tp.Price 689 | case BID_YIELD, DELAYED_YIELD_BID: 690 | t.bidYield = tp.Price 691 | case ASK_YIELD, DELAYED_YIELD_ASK: 692 | t.askYield = tp.Price 693 | case LAST_YIELD: 694 | t.lastYield = tp.Price 695 | default: 696 | log.Warn().Err(errUnknownTickType).Int64("TickType", tp.TickType).Msg("SetTickPrice") 697 | } 698 | td := TickData{ 699 | Time: time.Now().UTC(), 700 | TickType: tp.TickType, 701 | Price: tp.Price, 702 | Size: size, 703 | } 704 | t.ticks = append(t.ticks, td) 705 | } 706 | 707 | func (t *Ticker) SetTickSize(ts TickSize) { 708 | t.mu.Lock() 709 | defer t.mu.Unlock() 710 | var price float64 711 | switch ts.TickType { 712 | case BID_SIZE, DELAYED_BID_SIZE: 713 | if ts.Size == t.bidSize { 714 | return 715 | } 716 | price = t.bid 717 | t.prevBidSize = t.bidSize 718 | t.bidSize = ts.Size 719 | case ASK_SIZE, DELAYED_ASK_SIZE: 720 | if ts.Size == t.askSize { 721 | return 722 | } 723 | price = t.ask 724 | t.prevAskSize = t.askSize 725 | t.askSize = ts.Size 726 | case LAST_SIZE, DELAYED_LAST_SIZE: 727 | price = t.last 728 | if price == 0 { 729 | return 730 | } 731 | if ts.Size != t.lastSize { 732 | t.prevLastSize = t.lastSize 733 | t.lastSize = ts.Size 734 | } 735 | case VOLUME, DELAYED_VOLUME: 736 | t.volume = ts.Size 737 | case AVG_VOLUME: 738 | t.avVolume = ts.Size 739 | case OPTION_CALL_OPEN_INTEREST: 740 | t.callOpenInterest = ts.Size 741 | case OPTION_PUT_OPEN_INTEREST: 742 | t.putOpenInterest = ts.Size 743 | case OPTION_CALL_VOLUME: 744 | t.callVolume = ts.Size 745 | case OPTION_PUT_VOLUME: 746 | t.putVolume = ts.Size 747 | case AUCTION_VOLUME: 748 | t.auctionVolume = ts.Size 749 | case AUCTION_IMBALANCE: 750 | t.auctionImbalance = ts.Size 751 | case REGULATORY_IMBALANCE: 752 | t.regulatoryImbalance = ts.Size 753 | case FUTURES_OPEN_INTEREST: 754 | t.futuresOpenInterest = ts.Size 755 | case AVG_OPT_VOLUME: 756 | t.avOptionVolume = ts.Size 757 | case SHORTABLE_SHARES: 758 | t.shortableShares = ts.Size 759 | default: 760 | log.Warn().Err(errUnknownTickType).Int64("TickType", ts.TickType).Msg("SetTickSize") 761 | } 762 | td := TickData{ 763 | Time: time.Now().UTC(), 764 | TickType: ts.TickType, 765 | Price: price, 766 | Size: ts.Size, 767 | } 768 | t.ticks = append(t.ticks, td) 769 | } 770 | 771 | func (t *Ticker) SetTickOptionComputation(toc TickOptionComputation) { 772 | t.mu.Lock() 773 | defer t.mu.Unlock() 774 | switch toc.TickType { 775 | case BID_OPTION_COMPUTATION, DELAYED_BID_OPTION: 776 | t.bidGreeks = toc 777 | case ASK_OPTION_COMPUTATION, DELAYED_ASK_OPTION: 778 | t.askGreeks = toc 779 | case LAST_OPTION_COMPUTATION, DELAYED_LAST_OPTION: 780 | t.lastGreeks = toc 781 | case MODEL_OPTION, DELAYED_MODEL_OPTION: 782 | t.modelGreeks = toc 783 | default: 784 | log.Warn().Err(errUnknownTickType).Int64("TickType", toc.TickType).Msg("SetTickOptionComputation") 785 | } 786 | } 787 | 788 | func (t *Ticker) SetTickGeneric(tg TickGeneric) { 789 | t.mu.Lock() 790 | defer t.mu.Unlock() 791 | switch tg.TickType { 792 | case OPTION_HISTORICAL_VOL: 793 | t.histVolatility = tg.Value 794 | case OPTION_IMPLIED_VOL: 795 | t.impliedVolatility = tg.Value 796 | case INDEX_FUTURE_PREMIUM: 797 | t.indexFuturePremium = tg.Value 798 | case HALTED, DELAYED_HALTED: 799 | t.halted = tg.Value 800 | case TRADE_COUNT: 801 | t.tradeCount = tg.Value 802 | case TRADE_RATE: 803 | t.tradeRate = tg.Value 804 | case VOLUME_RATE: 805 | t.volumeRate = tg.Value 806 | case RT_HISTORICAL_VOL: 807 | t.rtHistVolatility = tg.Value 808 | default: 809 | log.Warn().Err(errUnknownTickType).Int64("TickType", tg.TickType).Float64("TickValue", tg.Value).Msg("SetTickGeneric") 810 | } 811 | td := TickData{ 812 | Time: time.Now().UTC(), 813 | TickType: tg.TickType, 814 | Price: tg.Value, 815 | Size: ZERO, 816 | } 817 | t.ticks = append(t.ticks, td) 818 | } 819 | 820 | func (t *Ticker) SetTickString(ts TickString) { 821 | t.mu.Lock() 822 | defer t.mu.Unlock() 823 | switch ts.TickType { 824 | case BID_EXCH: 825 | t.bidExchange = ts.Value 826 | case ASK_EXCH: 827 | t.askExchange = ts.Value 828 | case LAST_EXCH: 829 | t.lastExchange = ts.Value 830 | case LAST_TIMESTAMP, DELAYED_LAST_TIMESTAMP: 831 | t.lastTimestamp = ts.Value 832 | case FUNDAMENTAL_RATIOS: 833 | d := make(FundamentalRatios) 834 | for _, t := range strings.Split(ts.Value, ";") { 835 | if t == "" { 836 | continue 837 | } 838 | kv := strings.Split(t, "=") 839 | if len(kv) == 2 { 840 | k, v := kv[0], kv[1] 841 | if v == "-99999.99" { 842 | d[k] = UNSET_FLOAT 843 | continue 844 | } 845 | f, err := strconv.ParseFloat(v, 64) 846 | if err != nil { 847 | log.Warn().Err(errors.New("fundamental ratio error")).Str("key", k).Str("value", v).Msg("SetTickString") 848 | continue 849 | } 850 | d[k] = f 851 | } 852 | } 853 | t.fundamentalRatios = d 854 | case RT_VOLUME, RT_TRD_VOLUME: 855 | // RT Volume or RT Trade Volume value: " price;size;ms since epoch;total volume;VWAP;single trade" 856 | split := strings.Split(ts.Value, ";") 857 | if split[3] != "" { 858 | f, err := strconv.ParseFloat(split[3], 64) 859 | if err != nil { 860 | log.Error().Err(err).Msg("") 861 | } 862 | if ts.TickType == RT_VOLUME { 863 | t.rtVolume = f 864 | } else { 865 | t.rtTradeVolume = f 866 | } 867 | } 868 | if split[4] != "" { 869 | f, err := strconv.ParseFloat(split[4], 64) 870 | if err != nil { 871 | log.Error().Err(err).Msg("") 872 | } 873 | t.vwap = f 874 | } 875 | if split[2] != "" { 876 | d, err := ParseIBTime(split[2]) 877 | if err != nil { 878 | log.Error().Err(err).Msg("") 879 | } 880 | t.rtTime = d 881 | } 882 | if split[0] != "" { 883 | return 884 | } 885 | price, err := strconv.ParseFloat(split[0], 64) 886 | if err != nil { 887 | log.Error().Err(err).Msg("") 888 | } 889 | if split[1] != "" { 890 | size := StringToDecimal(split[1]) 891 | if err != nil { 892 | log.Error().Err(err).Msg("") 893 | } 894 | if t.prevLast != t.last { 895 | t.prevLast = t.last 896 | t.last = price 897 | } 898 | if t.prevLastSize != t.lastSize { 899 | t.prevLastSize = t.lastSize 900 | t.lastSize = size 901 | } 902 | td := TickData{ 903 | Time: time.Now().UTC(), 904 | TickType: ts.TickType, 905 | Price: price, 906 | Size: size, 907 | } 908 | t.ticks = append(t.ticks, td) 909 | } 910 | case IB_DIVIDENDS: 911 | // Dividend Value: "past12,next12,nextDate,nextAmount" 912 | split := strings.Split(ts.Value, ",") 913 | ds := Dividends{} 914 | if split[0] != "" { 915 | f, err := strconv.ParseFloat(split[0], 64) 916 | if err != nil { 917 | log.Error().Err(err).Msg("") 918 | } 919 | ds.Past12Months = f 920 | } 921 | if split[1] != "" { 922 | f, err := strconv.ParseFloat(split[1], 64) 923 | if err != nil { 924 | log.Error().Err(err).Msg("") 925 | } 926 | ds.Next12Months = f 927 | } 928 | if split[2] != "" { 929 | d, err := ParseIBTime(split[2]) 930 | if err != nil { 931 | log.Error().Err(err).Msg("") 932 | } 933 | ds.NextDate = d 934 | } 935 | if split[3] != "" { 936 | f, err := strconv.ParseFloat(split[3], 64) 937 | if err != nil { 938 | log.Error().Err(err).Msg("") 939 | } 940 | ds.NextAmount = f 941 | } 942 | t.dividends = ds 943 | default: 944 | log.Warn().Err(errUnknownTickType).Int64("TickType", ts.TickType).Msg("SetTickString") 945 | } 946 | } 947 | 948 | func (t *Ticker) SetTickEFP(te TickEFP) { 949 | t.mu.Lock() 950 | defer t.mu.Unlock() 951 | // TODO 952 | } 953 | 954 | func (t *Ticker) SetTickByTickAllLast(tbt TickByTickAllLast) { 955 | t.mu.Lock() 956 | defer t.mu.Unlock() 957 | if tbt.Price != t.last { 958 | t.prevLast = t.last 959 | t.last = tbt.Price 960 | } 961 | if tbt.Size != t.lastSize { 962 | t.prevLastSize = t.lastSize 963 | t.lastSize = tbt.Size 964 | } 965 | t.tickByTicks = append(t.tickByTicks, tbt) 966 | } 967 | 968 | func (t *Ticker) SetTickByTickBidAsk(tbt TickByTickBidAsk) { 969 | t.mu.Lock() 970 | defer t.mu.Unlock() 971 | if tbt.BidPrice != t.bid { 972 | t.prevBid = t.bid 973 | t.bid = tbt.BidPrice 974 | } 975 | if tbt.BidSize != t.bidSize { 976 | t.prevBidSize = t.bidSize 977 | t.bidSize = tbt.BidSize 978 | } 979 | if tbt.AskPrice != t.ask { 980 | t.prevAsk = t.ask 981 | t.ask = tbt.AskPrice 982 | } 983 | if tbt.AskSize != t.askSize { 984 | t.prevAskSize = t.askSize 985 | t.askSize = tbt.AskSize 986 | } 987 | t.tickByTicks = append(t.tickByTicks, tbt) 988 | } 989 | 990 | func (t *Ticker) SetTickByTickMidPoint(tbt TickByTickMidPoint) { 991 | t.mu.Lock() 992 | defer t.mu.Unlock() 993 | t.tickByTicks = append(t.tickByTicks, tbt) 994 | } 995 | 996 | func (t *Ticker) String() string { 997 | return Stringify(struct { 998 | Contract *Contract 999 | Time time.Time 1000 | MarketDataType int64 1001 | MinTick float64 1002 | Bid float64 1003 | BidSize Decimal 1004 | BidExchange string 1005 | Ask float64 1006 | AskSize Decimal 1007 | AskExchange string 1008 | Last float64 1009 | LastSize Decimal 1010 | LastExchange string 1011 | LastTimestamp string 1012 | PrevBid float64 1013 | PrevBidSize Decimal 1014 | PrevAsk float64 1015 | PrevAskSize Decimal 1016 | PrevLast float64 1017 | PrevLastSize Decimal 1018 | Volume Decimal 1019 | Open float64 1020 | High float64 1021 | Low float64 1022 | Close float64 1023 | Vwap float64 1024 | Low13Week float64 1025 | High13Week float64 1026 | Low26Week float64 1027 | High26Week float64 1028 | Low52Week float64 1029 | High52Week float64 1030 | BidYield float64 1031 | AskYield float64 1032 | LastYield float64 1033 | MarkPrice float64 1034 | Halted float64 1035 | RtHistVolatility float64 1036 | RtVolume float64 1037 | RtTradeVolume float64 1038 | RtTime time.Time 1039 | AvVolume Decimal 1040 | TradeCount float64 1041 | TradeRate float64 1042 | VolumeRate float64 1043 | ShortableShares Decimal 1044 | IndexFuturePremium float64 1045 | FuturesOpenInterest Decimal 1046 | PutOpenInterest Decimal 1047 | CallOpenInterest Decimal 1048 | PutVolume Decimal 1049 | CallVolume Decimal 1050 | AvOptionVolume Decimal 1051 | HistVolatility float64 1052 | ImpliedVolatility float64 1053 | Dividends Dividends 1054 | FundamentalRatios FundamentalRatios 1055 | Ticks []TickData 1056 | TickByTicks []TickByTick 1057 | DomBids map[int64]DOMLevel 1058 | DomAsks map[int64]DOMLevel 1059 | DomTicks []MktDepthData 1060 | BidGreeks TickOptionComputation 1061 | AskGreeks TickOptionComputation 1062 | LastGreeks TickOptionComputation 1063 | ModelGreeks TickOptionComputation 1064 | AuctionVolume Decimal 1065 | AuctionPrice float64 1066 | AuctionImbalance Decimal 1067 | RegulatoryImbalance Decimal 1068 | BboExchange string 1069 | SnapshotPermissions int64 1070 | }{ 1071 | Contract: t.contract, 1072 | Time: t.time, 1073 | MarketDataType: t.marketDataType, 1074 | MinTick: t.minTick, 1075 | Bid: t.bid, 1076 | BidSize: t.bidSize, 1077 | BidExchange: t.bidExchange, 1078 | Ask: t.ask, 1079 | AskSize: t.askSize, 1080 | AskExchange: t.askExchange, 1081 | Last: t.last, 1082 | LastSize: t.lastSize, 1083 | LastExchange: t.lastExchange, 1084 | LastTimestamp: t.lastTimestamp, 1085 | PrevBid: t.prevBid, 1086 | PrevBidSize: t.prevBidSize, 1087 | PrevAsk: t.prevAsk, 1088 | PrevAskSize: t.prevAskSize, 1089 | PrevLast: t.prevLast, 1090 | PrevLastSize: t.prevLastSize, 1091 | Volume: t.volume, 1092 | Open: t.open, 1093 | High: t.high, 1094 | Low: t.low, 1095 | Close: t.close, 1096 | Vwap: t.vwap, 1097 | Low13Week: t.low13Week, 1098 | High13Week: t.high13Week, 1099 | Low26Week: t.low26Week, 1100 | High26Week: t.high26Week, 1101 | Low52Week: t.low52Week, 1102 | High52Week: t.high52Week, 1103 | BidYield: t.bidYield, 1104 | AskYield: t.askYield, 1105 | LastYield: t.lastYield, 1106 | MarkPrice: t.markPrice, 1107 | Halted: t.halted, 1108 | RtHistVolatility: t.rtHistVolatility, 1109 | RtVolume: t.rtVolume, 1110 | RtTradeVolume: t.rtTradeVolume, 1111 | RtTime: t.rtTime, 1112 | AvVolume: t.avVolume, 1113 | TradeCount: t.tradeCount, 1114 | TradeRate: t.tradeRate, 1115 | VolumeRate: t.volumeRate, 1116 | ShortableShares: t.shortableShares, 1117 | IndexFuturePremium: t.indexFuturePremium, 1118 | FuturesOpenInterest: t.futuresOpenInterest, 1119 | PutOpenInterest: t.putOpenInterest, 1120 | CallOpenInterest: t.callOpenInterest, 1121 | PutVolume: t.putVolume, 1122 | CallVolume: t.callVolume, 1123 | AvOptionVolume: t.avOptionVolume, 1124 | HistVolatility: t.histVolatility, 1125 | ImpliedVolatility: t.impliedVolatility, 1126 | Dividends: t.dividends, 1127 | FundamentalRatios: t.fundamentalRatios, 1128 | Ticks: t.ticks, 1129 | TickByTicks: t.tickByTicks, 1130 | DomBids: t.domBids, 1131 | DomAsks: t.domAsks, 1132 | DomTicks: t.domTicks, 1133 | BidGreeks: t.bidGreeks, 1134 | AskGreeks: t.askGreeks, 1135 | LastGreeks: t.lastGreeks, 1136 | ModelGreeks: t.modelGreeks, 1137 | AuctionVolume: t.auctionVolume, 1138 | AuctionPrice: t.auctionPrice, 1139 | AuctionImbalance: t.auctionImbalance, 1140 | RegulatoryImbalance: t.regulatoryImbalance, 1141 | BboExchange: t.bboExchange, 1142 | SnapshotPermissions: t.snapshotPermissions, 1143 | }) 1144 | } 1145 | -------------------------------------------------------------------------------- /trade.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Status represents the current state of an order in the trading system. 10 | type Status string 11 | 12 | // Order status constants define all possible states an order can be in. 13 | const ( 14 | PendingSubmit Status = "PendingSubmit" // Order is pending submission to IB 15 | PendingCancel Status = "PendingCancel" // Order cancellation is pending 16 | PreSubmitted Status = "PreSubmitted" // Order has been sent but not yet confirmed 17 | Submitted Status = "Submitted" // Order has been submitted to IB 18 | ApiPending Status = "ApiPending" // Order is pending processing by the API 19 | ApiCancelled Status = "ApiCancelled" // Order was cancelled through the API 20 | Cancelled Status = "Cancelled" // Order has been cancelled 21 | Filled Status = "Filled" // Order has been completely filled 22 | Inactive Status = "Inactive" // Order is inactive 23 | ) 24 | 25 | // IsActive returns true if the status indicates the order is still active in the market. 26 | func (s Status) IsActive() bool { 27 | switch s { 28 | case PendingSubmit, ApiPending, PreSubmitted, Submitted: 29 | return true 30 | default: 31 | return false 32 | } 33 | } 34 | 35 | // IsDone returns true if the status indicates the order has reached a terminal state. 36 | func (s Status) IsDone() bool { 37 | switch s { 38 | case Filled, Cancelled, ApiCancelled: 39 | return true 40 | default: 41 | return false 42 | } 43 | } 44 | 45 | // OrderStatus represents the current state and details of an order. 46 | type OrderStatus struct { 47 | OrderID int64 // Unique identifier for the order 48 | Status Status // Current status of the order 49 | Filled Decimal // Amount of order that has been filled 50 | Remaining Decimal // Amount of order remaining to be filled 51 | AvgFillPrice float64 // Average price of filled portions 52 | PermID int64 // Permanent ID assigned by IB 53 | ParentID int64 // ID of parent order if this is a child order 54 | LastFillPrice float64 // Price of the last fill 55 | ClientID int64 // Client identifier 56 | WhyHeld string // Reason why order is being held 57 | MktCapPrice float64 // Market cap price 58 | } 59 | 60 | // IsActive returns true if the order status indicates an active order. 61 | func (os OrderStatus) IsActive() bool { 62 | return os.Status.IsActive() 63 | } 64 | 65 | // IsDone returns true if the order status indicates the order has reached a terminal state. 66 | func (os OrderStatus) IsDone() bool { 67 | return os.Status.IsDone() 68 | } 69 | 70 | // Fill represents a single execution fill of an order, including contract details, 71 | // execution information, and commission data. 72 | type Fill struct { 73 | Contract *Contract // Contract details for the filled order 74 | Execution *Execution // Execution details of the fill 75 | CommissionAndFeesReport CommissionAndFeesReport // Commission and fees information for the fill 76 | Time time.Time // Timestamp of the fill 77 | } 78 | 79 | // PassesExecutionFilter checks if the fill matches the specified execution filter criteria. 80 | func (f *Fill) Matches(filter *ExecutionFilter) bool { 81 | if f == nil { 82 | return false 83 | } 84 | if filter.AcctCode != "" && filter.AcctCode != f.Execution.AcctNumber { 85 | return false 86 | } 87 | if filter.ClientID != 0 && filter.ClientID != f.Execution.ClientID { 88 | return false 89 | } 90 | if filter.Exchange != "" && filter.Exchange != f.Execution.Exchange { 91 | return false 92 | } 93 | if filter.SecType != "" && filter.SecType != f.Contract.SecType { 94 | return false 95 | } 96 | if filter.Side != "" && filter.Side != f.Execution.Side { 97 | return false 98 | } 99 | if filter.Symbol != "" && filter.Symbol != f.Contract.Symbol { 100 | return false 101 | } 102 | if filter.Time != "" { 103 | filterTime, err := ParseIBTime(filter.Time) 104 | if err != nil { 105 | log.Error().Err(err).Msg("PassesExecutionFilter") 106 | return false 107 | } 108 | if f.Time.Before(filterTime) { 109 | return false 110 | } 111 | } 112 | return true 113 | } 114 | 115 | // TradeLogEntry represents a single entry in the trade's log, recording status changes 116 | // and other significant events. 117 | type TradeLogEntry struct { 118 | Time time.Time // Timestamp of the log entry 119 | Status Status // Status at the time of the log entry 120 | Message string // Descriptive message about the event 121 | ErrorCode int64 // Error code if applicable 122 | } 123 | 124 | // Trade represents a complete trading operation, including the contract, order details, 125 | // current status, and execution fills. 126 | type Trade struct { 127 | Contract *Contract 128 | Order *Order 129 | OrderStatus OrderStatus 130 | mu sync.RWMutex 131 | fills []*Fill 132 | logs []TradeLogEntry 133 | done chan struct{} 134 | } 135 | 136 | /* func (t* Trade) Equal(other Trade) bool{ 137 | t.Order 138 | } */ 139 | 140 | // NewTrade creates a new Trade instance with the specified contract and order details. 141 | // Optional initial order status can be provided. 142 | func NewTrade(contract *Contract, order *Order, orderStatus ...OrderStatus) *Trade { 143 | var os OrderStatus 144 | if len(orderStatus) > 0 { 145 | os = orderStatus[0] 146 | } else { 147 | os = OrderStatus{ 148 | OrderID: order.OrderID, 149 | Status: PendingSubmit, 150 | } 151 | } 152 | return &Trade{ 153 | Contract: contract, 154 | Order: order, 155 | OrderStatus: os, 156 | fills: make([]*Fill, 0), 157 | logs: []TradeLogEntry{{Time: time.Now().UTC(), Status: PendingSubmit}}, 158 | done: make(chan struct{}), 159 | } 160 | } 161 | 162 | // IsActive returns true if the trade is currently active in the market. 163 | func (t *Trade) IsActive() bool { 164 | t.mu.RLock() 165 | defer t.mu.RUnlock() 166 | return t.isActive() 167 | } 168 | 169 | // isActive is an internal helper that checks if the trade is active without locking. 170 | func (t *Trade) isActive() bool { 171 | return t.OrderStatus.IsActive() 172 | } 173 | 174 | // IsDone returns true if the trade has reached a terminal state. 175 | func (t *Trade) IsDone() bool { 176 | t.mu.RLock() 177 | defer t.mu.RUnlock() 178 | return t.isDone() 179 | } 180 | 181 | // isDone is an internal helper that checks if the trade is done without locking. 182 | func (t *Trade) isDone() bool { 183 | return t.OrderStatus.IsDone() 184 | } 185 | 186 | // Done returns a channel that will be closed when the trade reaches a terminal state. 187 | func (t *Trade) Done() <-chan struct{} { 188 | return t.done 189 | } 190 | 191 | // markDone closes the done channel to signal trade completion. 192 | // This is an internal method and should be called with appropriate locking. 193 | func (t *Trade) markDone() { 194 | // Ensure that the done channel is closed only once 195 | select { 196 | case <-t.done: 197 | // Channel already closed 198 | default: 199 | close(t.done) 200 | } 201 | } 202 | 203 | // markDoneSafe safely marks the trade as done with proper locking. 204 | func (t *Trade) markDoneSafe() { 205 | t.mu.RLock() 206 | defer t.mu.RUnlock() 207 | t.markDone() 208 | } 209 | 210 | // Fills returns a copy of all fills for this trade 211 | func (t *Trade) Fills() []*Fill { 212 | t.mu.RLock() 213 | defer t.mu.RUnlock() 214 | 215 | fills := make([]*Fill, len(t.fills)) 216 | copy(fills, t.fills) 217 | return fills 218 | } 219 | 220 | // addFill adds a new fill to the trade's fill history 221 | func (t *Trade) addFill(fill *Fill) { 222 | t.fills = append(t.fills, fill) 223 | } 224 | 225 | // Logs returns a copy of all log entries for this trade 226 | func (t *Trade) Logs() []TradeLogEntry { 227 | t.mu.RLock() 228 | defer t.mu.RUnlock() 229 | 230 | logs := make([]TradeLogEntry, len(t.logs)) 231 | copy(logs, t.logs) 232 | return logs 233 | } 234 | 235 | // addLog adds a new log entry to the trade's history 236 | func (t *Trade) addLog(tradeLogEntry TradeLogEntry) { 237 | t.logs = append(t.logs, tradeLogEntry) 238 | } 239 | 240 | func (t *Trade) Equal(other *Trade) bool { 241 | return t.Order.HasSameID(other.Order) 242 | } 243 | 244 | func (t *Trade) String() string { 245 | t.mu.RLock() 246 | defer t.mu.RUnlock() 247 | 248 | return fmt.Sprintf("Trade{Contract: %v, Order: %v, Status: %v, Fills: %d}", 249 | t.Contract, t.Order, t.OrderStatus, len(t.fills)) 250 | } 251 | -------------------------------------------------------------------------------- /trade_test.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestStatus_IsActive(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | status Status 13 | want bool 14 | }{ 15 | {"PendingSubmit is active", PendingSubmit, true}, 16 | {"ApiPending is active", ApiPending, true}, 17 | {"PreSubmitted is active", PreSubmitted, true}, 18 | {"Submitted is active", Submitted, true}, 19 | {"Cancelled is not active", Cancelled, false}, 20 | {"Filled is not active", Filled, false}, 21 | {"ApiCancelled is not active", ApiCancelled, false}, 22 | {"Inactive is not active", Inactive, false}, 23 | {"Empty status is not active", Status(""), false}, 24 | } 25 | 26 | for _, tt := range tests { 27 | t.Run(tt.name, func(t *testing.T) { 28 | if got := tt.status.IsActive(); got != tt.want { 29 | t.Errorf("Status.IsActive() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestStatus_IsDone(t *testing.T) { 36 | tests := []struct { 37 | name string 38 | status Status 39 | want bool 40 | }{ 41 | {"Filled is done", Filled, true}, 42 | {"Cancelled is done", Cancelled, true}, 43 | {"ApiCancelled is done", ApiCancelled, true}, 44 | {"PendingSubmit is not done", PendingSubmit, false}, 45 | {"Submitted is not done", Submitted, false}, 46 | {"PreSubmitted is not done", PreSubmitted, false}, 47 | {"ApiPending is not done", ApiPending, false}, 48 | {"Inactive is not done", Inactive, false}, 49 | {"Empty status is not done", Status(""), false}, 50 | } 51 | 52 | for _, tt := range tests { 53 | t.Run(tt.name, func(t *testing.T) { 54 | if got := tt.status.IsDone(); got != tt.want { 55 | t.Errorf("Status.IsDone() = %v, want %v", got, tt.want) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func TestFill_Matches(t *testing.T) { 62 | timeStr := "20240102-15:04:05" 63 | testTime, _ := time.Parse("20060102-15:04:05", timeStr) 64 | 65 | tests := []struct { 66 | name string 67 | fill *Fill 68 | filter *ExecutionFilter 69 | want bool 70 | }{ 71 | { 72 | name: "nil fill never matches", 73 | fill: nil, 74 | filter: &ExecutionFilter{}, 75 | want: false, 76 | }, 77 | { 78 | name: "empty filter matches valid fill", 79 | fill: &Fill{ 80 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 81 | Execution: &Execution{ 82 | AcctNumber: "123", 83 | ClientID: 456, 84 | Exchange: "NASDAQ", 85 | Side: "BUY", 86 | }, 87 | Time: testTime, 88 | }, 89 | filter: NewExecutionFilter(), 90 | want: true, 91 | }, 92 | { 93 | name: "matching all criteria", 94 | fill: &Fill{ 95 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 96 | Execution: &Execution{ 97 | AcctNumber: "123", 98 | ClientID: 456, 99 | Exchange: "NASDAQ", 100 | Side: "BUY", 101 | }, 102 | Time: testTime, 103 | }, 104 | filter: &ExecutionFilter{ 105 | AcctCode: "123", 106 | ClientID: 456, 107 | Exchange: "NASDAQ", 108 | SecType: "STK", 109 | Side: "BUY", 110 | Symbol: "AAPL", 111 | Time: timeStr, 112 | }, 113 | want: true, 114 | }, 115 | { 116 | name: "non-matching account", 117 | fill: &Fill{ 118 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 119 | Execution: &Execution{AcctNumber: "123"}, 120 | }, 121 | filter: func() *ExecutionFilter { 122 | f := NewExecutionFilter() 123 | f.AcctCode = "456" 124 | return f 125 | }(), 126 | want: false, 127 | }, 128 | { 129 | name: "non-matching client ID", 130 | fill: &Fill{ 131 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 132 | Execution: &Execution{ 133 | AcctNumber: "123", 134 | ClientID: 456, 135 | }, 136 | }, 137 | filter: func() *ExecutionFilter { 138 | f := NewExecutionFilter() 139 | f.ClientID = 789 140 | return f 141 | }(), 142 | want: false, 143 | }, 144 | { 145 | name: "non-matching exchange", 146 | fill: &Fill{ 147 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 148 | Execution: &Execution{ 149 | AcctNumber: "123", 150 | Exchange: "NASDAQ", 151 | }, 152 | }, 153 | filter: func() *ExecutionFilter { 154 | f := NewExecutionFilter() 155 | f.Exchange = "NYSE" 156 | return f 157 | }(), 158 | want: false, 159 | }, 160 | { 161 | name: "non-matching security type", 162 | fill: &Fill{ 163 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 164 | Execution: &Execution{AcctNumber: "123"}, 165 | }, 166 | filter: func() *ExecutionFilter { 167 | f := NewExecutionFilter() 168 | f.SecType = "OPT" 169 | return f 170 | }(), 171 | want: false, 172 | }, 173 | { 174 | name: "non-matching side", 175 | fill: &Fill{ 176 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 177 | Execution: &Execution{ 178 | AcctNumber: "123", 179 | Side: "BUY", 180 | }, 181 | }, 182 | filter: func() *ExecutionFilter { 183 | f := NewExecutionFilter() 184 | f.Side = "SELL" 185 | return f 186 | }(), 187 | want: false, 188 | }, 189 | { 190 | name: "non-matching symbol", 191 | fill: &Fill{ 192 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 193 | Execution: &Execution{AcctNumber: "123"}, 194 | }, 195 | filter: func() *ExecutionFilter { 196 | f := NewExecutionFilter() 197 | f.Symbol = "MSFT" 198 | return f 199 | }(), 200 | want: false, 201 | }, 202 | { 203 | name: "non-matching time", 204 | fill: &Fill{ 205 | Contract: &Contract{Symbol: "AAPL", SecType: "STK"}, 206 | Execution: &Execution{AcctNumber: "123"}, 207 | Time: testTime, 208 | }, 209 | filter: func() *ExecutionFilter { 210 | f := NewExecutionFilter() 211 | f.Time = "20240102-16:04:05" 212 | return f 213 | }(), 214 | want: false, 215 | }, 216 | } 217 | 218 | for _, tt := range tests { 219 | t.Run(tt.name, func(t *testing.T) { 220 | if got := tt.fill.Matches(tt.filter); got != tt.want { 221 | t.Errorf("Fill.Matches() = %v, want %v", got, tt.want) 222 | } 223 | }) 224 | } 225 | } 226 | 227 | func TestNewTrade(t *testing.T) { 228 | contract := &Contract{Symbol: "AAPL"} 229 | order := &Order{OrderID: 123} 230 | customStatus := OrderStatus{ 231 | OrderID: 123, 232 | Status: Submitted, 233 | } 234 | 235 | tests := []struct { 236 | name string 237 | contract *Contract 238 | order *Order 239 | status []OrderStatus 240 | wantErr bool 241 | }{ 242 | { 243 | name: "basic creation", 244 | contract: contract, 245 | order: order, 246 | status: nil, 247 | wantErr: false, 248 | }, 249 | { 250 | name: "with custom status", 251 | contract: contract, 252 | order: order, 253 | status: []OrderStatus{customStatus}, 254 | wantErr: false, 255 | }, 256 | } 257 | 258 | for _, tt := range tests { 259 | t.Run(tt.name, func(t *testing.T) { 260 | trade := NewTrade(tt.contract, tt.order, tt.status...) 261 | if (trade == nil) != tt.wantErr { 262 | t.Errorf("NewTrade() error = %v, wantErr %v", trade == nil, tt.wantErr) 263 | return 264 | } 265 | if trade != nil { 266 | if trade.Contract != tt.contract { 267 | t.Errorf("NewTrade() contract = %v, want %v", trade.Contract, tt.contract) 268 | } 269 | if trade.Order != tt.order { 270 | t.Errorf("NewTrade() order = %v, want %v", trade.Order, tt.order) 271 | } 272 | } 273 | }) 274 | } 275 | } 276 | 277 | func TestTrade_Fills(t *testing.T) { 278 | trade := NewTrade(&Contract{}, &Order{}) 279 | fill1 := &Fill{Time: time.Now()} 280 | fill2 := &Fill{Time: time.Now()} 281 | 282 | trade.addFill(fill1) 283 | trade.addFill(fill2) 284 | 285 | fills := trade.Fills() 286 | if len(fills) != 2 { 287 | t.Errorf("Trade.Fills() len = %v, want %v", len(fills), 2) 288 | } 289 | 290 | // Verify that modifying returned fills doesn't affect original 291 | fills[0] = &Fill{} 292 | if reflect.DeepEqual(fills[0], trade.fills[0]) { 293 | t.Error("Trade.Fills() returned slice should be a copy") 294 | } 295 | } 296 | 297 | func TestTrade_Logs(t *testing.T) { 298 | trade := NewTrade(&Contract{}, &Order{}) 299 | 300 | // Verify initial log entry 301 | logs := trade.Logs() 302 | if len(logs) != 1 { 303 | t.Errorf("New trade should have 1 initial log entry, got %d", len(logs)) 304 | } 305 | if logs[0].Status != PendingSubmit { 306 | t.Errorf("Initial log status = %v, want %v", logs[0].Status, PendingSubmit) 307 | } 308 | 309 | // Add new log entry 310 | newEntry := TradeLogEntry{ 311 | Time: time.Now(), 312 | Status: Submitted, 313 | Message: "Test message", 314 | } 315 | trade.addLog(newEntry) 316 | 317 | // Verify log was added 318 | logs = trade.Logs() 319 | if len(logs) != 2 { 320 | t.Errorf("Trade.Logs() len = %v, want %v", len(logs), 2) 321 | } 322 | if !reflect.DeepEqual(logs[1], newEntry) { 323 | t.Errorf("Trade.Logs()[1] = %v, want %v", logs[1], newEntry) 324 | } 325 | } 326 | 327 | func TestTrade_IsActive(t *testing.T) { 328 | tests := []struct { 329 | name string 330 | status Status 331 | want bool 332 | }{ 333 | {"active status", PendingSubmit, true}, 334 | {"inactive status", Cancelled, false}, 335 | {"done status", Filled, false}, 336 | } 337 | 338 | for _, tt := range tests { 339 | t.Run(tt.name, func(t *testing.T) { 340 | trade := NewTrade(&Contract{}, &Order{}, OrderStatus{Status: tt.status}) 341 | if got := trade.IsActive(); got != tt.want { 342 | t.Errorf("Trade.IsActive() = %v, want %v", got, tt.want) 343 | } 344 | }) 345 | } 346 | } 347 | 348 | func TestTrade_IsDone(t *testing.T) { 349 | tests := []struct { 350 | name string 351 | status Status 352 | want bool 353 | }{ 354 | {"done status", Filled, true}, 355 | {"active status", PendingSubmit, false}, 356 | {"inactive status", Inactive, false}, 357 | } 358 | 359 | for _, tt := range tests { 360 | t.Run(tt.name, func(t *testing.T) { 361 | trade := NewTrade(&Contract{}, &Order{}, OrderStatus{Status: tt.status}) 362 | if got := trade.IsDone(); got != tt.want { 363 | t.Errorf("Trade.IsDone() = %v, want %v", got, tt.want) 364 | } 365 | }) 366 | } 367 | } 368 | 369 | func TestTrade_Done(t *testing.T) { 370 | trade := NewTrade(&Contract{}, &Order{}) 371 | 372 | // Test initial state 373 | select { 374 | case <-trade.Done(): 375 | t.Error("Done channel should not be closed initially") 376 | default: 377 | // Expected behavior 378 | } 379 | 380 | // Mark as done 381 | trade.markDoneSafe() 382 | 383 | // Verify channel is closed 384 | select { 385 | case <-trade.Done(): 386 | // Expected behavior 387 | default: 388 | t.Error("Done channel should be closed after markDoneSafe()") 389 | } 390 | 391 | // Verify multiple markDone calls don't panic 392 | trade.markDoneSafe() 393 | } 394 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | "unicode" 11 | ) 12 | 13 | // TickTypesToString converts a variadic number of TickType values to a comma-separated string representation. 14 | func TickTypesToString(tt ...TickType) string { 15 | var strs []string 16 | for _, t := range tt { 17 | strs = append(strs, fmt.Sprintf("%d", t)) 18 | } 19 | return strings.Join(strs, ",") 20 | } 21 | 22 | // isDigit checks if the provided string consists only of digits. 23 | func isDigit(s string) bool { 24 | for _, r := range s { 25 | if !unicode.IsDigit(r) { 26 | return false 27 | } 28 | } 29 | return true 30 | } 31 | 32 | // FormatIBTime formats a time.Time value into the string format required by IB API. 33 | // 34 | // The returned string is in the format "YYYYMMDD HH:MM:SS", which is used by Interactive Brokers for specifying date and time. 35 | // As no time zone is provided IB will use your local time zone so we enforce local time zone first. 36 | // It returns "" if time is zero. 37 | func FormatIBTime(t time.Time) string { 38 | if t.IsZero() { 39 | return "" 40 | } 41 | return t.In(time.Local).Format("20060102 15:04:05") 42 | } 43 | 44 | // FormatIBTimeUTC sets a time.Time value to UTC time zone and formats into the string format required by IB API. 45 | // 46 | // Note that there is a dash between the date and time in UTC notation. 47 | // The returned string is in the format "YYYYMMDD HH:MM:SS UTC", which is used by Interactive Brokers for specifying date and time. 48 | // It returns "" if time is zero. 49 | func FormatIBTimeUTC(t time.Time) string { 50 | if t.IsZero() { 51 | return "" 52 | } 53 | return t.UTC().Format("20060102-15:04:05") + " UTC" 54 | } 55 | 56 | // FormatIBTimeUSEastern sets a time.Time value to US/Eastern time zone and formats into the string format required by IB API. 57 | // 58 | // The returned string is in the format "YYYYMMDD HH:MM:SS US/Eastern", which is used by Interactive Brokers for specifying date and time. 59 | // It returns "" if time is zero. 60 | func FormatIBTimeUSEastern(t time.Time) string { 61 | if t.IsZero() { 62 | return "" 63 | } 64 | loc, _ := time.LoadLocation("America/New_York") 65 | return t.In(loc).Format("20060102 15:04:05") + " US/Eastern" 66 | } 67 | 68 | // ParseIBTime parses an IB string representation of time into a time.Time object. 69 | // It supports various IB formats, including: 70 | // 1. "YYYYMMDD" 71 | // 2. Unix timestamp ("1617206400") 72 | // 3. "YYYYMMDD HH:MM:SS Timezone" 73 | // 4. // "YYYYmmdd HH:MM:SS", "YYYY-mm-dd HH:MM:SS.0" or "YYYYmmdd-HH:MM:SS" 74 | func ParseIBTime(s string) (time.Time, error) { 75 | var layout string 76 | // "YYYYMMDD" 77 | if len(s) == 8 { 78 | layout = "20060102" 79 | return time.Parse(layout, s) 80 | } 81 | // "1617206400" 82 | if isDigit(s) { 83 | ts, err := strconv.ParseInt(s, 10, 64) 84 | if err != nil { 85 | return time.Time{}, err 86 | } 87 | return time.Unix(ts, 0), nil 88 | } 89 | // "20221125 10:00:00 Europe/Amsterdam" 90 | if strings.Count(s, " ") >= 2 && !strings.Contains(s, " ") { 91 | split := strings.Split(s, " ") 92 | layout = "20060102 15:04:05" 93 | t, err := time.Parse(layout, split[0]+" "+split[1]) 94 | if err != nil { 95 | return time.Time{}, err 96 | } 97 | loc, err := time.LoadLocation(split[2]) 98 | if err != nil { 99 | return time.Time{}, err 100 | } 101 | return t.In(loc), nil 102 | } 103 | // "YYYYmmdd HH:MM:SS", "YYYY-mm-dd HH:MM:SS.0" or "YYYYmmdd-HH:MM:SS" 104 | s = strings.ReplaceAll(s, "-", "") 105 | s = strings.ReplaceAll(s, " ", "") 106 | s = strings.ReplaceAll(s, " ", "") 107 | if len(s) > 15 { 108 | s = s[:16] 109 | } 110 | layout = "2006010215:04:05" 111 | return time.Parse(layout, s) 112 | } 113 | 114 | // LastWednesday12EST returns time.Time correspondong to last wednesday 12:00 EST 115 | // Without checking holidays this a high probability time for open market. Usefull for testing historical data 116 | func LastWednesday12EST() time.Time { 117 | // last Wednesday 118 | offset := (int(time.Now().Weekday()) - int(time.Wednesday) + 7) % 7 119 | if offset == 0 { 120 | offset = 7 121 | } 122 | lw := time.Now().AddDate(0, 0, -offset) 123 | EST, _ := time.LoadLocation("America/New_York") 124 | return time.Date(lw.Year(), lw.Month(), lw.Day(), 12, 0, 0, 0, EST) 125 | } 126 | 127 | // UpdateStruct copies non-zero fields from src to dest. 128 | // dest must be a pointer to a struct so that it can be updated; 129 | // src can be either a struct or a pointer to a struct. 130 | func UpdateStruct(dest, src any) error { 131 | destVal := reflect.ValueOf(dest) 132 | srcVal := reflect.Indirect(reflect.ValueOf(src)) // Handles both struct and *struct for src 133 | 134 | // Ensure that dest is a pointer to a struct 135 | if destVal.Kind() != reflect.Ptr || destVal.Elem().Kind() != reflect.Struct { 136 | return errors.New("dest must be a pointer to a struct") 137 | } 138 | // Ensure src is a struct (after dereferencing if it's a pointer) 139 | if srcVal.Kind() != reflect.Struct { 140 | return errors.New("src must be a struct or a pointer to a struct") 141 | } 142 | 143 | destVal = destVal.Elem() // Dereference dest to the actual struct 144 | srcType := srcVal.Type() 145 | 146 | // Iterate over each field in src struct 147 | for i := 0; i < srcVal.NumField(); i++ { 148 | srcField := srcVal.Field(i) 149 | fieldName := srcType.Field(i).Name 150 | destField := destVal.FieldByName(fieldName) 151 | 152 | // Update if field exists in dest and src field is non-zero 153 | if destField.IsValid() && destField.CanSet() && !srcField.IsZero() { 154 | destField.Set(srcField) 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | // Stringify converts a struct or pointer to a struct into a string representation 161 | // Skipping fields with zero or nil values and dereferencing pointers. 162 | // It is recursive and will apply to nested structs. 163 | // It can NOT handle unexported fields. 164 | func Stringify(obj interface{}) string { 165 | return stringifyValue(reflect.ValueOf(obj)) 166 | } 167 | 168 | // isEmptyValue checks if a value is considered "empty" 169 | func isEmptyValue(v reflect.Value) bool { 170 | // Handle nil pointers 171 | if v.Kind() == reflect.Ptr { 172 | return v.IsNil() 173 | } 174 | 175 | // Dereference pointer if needed 176 | if v.Kind() == reflect.Ptr { 177 | v = v.Elem() 178 | } 179 | 180 | switch v.Kind() { 181 | case reflect.Struct: 182 | // Special handling for Time struct 183 | if v.Type().String() == "time.Time" { 184 | return v.Interface().(time.Time).IsZero() 185 | } 186 | 187 | // Check if all fields are empty 188 | for i := 0; i < v.NumField(); i++ { 189 | if !isEmptyValue(v.Field(i)) { 190 | return false 191 | } 192 | } 193 | return true 194 | 195 | case reflect.Slice, reflect.Map: 196 | return v.Len() == 0 197 | 198 | case reflect.String: 199 | return v.String() == "" 200 | 201 | case reflect.Bool: 202 | return !v.Bool() 203 | 204 | case reflect.Int, reflect.Int32, reflect.Int64: 205 | return v.Int() == 0 || v.Int() == UNSET_INT || v.Int() == UNSET_LONG 206 | 207 | case reflect.Float32, reflect.Float64: 208 | return v.Float() == 0 || v.Float() == UNSET_FLOAT 209 | 210 | case reflect.Ptr: 211 | return v.IsNil() 212 | 213 | case reflect.Interface: 214 | return v.IsNil() 215 | } 216 | 217 | return false 218 | } 219 | 220 | // stringifyValue handles the recursive stringification of values 221 | func stringifyValue(v reflect.Value) string { 222 | // Handle pointer types by dereferencing 223 | if v.Kind() == reflect.Ptr { 224 | // If nil, return empty string 225 | if v.IsNil() { 226 | return "" 227 | } 228 | v = v.Elem() 229 | } 230 | 231 | switch v.Kind() { 232 | case reflect.Struct: 233 | return stringifyStruct(v) 234 | case reflect.Slice: 235 | return stringifySlice(v) 236 | case reflect.Map: 237 | return stringifyMap(v) 238 | default: 239 | return fmt.Sprintf("%v", v.Interface()) 240 | } 241 | } 242 | 243 | // stringifyStruct handles struct-specific stringification 244 | func stringifyStruct(v reflect.Value) string { 245 | // Get the type of the struct 246 | t := v.Type() 247 | 248 | // Skip completely empty structs 249 | if isEmptyValue(v) { 250 | return "" 251 | } 252 | 253 | // Build the string representation 254 | var fields []string 255 | for i := 0; i < v.NumField(); i++ { 256 | field := v.Field(i) 257 | fieldName := t.Field(i).Name 258 | 259 | // Skip unexported (private) fields 260 | if !field.CanInterface() { 261 | continue 262 | } 263 | 264 | // Skip empty values 265 | if isEmptyValue(field) { 266 | continue 267 | } 268 | 269 | // Recursively get the stringified value 270 | fieldValueStr := stringifyValue(field) 271 | 272 | // Add to fields if not empty 273 | if fieldValueStr != "" { 274 | fields = append(fields, fmt.Sprintf("%s=%s", fieldName, fieldValueStr)) 275 | } 276 | } 277 | 278 | // Construct the final string 279 | if len(fields) == 0 { 280 | return "" 281 | } 282 | return fmt.Sprintf("%s{%s}", t.Name(), strings.Join(fields, ", ")) 283 | } 284 | 285 | // stringifySlice handles slice stringification 286 | func stringifySlice(v reflect.Value) string { 287 | // If slice is empty or nil 288 | if v.Len() == 0 { 289 | return "" 290 | } 291 | 292 | // Convert slice elements to strings 293 | var elements []string 294 | for i := 0; i < v.Len(); i++ { 295 | elem := v.Index(i) 296 | 297 | // Skip empty values 298 | if isEmptyValue(elem) { 299 | continue 300 | } 301 | 302 | elemStr := stringifyValue(elem) 303 | if elemStr != "" { 304 | elements = append(elements, elemStr) 305 | } 306 | } 307 | 308 | // If no non-zero elements, return empty 309 | if len(elements) == 0 { 310 | return "" 311 | } 312 | 313 | return fmt.Sprintf("[%s]", strings.Join(elements, ", ")) 314 | } 315 | 316 | // stringifyMap handles map stringification 317 | func stringifyMap(v reflect.Value) string { 318 | // If map is empty or nil 319 | if v.Len() == 0 { 320 | return "" 321 | } 322 | 323 | // Convert map elements to strings 324 | var elements []string 325 | iter := v.MapRange() 326 | for iter.Next() { 327 | k := iter.Key() 328 | val := iter.Value() 329 | 330 | // Skip empty values 331 | if isEmptyValue(val) { 332 | continue 333 | } 334 | 335 | valStr := stringifyValue(val) 336 | if valStr != "" { 337 | elements = append(elements, fmt.Sprintf("%v=%s", k.Interface(), valStr)) 338 | } 339 | } 340 | 341 | // If no non-zero elements, return empty 342 | if len(elements) == 0 { 343 | return "" 344 | } 345 | 346 | return fmt.Sprintf("map[%s]", strings.Join(elements, ", ")) 347 | } 348 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package ibsync 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestIsDigit(t *testing.T) { 10 | tests := []struct { 11 | input string 12 | expected bool 13 | }{ 14 | { 15 | input: "123456", // All digits 16 | expected: true, 17 | }, 18 | { 19 | input: "001234", // Leading zeros, all digits 20 | expected: true, 21 | }, 22 | { 23 | input: "123a456", // Contains a non-digit character 24 | expected: false, 25 | }, 26 | { 27 | input: "abc", // All non-digit characters 28 | expected: false, 29 | }, 30 | { 31 | input: "", // Empty string 32 | expected: true, // Edge case, no characters means no non-digits 33 | }, 34 | { 35 | input: " ", // Spaces are not digits 36 | expected: false, 37 | }, 38 | { 39 | input: "123 456", // Contains a space 40 | expected: false, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.input, func(t *testing.T) { 46 | result := isDigit(tt.input) 47 | if result != tt.expected { 48 | t.Errorf("For input '%s': expected %v, got %v", tt.input, tt.expected, result) 49 | } 50 | }) 51 | } 52 | } 53 | 54 | func TestFormatIBTime(t *testing.T) { 55 | tests := []struct { 56 | name string 57 | input time.Time 58 | expected string 59 | }{ 60 | { 61 | name: "zero time", 62 | input: time.Time{}, 63 | expected: "", 64 | }, 65 | { 66 | name: "typical local date time", 67 | input: time.Date(2024, 3, 15, 14, 30, 45, 0, time.Local), 68 | expected: "20240315 14:30:45", 69 | }, 70 | { 71 | name: "UTC time with nanoseconds", 72 | input: time.Date(2024, 3, 15, 14, 30, 45, 123456789, time.UTC), 73 | expected: time.Date(2024, 3, 15, 14, 30, 45, 123456789, time.UTC).In(time.Local).Format("20060102 15:04:05"), 74 | }, 75 | } 76 | 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | got := FormatIBTime(tt.input) 80 | if got != tt.expected { 81 | t.Errorf("FormatIBTime() = %v, want %v", got, tt.expected) 82 | } 83 | }) 84 | } 85 | } 86 | 87 | func TestFormatIBTimeUTC(t *testing.T) { 88 | est, _ := time.LoadLocation("America/New_York") 89 | tests := []struct { 90 | name string 91 | input time.Time 92 | expected string 93 | }{ 94 | { 95 | name: "zero time", 96 | input: time.Time{}, 97 | expected: "", 98 | }, 99 | { 100 | name: "typical date time", 101 | input: time.Date(2024, 3, 15, 14, 30, 45, 0, time.UTC), 102 | expected: "20240315-14:30:45 UTC", 103 | }, 104 | { 105 | name: "convert from different timezone", 106 | input: time.Date(2024, 3, 15, 14, 30, 45, 0, time.UTC).In(est), 107 | expected: "20240315-14:30:45 UTC", // Should convert back to UTC 108 | }, 109 | { 110 | name: "midnight UTC", 111 | input: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), 112 | expected: "20240101-00:00:00 UTC", 113 | }, 114 | { 115 | name: "with nanoseconds", 116 | input: time.Date(2024, 1, 1, 12, 0, 0, 123456789, time.UTC), 117 | expected: "20240101-12:00:00 UTC", 118 | }, 119 | } 120 | 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | got := FormatIBTimeUTC(tt.input) 124 | if got != tt.expected { 125 | t.Errorf("FormatIBTimeUTC() = %v, want %v", got, tt.expected) 126 | } 127 | }) 128 | } 129 | } 130 | 131 | func TestFormatIBTimeUSEastern(t *testing.T) { 132 | EST, _ := time.LoadLocation("America/New_York") 133 | tests := []struct { 134 | name string 135 | input time.Time 136 | expected string 137 | }{ 138 | { 139 | name: "zero time", 140 | input: time.Time{}, 141 | expected: "", 142 | }, 143 | { 144 | name: "typical date time", 145 | input: time.Date(2024, 3, 15, 14, 30, 45, 0, EST), 146 | expected: "20240315 14:30:45 US/Eastern", // UTC-4 during DST 147 | }, 148 | { 149 | name: "UTC time conversion", 150 | input: time.Date(2024, 3, 15, 14, 30, 45, 0, time.UTC), 151 | expected: "20240315 10:30:45 US/Eastern", // UTC-4 during DST 152 | }, 153 | { 154 | name: "during EST (non-DST)", 155 | input: time.Date(2024, 1, 15, 14, 30, 45, 0, time.UTC), 156 | expected: "20240115 09:30:45 US/Eastern", // UTC-5 during EST 157 | }, 158 | { 159 | name: "DST transition spring forward", 160 | input: time.Date(2024, 3, 10, 14, 30, 45, 0, time.UTC), 161 | expected: "20240310 10:30:45 US/Eastern", 162 | }, 163 | { 164 | name: "DST transition fall back", 165 | input: time.Date(2024, 11, 3, 14, 30, 45, 0, time.UTC), 166 | expected: "20241103 09:30:45 US/Eastern", 167 | }, 168 | } 169 | for _, tt := range tests { 170 | t.Run(tt.name, func(t *testing.T) { 171 | got := FormatIBTimeUSEastern(tt.input) 172 | if got != tt.expected { 173 | t.Errorf("FormatIBTimeUSEastern() = %v, want %v", got, tt.expected) 174 | } 175 | }) 176 | } 177 | } 178 | 179 | func TestParseIBTime(t *testing.T) { 180 | tests := []struct { 181 | input string 182 | expected time.Time 183 | hasError bool 184 | }{ 185 | { 186 | input: "20231016", // YYYYMMDD 187 | expected: time.Date(2023, 10, 16, 0, 0, 0, 0, time.UTC), 188 | hasError: false, 189 | }, 190 | { 191 | input: "1617206400", // Unix timestamp 192 | expected: time.Unix(1617206400, 0), 193 | hasError: false, 194 | }, 195 | { 196 | input: "20221125 10:00:00 Europe/Amsterdam", // DateTime with timezone 197 | expected: time.Date(2022, 11, 25, 10, 0, 0, 0, time.FixedZone("CET", 0)), // Adjust to CET timezone 198 | hasError: false, 199 | }, 200 | { 201 | input: "2023-10-16 10:00:00", // YYYY-mm-dd HH:MM:SS 202 | expected: time.Date(2023, 10, 16, 10, 0, 0, 0, time.UTC), 203 | hasError: false, 204 | }, 205 | { 206 | input: "2023-10-16 10:00:00.0", // YYYY-mm-dd HH:MM:SS.0 207 | expected: time.Date(2023, 10, 16, 10, 0, 0, 0, time.UTC), 208 | hasError: false, 209 | }, 210 | { 211 | input: "20231016-10:00:00", // YYYY-mm-dd-HH:MM:SS 212 | expected: time.Date(2023, 10, 16, 10, 0, 0, 0, time.UTC), 213 | hasError: false, 214 | }, 215 | { 216 | input: "invalid-string", // Invalid format 217 | expected: time.Time{}, 218 | hasError: true, 219 | }, 220 | } 221 | 222 | for _, tt := range tests { 223 | t.Run(tt.input, func(t *testing.T) { 224 | result, err := ParseIBTime(tt.input) 225 | if tt.hasError { 226 | if err == nil { 227 | t.Errorf("expected an error but got none for input: %s", tt.input) 228 | } 229 | } else { 230 | if err != nil { 231 | t.Errorf("did not expect an error but got: %v for input: %s", err, tt.input) 232 | } 233 | if !result.Equal(tt.expected) { 234 | t.Errorf("expected: %v, got: %v for input: %s", tt.expected, result, tt.input) 235 | } 236 | } 237 | }) 238 | } 239 | } 240 | 241 | // Example struct for testing 242 | type Example struct { 243 | Name string 244 | Age int 245 | Address string 246 | Active bool 247 | } 248 | 249 | // Test function to test the UpdateStruct behavior 250 | func TestUpdateStruct(t *testing.T) { 251 | tests := []struct { 252 | name string 253 | dest Example 254 | src Example 255 | expected Example 256 | }{ 257 | { 258 | name: "Non-zero fields in src update dest", 259 | dest: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: false}, 260 | src: Example{Name: "Bob", Age: 30}, // Only Name and Age should update 261 | expected: Example{Name: "Bob", Age: 30, Address: "Old Address", Active: false}, 262 | }, 263 | { 264 | name: "Zero fields in src do not update dest", 265 | dest: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: true}, 266 | src: Example{Address: ""}, // Empty string should not override 267 | expected: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: true}, 268 | }, 269 | { 270 | name: "Empty src does not change dest", 271 | dest: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: true}, 272 | src: Example{}, // No fields to update 273 | expected: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: true}, 274 | }, 275 | { 276 | name: "Update boolean field in dest", 277 | dest: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: false}, 278 | src: Example{Active: true}, // Only Active should update 279 | expected: Example{Name: "Alice", Age: 25, Address: "Old Address", Active: true}, 280 | }, 281 | } 282 | 283 | for _, tt := range tests { 284 | t.Run(tt.name, func(t *testing.T) { 285 | // Make a copy of dest to pass as a pointer 286 | dest := tt.dest 287 | 288 | // Call UpdateStruct with a pointer to dest and src 289 | if err := UpdateStruct(&dest, tt.src); err != nil { 290 | t.Fatalf("UpdateStruct failed: %v", err) 291 | } 292 | 293 | // Check if dest matches the expected result 294 | if !reflect.DeepEqual(dest, tt.expected) { 295 | t.Errorf("UpdateStruct() = %v, want %v", dest, tt.expected) 296 | } 297 | }) 298 | } 299 | } 300 | 301 | // Test for handling incorrect types in dest or src 302 | func TestUpdateStructInvalidTypes(t *testing.T) { 303 | var dest Example 304 | src := Example{Name: "Bob"} 305 | 306 | // Non-pointer dest should return an error 307 | if err := UpdateStruct(dest, src); err == nil { 308 | t.Errorf("Expected error for non-pointer dest, got nil") 309 | } 310 | 311 | // dest as a pointer but src as a non-struct should return an error 312 | var nonStructSrc int 313 | if err := UpdateStruct(&dest, nonStructSrc); err == nil { 314 | t.Errorf("Expected error for non-struct src, got nil") 315 | } 316 | } 317 | --------------------------------------------------------------------------------