├── .gitignore ├── README.md ├── connect.go ├── connet_test.go ├── errors.go ├── example ├── example.go └── websocket │ └── example.go ├── funds.go ├── funds_test.go ├── go.mod ├── go.sum ├── http.go ├── market.go ├── market_test.go ├── mock_responses ├── holdings.json ├── logout.json ├── ltp.json ├── order_response.json ├── orders.json ├── position_conversion.json ├── positions.json ├── profile.json ├── rms.json ├── session.json └── trades.json ├── orders.go ├── orders_test.go ├── portfolio.go ├── portfolio_test.go ├── user.go ├── user_test.go ├── utils.go └── websocket └── websocket.go /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Smart API Go client 2 | 3 | The official Go client for communicating with the Angel Broking Smart APIs. 4 | 5 | SmartAPI is a set of REST-like APIs that expose many capabilities required to build a complete investment and trading platform. Execute orders in real time, manage user portfolio, stream live market data (WebSockets), and more, with the simple HTTP API collection. 6 | 7 | 8 | ## Installation 9 | ``` 10 | go get github.com/angel-one/smartapigo 11 | ``` 12 | ## API usage 13 | ```golang 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | SmartApi "github.com/angel-one/smartapigo" 19 | ) 20 | 21 | func main() { 22 | 23 | // Create New Angel Broking Client 24 | ABClient := SmartApi.New("ClientCode", "Password","API Key") 25 | 26 | fmt.Println("Client :- ",ABClient) 27 | 28 | // User Login and Generate User Session 29 | session, err := ABClient.GenerateSession("totp here") 30 | 31 | if err != nil { 32 | fmt.Println(err.Error()) 33 | return 34 | } 35 | 36 | //Renew User Tokens using refresh token 37 | session.UserSessionTokens, err = ABClient.RenewAccessToken(session.RefreshToken) 38 | 39 | if err != nil { 40 | fmt.Println(err.Error()) 41 | return 42 | } 43 | 44 | fmt.Println("User Session Tokens :- ", session.UserSessionTokens) 45 | 46 | //Get User Profile 47 | session.UserProfile, err = ABClient.GetUserProfile() 48 | 49 | if err != nil { 50 | fmt.Println(err.Error()) 51 | return 52 | } 53 | 54 | fmt.Println("User Profile :- ", session.UserProfile) 55 | fmt.Println("User Session Object :- ", session) 56 | 57 | //Place Order 58 | order, err := ABClient.PlaceOrder(SmartApi.OrderParams{Variety: "NORMAL", TradingSymbol: "SBIN-EQ", SymbolToken: "3045", TransactionType: "BUY", Exchange: "NSE", OrderType: "LIMIT", ProductType: "INTRADAY", Duration: "DAY", Price: "19500", SquareOff: "0", StopLoss: "0", Quantity: "1"}) 59 | 60 | if err != nil { 61 | fmt.Println(err.Error()) 62 | return 63 | } 64 | 65 | fmt.Println("Placed Order ID and Script :- ", order) 66 | } 67 | ``` 68 | ## Websocket Data Streaming 69 | ```golang 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | SmartApi "github.com/angel-one/smartapigo" 75 | "github.com/angel-one/smartapigo/websocket" 76 | "time" 77 | ) 78 | 79 | var socketClient *websocket.SocketClient 80 | 81 | // Triggered when any error is raised 82 | func onError(err error) { 83 | fmt.Println("Error: ", err) 84 | } 85 | 86 | // Triggered when websocket connection is closed 87 | func onClose(code int, reason string) { 88 | fmt.Println("Close: ", code, reason) 89 | } 90 | 91 | // Triggered when connection is established and ready to send and accept data 92 | func onConnect() { 93 | fmt.Println("Connected") 94 | err := socketClient.Subscribe() 95 | if err != nil { 96 | fmt.Println("err: ", err) 97 | } 98 | } 99 | 100 | // Triggered when a message is received 101 | func onMessage(message []map[string]interface{}) { 102 | fmt.Printf("Message Received :- %v\n",message) 103 | } 104 | 105 | // Triggered when reconnection is attempted which is enabled by default 106 | func onReconnect(attempt int, delay time.Duration) { 107 | fmt.Printf("Reconnect attempt %d in %fs\n", attempt, delay.Seconds()) 108 | } 109 | 110 | // Triggered when maximum number of reconnect attempt is made and the program is terminated 111 | func onNoReconnect(attempt int) { 112 | fmt.Printf("Maximum no of reconnect attempt reached: %d\n", attempt) 113 | } 114 | 115 | func main() { 116 | 117 | // Create New Angel Broking Client 118 | ABClient := SmartApi.New("ClientCode", "Password","API Key") 119 | 120 | // User Login and Generate User Session 121 | session, err := ABClient.GenerateSession() 122 | 123 | if err != nil { 124 | fmt.Println(err.Error()) 125 | return 126 | } 127 | 128 | //Get User Profile 129 | session.UserProfile, err = ABClient.GetUserProfile() 130 | 131 | if err != nil { 132 | fmt.Println(err.Error()) 133 | return 134 | } 135 | 136 | // New Websocket Client 137 | socketClient = websocket.New(session.ClientCode,session.FeedToken,"nse_cm|17963&nse_cm|3499&nse_cm|11536&nse_cm|21808&nse_cm|317") 138 | 139 | // Assign callbacks 140 | socketClient.OnError(onError) 141 | socketClient.OnClose(onClose) 142 | socketClient.OnMessage(onMessage) 143 | socketClient.OnConnect(onConnect) 144 | socketClient.OnReconnect(onReconnect) 145 | socketClient.OnNoReconnect(onNoReconnect) 146 | 147 | // Start Consuming Data 148 | socketClient.Serve() 149 | 150 | } 151 | ``` 152 | 153 | ## Examples 154 | Check example folder for more examples. 155 | 156 | You can run the following after updating the Credentials in the examples: 157 | ``` 158 | go run example/example.go 159 | ``` 160 | For websocket example 161 | ``` 162 | go run example/websocket/example.go 163 | ``` 164 | 165 | ## Run unit tests 166 | 167 | ``` 168 | go test -v 169 | ``` 170 | -------------------------------------------------------------------------------- /connect.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "crypto/tls" 5 | _ "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // Client represents interface for Kite Connect client. 11 | type Client struct { 12 | clientCode string 13 | password string 14 | accessToken string 15 | debug bool 16 | baseURI string 17 | apiKey string 18 | httpClient HTTPClient 19 | } 20 | 21 | const ( 22 | name string = "smartapi-go" 23 | requestTimeout time.Duration = 7000 * time.Millisecond 24 | baseURI string = "https://apiconnect.angelbroking.com/" 25 | ) 26 | 27 | // New creates a new Smart API client. 28 | func New(clientCode string,password string,apiKey string) *Client { 29 | client := &Client{ 30 | clientCode: clientCode, 31 | password: password, 32 | apiKey: apiKey, 33 | baseURI: baseURI, 34 | } 35 | 36 | // Create a default http handler with default timeout. 37 | client.SetHTTPClient(&http.Client{ 38 | Timeout: requestTimeout, 39 | Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify : true}}, 40 | }) 41 | 42 | return client 43 | } 44 | 45 | // SetHTTPClient overrides default http handler with a custom one. 46 | // This can be used to set custom timeouts and transport. 47 | func (c *Client) SetHTTPClient(h *http.Client) { 48 | c.httpClient = NewHTTPClient(h, nil, c.debug) 49 | } 50 | 51 | // SetDebug sets debug mode to enable HTTP logs. 52 | func (c *Client) SetDebug(debug bool) { 53 | c.debug = debug 54 | c.httpClient.GetClient().debug = debug 55 | } 56 | 57 | // SetBaseURI overrides the base SmartAPI endpoint with custom url. 58 | func (c *Client) SetBaseURI(baseURI string) { 59 | c.baseURI = baseURI 60 | } 61 | 62 | // SetTimeout sets request timeout for default http client. 63 | func (c *Client) SetTimeout(timeout time.Duration) { 64 | hClient := c.httpClient.GetClient().client 65 | hClient.Timeout = timeout 66 | } 67 | 68 | // SetAccessToken sets the access token to the Kite Connect instance. 69 | func (c *Client) SetAccessToken(accessToken string) { 70 | c.accessToken = accessToken 71 | } 72 | 73 | func (c *Client) doEnvelope(method, uri string, params map[string]interface{}, headers http.Header, v interface{}, authorization ...bool) error { 74 | if params == nil { 75 | params = map[string]interface{}{} 76 | } 77 | 78 | // Send custom headers set 79 | if headers == nil { 80 | headers = map[string][]string{} 81 | } 82 | 83 | localIp,publicIp,mac,err := getIpAndMac() 84 | 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // Add Kite Connect version to header 90 | headers.Add("Content-Type", "application/json") 91 | headers.Add("X-ClientLocalIP", localIp) 92 | headers.Add("X-ClientPublicIP", publicIp) 93 | headers.Add("X-MACAddress", mac) 94 | headers.Add("Accept", "application/json") 95 | headers.Add("X-UserType", "USER") 96 | headers.Add("X-SourceID", "WEB") 97 | headers.Add("X-PrivateKey",c.apiKey) 98 | if authorization != nil && authorization[0]{ 99 | headers.Add("Authorization","Bearer "+c.accessToken) 100 | } 101 | 102 | return c.httpClient.DoEnvelope(method, c.baseURI+uri, params, headers, v) 103 | } 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /connet_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/url" 7 | "path" 8 | "reflect" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | httpmock "github.com/jarcoal/httpmock" 15 | ) 16 | 17 | // Test New Smart API Connect instance 18 | func TestNewClient(t *testing.T) { 19 | t.Parallel() 20 | 21 | clientcode := "test" 22 | password := "test@444" 23 | apiKey := "test_key" 24 | client := New(clientcode,password,apiKey) 25 | 26 | if client.password != password || client.clientCode != clientcode { 27 | t.Errorf("Credentials not assigned properly.") 28 | } 29 | } 30 | 31 | // Test all client setters 32 | func TestClientSetters(t *testing.T) { 33 | t.Parallel() 34 | 35 | clientcode := "test" 36 | password := "test@444" 37 | apiKey := "test_key" 38 | client := New(clientcode,password,apiKey) 39 | 40 | customDebug := true 41 | customBaseURI := "test" 42 | customTimeout := 1000 * time.Millisecond 43 | customAccessToken := "accesstoken" 44 | customHTTPClientTimeout := time.Duration(2000) 45 | customHTTPClient := &http.Client{ 46 | Timeout: customHTTPClientTimeout, 47 | } 48 | 49 | // Check if default debug is false 50 | if client.debug != false || client.httpClient.GetClient().debug != false { 51 | t.Errorf("Default debug is not false.") 52 | } 53 | 54 | // Set custom debug 55 | client.SetDebug(customDebug) 56 | if client.debug != customDebug || client.httpClient.GetClient().debug != customDebug { 57 | t.Errorf("Debug is not set properly.") 58 | } 59 | 60 | // Test default base uri 61 | if client.baseURI != baseURI { 62 | t.Errorf("Default base URI is not set properly.") 63 | } 64 | 65 | // Set custom base URI 66 | client.SetBaseURI(customBaseURI) 67 | if client.baseURI != customBaseURI { 68 | t.Errorf("Base URI is not set properly.") 69 | } 70 | 71 | // Test default timeout 72 | if client.httpClient.GetClient().client.Timeout != requestTimeout { 73 | t.Errorf("Default request timeout is not set properly.") 74 | } 75 | 76 | // Set custom timeout for default http client 77 | client.SetTimeout(customTimeout) 78 | if client.httpClient.GetClient().client.Timeout != customTimeout { 79 | t.Errorf("HTTPClient timeout is not set properly.") 80 | } 81 | 82 | // Set access token 83 | client.SetAccessToken(customAccessToken) 84 | if client.accessToken != customAccessToken { 85 | t.Errorf("Access token is not set properly.") 86 | } 87 | 88 | // Set custom HTTP Client 89 | client.SetHTTPClient(customHTTPClient) 90 | if client.httpClient.GetClient().client != customHTTPClient { 91 | t.Errorf("Custom HTTPClient is not set properly.") 92 | } 93 | 94 | // Set timeout for custom http client 95 | if client.httpClient.GetClient().client.Timeout != customHTTPClientTimeout { 96 | t.Errorf("Custom HTTPClient timeout is not set properly.") 97 | } 98 | 99 | // Set custom timeout for custom http client 100 | client.SetTimeout(customTimeout) 101 | if client.httpClient.GetClient().client.Timeout != customTimeout { 102 | t.Errorf("HTTPClient timeout is not set properly.") 103 | } 104 | } 105 | 106 | // Following boiler plate is used to implement setup/teardown using Go subtests feature 107 | const mockBaseDir = "./mock_responses" 108 | 109 | var MockResponders = [][]string{ 110 | // Array of [, , ] 111 | 112 | // GET endpoints 113 | []string{http.MethodGet, URIUserProfile, "profile.json"}, 114 | []string{http.MethodGet, URIGetPositions, "positions.json"}, 115 | []string{http.MethodGet, URIGetHoldings, "holdings.json"}, 116 | []string{http.MethodGet, URIRMS, "rms.json"}, 117 | []string{http.MethodGet, URIGetTradeBook, "trades.json"}, 118 | []string{http.MethodGet, URIGetOrderBook, "orders.json"}, 119 | 120 | // POST endpoints 121 | []string{http.MethodPost, URIModifyOrder, "order_response.json"}, 122 | []string{http.MethodPost, URIPlaceOrder, "order_response.json"}, 123 | []string{http.MethodPost, URICancelOrder, "order_response.json"}, 124 | []string{http.MethodPost, URILTP, "ltp.json"}, 125 | []string{http.MethodPost, URILogin, "session.json"}, 126 | []string{http.MethodPost, URIUserSessionRenew, "session.json"}, 127 | []string{http.MethodPost, URIUserProfile, "profile.json"}, 128 | []string{http.MethodPost, URILogout, "logout.json"}, 129 | []string{http.MethodPost, URIConvertPosition, "position_conversion.json"}, 130 | 131 | } 132 | 133 | // Test only function prefix with this 134 | const suiteTestMethodPrefix = "Test" 135 | 136 | // TestSuite is an interface where you define suite and test case preparation and tear down logic. 137 | type TestSuite struct { 138 | TestConnect *Client 139 | } 140 | 141 | // Setup the API suit 142 | func (ts *TestSuite) SetupAPITestSuit() { 143 | 144 | clientcode := "test" 145 | password := "test@444" 146 | apiKey := "test_key" 147 | ts.TestConnect = New(clientcode,password,apiKey) 148 | httpmock.ActivateNonDefault(ts.TestConnect.httpClient.GetClient().client) 149 | 150 | for _, v := range MockResponders { 151 | httpMethod := v[0] 152 | route := v[1] 153 | filePath := v[2] 154 | 155 | resp, err := ioutil.ReadFile(path.Join(mockBaseDir, filePath)) 156 | if err != nil { 157 | panic("Error while reading mock response: " + filePath) 158 | } 159 | 160 | base, err := url.Parse(ts.TestConnect.baseURI) 161 | if err != nil { 162 | panic("Something went wrong") 163 | } 164 | // Replace all url variables with string "test" 165 | re := regexp.MustCompile("%s") 166 | formattedRoute := re.ReplaceAllString(route, "test") 167 | base.Path = path.Join(base.Path, formattedRoute) 168 | // fmt.Println(base.String()) 169 | // endpoint := path.Join(ts.KiteConnect.baseURI, route) 170 | httpmock.RegisterResponder(httpMethod, base.String(), httpmock.NewBytesResponder(200, resp)) 171 | 172 | } 173 | } 174 | 175 | // TearDown API suit 176 | func (ts *TestSuite) TearDownAPITestSuit() { 177 | //defer httpmock.DeactivateAndReset() 178 | } 179 | 180 | // Individual test setup 181 | func (ts *TestSuite) SetupAPITest() {} 182 | 183 | // Individual test teardown 184 | func (ts *TestSuite) TearDownAPITest() {} 185 | 186 | /* 187 | Run sets up the suite, runs its test cases and tears it down: 188 | 1. Calls `ts.SetUpSuite` 189 | 2. Seeks for any methods that have `Test` prefix, for each of them it: 190 | a. Calls `SetUp` 191 | b. Calls the test method itself 192 | c. Calls `TearDown` 193 | 3. Calls `ts.TearDownSuite` 194 | */ 195 | func RunAPITests(t *testing.T, ts *TestSuite) { 196 | ts.SetupAPITestSuit() 197 | 198 | suiteType := reflect.TypeOf(ts) 199 | for i := 0; i < suiteType.NumMethod(); i++ { 200 | m := suiteType.Method(i) 201 | if strings.HasPrefix(m.Name, suiteTestMethodPrefix) { 202 | t.Run(m.Name, func(t *testing.T) { 203 | ts.SetupAPITest() 204 | defer ts.TearDownAPITest() 205 | 206 | in := []reflect.Value{reflect.ValueOf(ts), reflect.ValueOf(t)} 207 | m.Func.Call(in) 208 | }) 209 | } 210 | } 211 | } 212 | 213 | func TestAPIMethods(t *testing.T) { 214 | s := &TestSuite{} 215 | RunAPITests(t, s) 216 | } 217 | 218 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | // Error is the error type used for all API errors. 4 | type Error struct { 5 | Code string 6 | Message string 7 | Data interface{} 8 | } 9 | 10 | // This makes Error a valid Go error type. 11 | func (e Error) Error() string { 12 | return e.Message 13 | } 14 | 15 | // NewError creates and returns a new instace of Error 16 | // with custom error metadata. 17 | func NewError(etype string, message string, data interface{}) error { 18 | err := Error{} 19 | err.Message = message 20 | err.Code = etype 21 | err.Data = data 22 | return err 23 | } 24 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | SmartApi "github.com/angel-one/smartapigo" 6 | ) 7 | 8 | func main() { 9 | 10 | // Create New Angel Broking Client 11 | ABClient := SmartApi.New("Your Client Code", "Your Password", "Your api key") 12 | 13 | fmt.Println("Client :- ", ABClient) 14 | 15 | // User Login and Generate User Session 16 | session, err := ABClient.GenerateSession("your totp here") 17 | 18 | if err != nil { 19 | fmt.Println(err.Error()) 20 | return 21 | } 22 | 23 | //Renew User Tokens using refresh token 24 | session.UserSessionTokens, err = ABClient.RenewAccessToken(session.RefreshToken) 25 | 26 | if err != nil { 27 | fmt.Println(err.Error()) 28 | return 29 | } 30 | 31 | fmt.Println("User Session Tokens :- ", session.UserSessionTokens) 32 | 33 | //Get User Profile 34 | session.UserProfile, err = ABClient.GetUserProfile() 35 | 36 | if err != nil { 37 | fmt.Println(err.Error()) 38 | return 39 | } 40 | 41 | fmt.Println("User Profile :- ", session.UserProfile) 42 | fmt.Println("User Session Object :- ", session) 43 | 44 | //Place Order 45 | order, err := ABClient.PlaceOrder(SmartApi.OrderParams{Variety: "NORMAL", TradingSymbol: "SBIN-EQ", SymbolToken: "3045", TransactionType: "BUY", Exchange: "NSE", OrderType: "LIMIT", ProductType: "INTRADAY", Duration: "DAY", Price: "19500", SquareOff: "0", StopLoss: "0", Quantity: "1"}) 46 | 47 | if err != nil { 48 | fmt.Println(err.Error()) 49 | return 50 | } 51 | 52 | fmt.Println("Placed Order ID and Script :- ", order) 53 | 54 | //Modify Order 55 | modifiedOrder, err := ABClient.ModifyOrder(SmartApi.ModifyOrderParams{Variety: "NORMAL", OrderID: order.OrderID, OrderType: "LIMIT", ProductType: "INTRADAY", Duration: "DAY", Price: "19400", Quantity: "1", TradingSymbol: "SBI-EQ", SymbolToken: "3045", Exchange: "NSE"}) 56 | 57 | if err != nil { 58 | fmt.Println(err.Error()) 59 | return 60 | } 61 | 62 | fmt.Println("Modified Order ID :- ", modifiedOrder) 63 | 64 | //Cancel Order 65 | cancelledOrder, err := ABClient.CancelOrder("NORMAL", modifiedOrder.OrderID) 66 | 67 | if err != nil { 68 | fmt.Println(err.Error()) 69 | return 70 | } 71 | 72 | fmt.Println("Cancelled Order ID :- ", cancelledOrder) 73 | 74 | //Get Holdings 75 | holdings, err := ABClient.GetHoldings() 76 | 77 | if err != nil { 78 | fmt.Println(err.Error()) 79 | } else { 80 | 81 | fmt.Println("Holdings :- ", holdings) 82 | } 83 | 84 | //Get Positions 85 | positions, err := ABClient.GetPositions() 86 | 87 | if err != nil { 88 | fmt.Println(err.Error()) 89 | } else { 90 | 91 | fmt.Println("Positions :- ", positions) 92 | } 93 | 94 | //Get TradeBook 95 | trades, err := ABClient.GetTradeBook() 96 | 97 | if err != nil { 98 | fmt.Println(err.Error()) 99 | } else { 100 | 101 | fmt.Println("All Trades :- ", trades) 102 | } 103 | 104 | //Get Last Traded Price 105 | ltp, err := ABClient.GetLTP(SmartApi.LTPParams{Exchange: "NSE", TradingSymbol: "SBIN-EQ", SymbolToken: "3045"}) 106 | 107 | if err != nil { 108 | fmt.Println(err.Error()) 109 | return 110 | } 111 | 112 | fmt.Println("Last Traded Price :- ", ltp) 113 | 114 | //Get Risk Management System 115 | rms, err := ABClient.GetRMS() 116 | 117 | if err != nil { 118 | fmt.Println(err.Error()) 119 | return 120 | } 121 | 122 | fmt.Println("Risk Managemanet System :- ", rms) 123 | 124 | //Position Conversion 125 | err = ABClient.ConvertPosition(SmartApi.ConvertPositionParams{"NSE", "SBIN-EQ", "INTRADAY", "MARGIN", "BUY", 1, "DAY"}) 126 | if err != nil { 127 | fmt.Println(err.Error()) 128 | return 129 | } 130 | 131 | fmt.Println("Position Conversion Successful") 132 | } 133 | -------------------------------------------------------------------------------- /example/websocket/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | SmartApi "github.com/angel-one/smartapigo" 6 | "github.com/angel-one/smartapigo/websocket" 7 | "time" 8 | ) 9 | 10 | var socketClient *websocket.SocketClient 11 | 12 | // Triggered when any error is raised 13 | func onError(err error) { 14 | fmt.Println("Error: ", err) 15 | } 16 | 17 | // Triggered when websocket connection is closed 18 | func onClose(code int, reason string) { 19 | fmt.Println("Close: ", code, reason) 20 | } 21 | 22 | // Triggered when connection is established and ready to send and accept data 23 | func onConnect() { 24 | fmt.Println("Connected") 25 | err := socketClient.Subscribe() 26 | if err != nil { 27 | fmt.Println("err: ", err) 28 | } 29 | } 30 | 31 | // Triggered when a message is received 32 | func onMessage(message []map[string]interface{}) { 33 | fmt.Printf("Message Received :- %v\n", message) 34 | } 35 | 36 | // Triggered when reconnection is attempted which is enabled by default 37 | func onReconnect(attempt int, delay time.Duration) { 38 | fmt.Printf("Reconnect attempt %d in %fs\n", attempt, delay.Seconds()) 39 | } 40 | 41 | // Triggered when maximum number of reconnect attempt is made and the program is terminated 42 | func onNoReconnect(attempt int) { 43 | fmt.Printf("Maximum no of reconnect attempt reached: %d\n", attempt) 44 | } 45 | 46 | func main() { 47 | 48 | // Create New Angel Broking Client 49 | ABClient := SmartApi.New("Your Client Code", "Your Password", "Your api key") 50 | 51 | // User Login and Generate User Session 52 | session, err := ABClient.GenerateSession() 53 | 54 | if err != nil { 55 | fmt.Println(err.Error()) 56 | return 57 | } 58 | 59 | //Get User Profile 60 | session.UserProfile, err = ABClient.GetUserProfile() 61 | 62 | if err != nil { 63 | fmt.Println(err.Error()) 64 | return 65 | } 66 | 67 | // New Websocket Client 68 | socketClient = websocket.New(session.ClientCode, session.FeedToken, "nse_cm|17963&nse_cm|3499&nse_cm|11536&nse_cm|21808&nse_cm|317") 69 | 70 | // Assign callbacks 71 | socketClient.OnError(onError) 72 | socketClient.OnClose(onClose) 73 | socketClient.OnMessage(onMessage) 74 | socketClient.OnConnect(onConnect) 75 | socketClient.OnReconnect(onReconnect) 76 | socketClient.OnNoReconnect(onNoReconnect) 77 | 78 | // Start Consuming Data 79 | socketClient.Serve() 80 | 81 | } 82 | -------------------------------------------------------------------------------- /funds.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import "net/http" 4 | 5 | // RMS represents API response. 6 | type RMS struct { 7 | Net string `json:"net"` 8 | AvailableCash string `json:"availablecash"` 9 | AvailableIntraDayPayIn string `json:"availableintradaypayin"` 10 | AvailableLimitMargin string `json:"availablelimitmargin"` 11 | Collateral string `json:"collateral"` 12 | M2MUnrealized string `json:"m2munrealized"` 13 | M2MRealized string `json:"m2mrealized"` 14 | UtilisedDebits string `json:"utiliseddebits"` 15 | UtilisedSpan string `json:"utilisedspan"` 16 | UtilisedOptionPremium string `json:"utilisedoptionpremium"` 17 | UtilisedHoldingSales string `json:"utilisedholdingsales"` 18 | UtilisedExposure string `json:"utilisedexposure"` 19 | UtilisedTurnover string `json:"utilisedturnover"` 20 | UtilisedPayout string `json:"utilisedpayout"` 21 | } 22 | 23 | // GetRMS gets Risk Management System. 24 | func (c *Client) GetRMS() (RMS, error) { 25 | var rms RMS 26 | err := c.doEnvelope(http.MethodGet, URIRMS, nil, nil, &rms, true) 27 | return rms, err 28 | } 29 | -------------------------------------------------------------------------------- /funds_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (ts *TestSuite) TestGetRMS(t *testing.T) { 8 | t.Parallel() 9 | rms, err := ts.TestConnect.GetRMS() 10 | if err != nil { 11 | t.Errorf("Error while fetching RMS. %v", err) 12 | } 13 | 14 | if rms.Net == "" { 15 | t.Errorf("Error while fetching Net from RMS. %v", err) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gotrade94/angel-sdk 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/angel-one/smartapigo v0.0.0-20221003090802-47cbce38ac5c 7 | github.com/gorilla/websocket v1.5.0 8 | github.com/jarcoal/httpmock v1.2.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/angel-one/smartapigo v0.0.0-20221003090802-47cbce38ac5c h1:17nUHJinYRx651xlat5FUZ2ciPOfpg8TcrNkXCXntuQ= 2 | github.com/angel-one/smartapigo v0.0.0-20221003090802-47cbce38ac5c/go.mod h1:2lgK/wnNg2njLIUsWWo4EX3wvL8sMlHwuaJXKM3m90I= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 5 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 6 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= 8 | github.com/jarcoal/httpmock v1.2.0 h1:gSvTxxFR/MEMfsGrvRbdfpRUMBStovlSRLw0Ep1bwwc= 9 | github.com/jarcoal/httpmock v1.2.0/go.mod h1:oCoTsnAz4+UoOUIf5lJOWV2QQIW5UoeUI6aM2YnWAZk= 10 | github.com/maxatome/go-testdeep v1.11.0 h1:Tgh5efyCYyJFGUYiT0qxBSIDeXw0F5zSoatlou685kk= 11 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "bytes" 5 | "crypto/tls" 6 | "encoding/json" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "os" 12 | "time" 13 | ) 14 | 15 | // HTTPClient represents an HTTP client. 16 | type HTTPClient interface { 17 | Do(method, rURL string, params map[string]interface{}, headers http.Header) (HTTPResponse, error) 18 | DoEnvelope(method, url string, params map[string]interface{}, headers http.Header, obj interface{}) error 19 | GetClient() *httpClient 20 | } 21 | 22 | // httpClient is the default implementation of HTTPClient. 23 | type httpClient struct { 24 | client *http.Client 25 | hLog *log.Logger 26 | debug bool 27 | } 28 | 29 | // HTTPResponse encompasses byte body + the response of an HTTP request. 30 | type HTTPResponse struct { 31 | Body []byte 32 | Response *http.Response 33 | } 34 | 35 | type envelope struct { 36 | Status bool `json:"status"` 37 | ErrorCode string `json:"errorcode"` 38 | Message string `json:"message"` 39 | Data interface{} `json:"data"` 40 | } 41 | 42 | // NewHTTPClient returns a self-contained HTTP request object 43 | // with underlying keep-alive transport. 44 | func NewHTTPClient(h *http.Client, hLog *log.Logger, debug bool) HTTPClient { 45 | if hLog == nil { 46 | hLog = log.New(os.Stdout, "base.HTTP: ", log.Ldate|log.Ltime|log.Lshortfile) 47 | } 48 | 49 | if h == nil { 50 | h = &http.Client{ 51 | Timeout: time.Duration(5) * time.Second, 52 | Transport: &http.Transport{ 53 | MaxIdleConnsPerHost: 10, 54 | ResponseHeaderTimeout: time.Second * time.Duration(5), 55 | TLSClientConfig: &tls.Config{InsecureSkipVerify : true}, 56 | }, 57 | } 58 | } 59 | 60 | return &httpClient{ 61 | hLog: hLog, 62 | client: h, 63 | debug: debug, 64 | } 65 | } 66 | 67 | // Do executes an HTTP request and returns the response. 68 | func (h *httpClient) Do(method, rURL string, params map[string]interface{}, headers http.Header) (HTTPResponse, error) { 69 | var ( 70 | resp = HTTPResponse{} 71 | postParams io.Reader 72 | err error 73 | ) 74 | 75 | if method == http.MethodPost && params != nil { 76 | jsonParams, err := json.Marshal(params) 77 | 78 | if err != nil { 79 | return resp, err 80 | } 81 | 82 | postParams = bytes.NewBuffer(jsonParams) 83 | } 84 | 85 | req, err := http.NewRequest(method, rURL, postParams) 86 | 87 | if err != nil { 88 | h.hLog.Printf("Request preparation failed: %v", err) 89 | return resp,err 90 | } 91 | 92 | if headers != nil { 93 | req.Header = headers 94 | } 95 | 96 | // If a content-type isn't set, set the default one. 97 | if req.Header.Get("Content-Type") == "" { 98 | if method == http.MethodPost || method == http.MethodPut { 99 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 100 | } 101 | } 102 | 103 | // If the request method is GET or DELETE, add the params as QueryString. 104 | //if method == http.MethodGet || method == http.MethodDelete { 105 | // req.URL.RawQuery = params.Encode() 106 | //} 107 | 108 | r, err := h.client.Do(req) 109 | if err != nil { 110 | h.hLog.Printf("Request failed: %v", err) 111 | return resp,err 112 | } 113 | 114 | defer r.Body.Close() 115 | 116 | body, err := ioutil.ReadAll(r.Body) 117 | if err != nil { 118 | h.hLog.Printf("Unable to read response: %v", err) 119 | return resp,err 120 | } 121 | 122 | resp.Response = r 123 | resp.Body = body 124 | if h.debug { 125 | h.hLog.Printf("%s %s -- %d %v", method, req.URL.RequestURI(), resp.Response.StatusCode, req.Header) 126 | } 127 | 128 | return resp, nil 129 | } 130 | 131 | // DoEnvelope makes an HTTP request and parses the JSON response (fastglue envelop structure) 132 | func (h *httpClient) DoEnvelope(method, url string, params map[string]interface{}, headers http.Header, obj interface{}) error { 133 | resp, err := h.Do(method, url, params, headers) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | // Successful request, but error envelope. 139 | if resp.Response.StatusCode >= http.StatusBadRequest { 140 | var e envelope 141 | if err := json.Unmarshal(resp.Body, &e); err != nil { 142 | h.hLog.Printf("Error parsing JSON response: %v", err) 143 | return err 144 | } 145 | 146 | return NewError(e.ErrorCode, e.Message, e.Data) 147 | } 148 | 149 | // We now unmarshal the body. 150 | envl := envelope{} 151 | envl.Data = obj 152 | 153 | if err := json.Unmarshal(resp.Body, &envl); err != nil { 154 | h.hLog.Printf("Error parsing JSON response: %v | %s", err, resp.Body) 155 | return err 156 | } 157 | 158 | if !envl.Status { 159 | return NewError(envl.ErrorCode, envl.Message, envl.Data) 160 | } 161 | 162 | return nil 163 | } 164 | 165 | // GetClient return's the underlying net/http client. 166 | func (h *httpClient) GetClient() *httpClient { 167 | return h 168 | } 169 | -------------------------------------------------------------------------------- /market.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import "net/http" 4 | 5 | // LTPResponse represents LTP API Response. 6 | type LTPResponse struct { 7 | Exchange string `json:"exchange"` 8 | TradingSymbol string `json:"tradingsymbol"` 9 | SymbolToken string `json:"symboltoken"` 10 | Open float64 `json:"open"` 11 | High float64 `json:"high"` 12 | Low float64 `json:"low"` 13 | Close float64 `json:"close"` 14 | Ltp float64 `json:"ltp"` 15 | } 16 | 17 | // LTPParams represents parameters for getting LTP. 18 | type LTPParams struct { 19 | Exchange string `json:"exchange"` 20 | TradingSymbol string `json:"tradingsymbol"` 21 | SymbolToken string `json:"symboltoken"` 22 | } 23 | 24 | // GetLTP gets Last Traded Price. 25 | func (c *Client) GetLTP(ltpParams LTPParams) (LTPResponse, error) { 26 | var ltp LTPResponse 27 | params := structToMap(ltpParams, "json") 28 | err := c.doEnvelope(http.MethodPost, URILTP, params, nil, <p, true) 29 | return ltp, err 30 | } 31 | -------------------------------------------------------------------------------- /market_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (ts *TestSuite) TestGetLTP(t *testing.T) { 8 | t.Parallel() 9 | params := LTPParams{ 10 | Exchange: "NSE", 11 | TradingSymbol: "SBIN-EQ", 12 | SymbolToken: "3045", 13 | } 14 | ltp, err := ts.TestConnect.GetLTP(params) 15 | if err != nil { 16 | t.Errorf("Error while fetching LTP. %v", err) 17 | } 18 | 19 | if ltp.Exchange == "" { 20 | t.Errorf("Error while exchange in LTP. %v", err) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /mock_responses/holdings.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": [ 6 | { 7 | "tradingSymbol": "RELIANCE-EQ", 8 | "exchange": "NSE", 9 | "isin": "INE002A01018", 10 | "t1quantity": "0", 11 | "realisedquantity": "0", 12 | "quantity": "0", 13 | "authorisedquantity": "0", 14 | "profitandloss": "0", 15 | "product": "MIS", 16 | "collateralquantity": "0", 17 | "collateraltype": null, 18 | "haircut": "0" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /mock_responses/logout.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": "" 6 | } -------------------------------------------------------------------------------- /mock_responses/ltp.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": { 6 | "exchange": "NSE", 7 | "tradingsymbol": "SBIN-EQ", 8 | "symboltoken":"3045", 9 | "open": "18600", 10 | "high": "19125", 11 | "low": "18500", 12 | "close": "18780", 13 | "ltp": "19100" 14 | } 15 | } -------------------------------------------------------------------------------- /mock_responses/order_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": { 6 | "script": "SBIN-EQ", 7 | "orderid": "201020000000080" 8 | } 9 | } -------------------------------------------------------------------------------- /mock_responses/orders.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": [ 6 | { 7 | "variety": null, 8 | "ordertype": "LIMIT", 9 | "producttype": "INTRADAY", 10 | "duration": "DAY", 11 | "price": "19400", 12 | "triggerprice": "0", 13 | "quantity": "1", 14 | "disclosedquantity": "0", 15 | "squareoff": "0", 16 | "stoploss": "0", 17 | "trailingstoploss": "0", 18 | "tradingsymbol": "SBIN-EQ", 19 | "transactiontype": "BUY", 20 | "exchange": "NSE", 21 | "symboltoken": null, 22 | "instrumenttype": "", 23 | "strikeprice": "-1", 24 | "optiontype": "", 25 | "expirydate": "", 26 | "lotsize": "1", 27 | "cancelsize": "1", 28 | "averageprice": "0", 29 | "filledshares": "0", 30 | "unfilledshares": "1", 31 | "orderid": "201020000000080", 32 | "text": "", 33 | "status": "cancelled", 34 | "orderstatus": "cancelled", 35 | "updatetime": "20-Oct-2020 13:10:59", 36 | "exchtime": "20-Oct-2020 13:10:59", 37 | "exchorderupdatetime": "20-Oct-2020 13:10:59", 38 | "fillid": null, 39 | "filltime": null 40 | } 41 | ] 42 | } -------------------------------------------------------------------------------- /mock_responses/position_conversion.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": null 6 | } -------------------------------------------------------------------------------- /mock_responses/positions.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": [ 6 | { 7 | "exchange": "NSE", 8 | "symboltoken": "2885", 9 | "producttype": "DELIVERY", 10 | "tradingsymbol": "RELIANCE-EQ", 11 | "symbolname": "RELIANCE", 12 | "instrumenttype": "", 13 | "priceden": "1", 14 | "pricenum": "1", 15 | "genden": "1", 16 | "gennum": "1", 17 | "precision": "2", 18 | "multiplier": "-1", 19 | "boardlotsize": "1", 20 | "buyqty": "1", 21 | "sellqty": "0", 22 | "buyamount": "223580", 23 | "sellamount": "0", 24 | "symbolgroup": "EQ", 25 | "strikeprice": "-1", 26 | "optiontype": "", 27 | "expirydate": "", 28 | "lotsize": "1", 29 | "cfbuyqty": "0", 30 | "cfsellqty": "0", 31 | "cfbuyamount": "0", 32 | "cfsellamount": "0", 33 | "buyavgprice": "223580", 34 | "sellavgprice": "0", 35 | "avgnetprice": "223580", 36 | "netvalue": "-223580", 37 | "netqty": "1", 38 | "totalbuyvalue": "223580", 39 | "totalsellvalue": "0", 40 | "cfbuyavgprice": "0", 41 | "cfsellavgprice": "0", 42 | "totalbuyavgprice": "223580", 43 | "totalsellavgprice": "0", 44 | "netprice": "223580" 45 | } 46 | ] } -------------------------------------------------------------------------------- /mock_responses/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": { 6 | "clientcode": "D88311", 7 | "name": "SANDIP KHAIRNAR", 8 | "email": "SANDIP.KHAIRNAR@ANGELBROKING.COM", 9 | "mobileno": "9920292496", 10 | "exchanges": [ 11 | "NSE", 12 | "BSE", 13 | "MCX", 14 | "CDS", 15 | "NCDEX", 16 | "NFO" 17 | ], 18 | "products": [ 19 | "DELIVERY", 20 | "INTRADAY", 21 | "MARGIN" 22 | ], 23 | "lastlogintime": "", 24 | "brokerid": "B2C" 25 | } 26 | } -------------------------------------------------------------------------------- /mock_responses/rms.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": { 6 | "net": "9999999999999", 7 | "availablecash": "9999999999999", 8 | "availableintradaypayin": "0", 9 | "availablelimitmargin": "0", 10 | "collateral": "0", 11 | "m2munrealized": "0", 12 | "m2mrealized": "0", 13 | "utiliseddebits": "0", 14 | "utilisedspan": "0", 15 | "utilisedoptionpremium": "0", 16 | "utilisedholdingsales": "0", 17 | "utilisedexposure": "0", 18 | "utilisedturnover": "0", 19 | "utilisedpayout": "0" 20 | } 21 | } -------------------------------------------------------------------------------- /mock_responses/session.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": { 6 | "jwtToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJEODgzIiwiUk9MRVMiOjAsIkFQUCI6IlVTRVIiLCJpYXQiOjE1OTk0ODkwMzksImV4cCI6MTU5OTQ5ODAzOX0.E_AK-K70tEgAZA7ddRnQPcBYQw38p4MBc57kwEpnFoNsTcmEHe3AwFblcDRVBWqTC6AGgZID7ttC1C4QfMTTEw", 7 | "refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1OTk0ODkwMzl9.3jX_LpNtQKZo_Ufpux4qxGgdBNZosEM8BqkWkBJQli_4B0FjJdRLNMueyyGl5F5qv1qv1wdK-jFRYnlr2IXktg", 8 | "feedToken": "1345613428" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /mock_responses/trades.json: -------------------------------------------------------------------------------- 1 | { 2 | "status": true, 3 | "message": "SUCCESS", 4 | "errorcode": "", 5 | "data": [ 6 | { 7 | "exchange": "NSE", 8 | "producttype": "DELIVERY", 9 | "tradingsymbol": "ITC-EQ", 10 | "instrumenttype": "", 11 | "symbolgroup": "EQ", 12 | "strikeprice": "-1", 13 | "optiontype": "", 14 | "expirydate": "", 15 | "marketlot": "1", 16 | "precision": "2", 17 | "multiplier": "-1", 18 | "tradevalue": "17500", 19 | "transactiontype": "BUY", 20 | "fillprice": "17500", 21 | "fillsize": "1", 22 | "orderid": "201020000000095", 23 | "fillid": "50005750", 24 | "filltime": "13:27:53" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /orders.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Order represents a individual order response. 8 | type Order struct { 9 | Variety string `json:"variety"` 10 | OrderType string `json:"ordertype"` 11 | ProductType string `json:"producttype"` 12 | Duration string `json:"duration"` 13 | Price float64 `json:"price"` 14 | TriggerPrice float64 `json:"triggerprice"` 15 | Quantity string `json:"quantity"` 16 | DisclosedQuantity string `json:"disclosedquantity"` 17 | SquareOff float64 `json:"squareoff"` 18 | StopLoss float64 `json:"stoploss"` 19 | TrailingStopLoss float64 `json:"trailingstoploss"` 20 | TrailingSymbol string `json:"trailingsymbol"` 21 | TransactionType string `json:"transactiontype"` 22 | Exchange string `json:"exchange"` 23 | SymbolToken string `json:"symboltoken"` 24 | InstrumentType string `json:"instrumenttype"` 25 | StrikePrice float64 `json:"strikeprice"` 26 | OptionType string `json:"optiontype"` 27 | ExpiryDate string `json:"expirydate"` 28 | LotSize string `json:"lotsize"` 29 | CancelSize string `json:"cancelsize"` 30 | AveragePrice float64 `json:"averageprice"` 31 | FilledShares string `json:"filledshares"` 32 | UnfilledShares string `json:"unfilledshares"` 33 | OrderID string `json:"orderid"` 34 | Text string `json:"text"` 35 | Status string `json:"status"` 36 | OrderStatus string `json:"orderstatus"` 37 | UpdateTime string `json:"updatetime"` 38 | ExchangeTime string `json:"exchtime"` 39 | ExchangeOrderUpdateTime string `json:"exchorderupdatetime"` 40 | FillID string `json:"fillid"` 41 | FillTime string `json:"filltime"` 42 | } 43 | 44 | // Orders is a list of orders. 45 | type Orders []Order 46 | 47 | // OrderParams represents parameters for placing an order. 48 | type OrderParams struct { 49 | Variety string `json:"variety"` 50 | TradingSymbol string `json:"tradingsymbol"` 51 | SymbolToken string `json:"symboltoken"` 52 | TransactionType string `json:"transactiontype"` 53 | Exchange string `json:"exchange"` 54 | OrderType string `json:"ordertype"` 55 | ProductType string `json:"producttype"` 56 | Duration string `json:"duration"` 57 | Price string `json:"price"` 58 | SquareOff string `json:"squareoff"` 59 | StopLoss string `json:"stoploss"` 60 | Quantity string `json:"quantity"` 61 | } 62 | 63 | // OrderParams represents parameters for modifying an order. 64 | type ModifyOrderParams struct { 65 | Variety string `json:"variety"` 66 | OrderID string `json:"orderid"` 67 | OrderType string `json:"ordertype"` 68 | ProductType string `json:"producttype"` 69 | Duration string `json:"duration"` 70 | Price string `json:"price"` 71 | Quantity string `json:"quantity"` 72 | TradingSymbol string `json:"tradingsymbol"` 73 | SymbolToken string `json:"symboltoken"` 74 | Exchange string `json:"exchange"` 75 | } 76 | 77 | // OrderResponse represents the order place success response. 78 | type OrderResponse struct { 79 | Script string `json:"script"` 80 | OrderID string `json:"orderid"` 81 | } 82 | 83 | // Trade represents an individual trade response. 84 | type Trade struct { 85 | Exchange string `json:"exchange"` 86 | ProductType string `json:"producttype"` 87 | TradingSymbol string `json:"tradingsymbol"` 88 | InstrumentType string `json:"instrumenttype"` 89 | SymbolGroup string `json:"symbolgroup"` 90 | StrikePrice string `json:"strikeprice"` 91 | OptionType string `json:"optiontype"` 92 | ExpiryDate string `json:"expirydate"` 93 | MarketLot string `json:"marketlot"` 94 | Precision string `json:"precision"` 95 | Multiplier string `json:"multiplier"` 96 | TradeValue string `json:"tradevalue"` 97 | TransactionType string `json:"transactiontype"` 98 | FillPrice string `json:"fillprice"` 99 | FillSize string `json:"fillsize"` 100 | OrderID string `json:"orderid"` 101 | FillID string `json:"fillid"` 102 | FillTime string `json:"filltime"` 103 | } 104 | 105 | // Trades is a list of trades. 106 | type Trades []Trade 107 | 108 | // Position represents an individual position response. 109 | type Position struct { 110 | Exchange string `json:"exchange"` 111 | SymbolToken string `json:"symboltoken"` 112 | ProductType string `json:"producttype"` 113 | Tradingsymbol string `json:"tradingsymbol"` 114 | SymbolName string `json:"symbolname"` 115 | InstrumentType string `json:"instrumenttype"` 116 | PriceDen string `json:"priceden"` 117 | PriceNum string `json:"pricenum"` 118 | GenDen string `json:"genden"` 119 | GenNum string `json:"gennum"` 120 | Precision string `json:"precision"` 121 | Multiplier string `json:"multiplier"` 122 | BoardLotSize string `json:"boardlotsize"` 123 | BuyQuantity string `json:"buyquantity"` 124 | SellQuantity string `json:"sellquantity"` 125 | BuyAmount string `json:"buyamount"` 126 | SellAmount string `json:"sellamount"` 127 | SymbolGroup string `json:"symbolgroup"` 128 | StrikePrice string `json:"strikeprice"` 129 | OptionType string `json:"optiontype"` 130 | ExpiryDate string `json:"expirydate"` 131 | LotSize string `json:"lotsize"` 132 | CfBuyQty string `json:"cfbuyqty"` 133 | CfSellQty string `json:"cfsellqty"` 134 | CfBuyAmount string `json:"cfbuyamount"` 135 | CfSellAmount string `json:"cfsellamount"` 136 | BuyAveragePrice string `json:"buyavgprice"` 137 | SellAveragePrice string `json:"sellavgprice"` 138 | AverageNetPrice string `json:"avgnetprice"` 139 | NetValue string `json:"netvalue"` 140 | NetQty string `json:"netqty"` 141 | TotalBuyValue string `json:"totalbuyvalue"` 142 | TotalSellValue string `json:"totalsellvalue"` 143 | CfBuyAveragePrice string `json:"cfbuyavgprice"` 144 | CfSellAveragePrice string `json:"cfsellavgprice"` 145 | TotalBuyAveragePrice string `json:"totalbuyavgprice"` 146 | TotalSellAveragePrice string `json:"totalsellavgprice"` 147 | NetPrice string `json:"netprice"` 148 | } 149 | 150 | // Positions represents a list of net and day positions. 151 | type Positions []Position 152 | 153 | // ConvertPositionParams represents the input params for a position conversion. 154 | type ConvertPositionParams struct { 155 | Exchange string `url:"exchange"` 156 | TradingSymbol string `url:"tradingsymbol"` 157 | OldProductType string `url:"oldproducttype"` 158 | NewProductType string `url:"newproducttype"` 159 | TransactionType string `url:"transactiontype"` 160 | Quantity int `url:"quantity"` 161 | Type string `json:"type"` 162 | } 163 | 164 | // GetOrderBook gets user orders. 165 | func (c *Client) GetOrderBook() (Orders, error) { 166 | var orders Orders 167 | err := c.doEnvelope(http.MethodGet, URIGetOrderBook, nil, nil, &orders, true) 168 | return orders, err 169 | } 170 | 171 | // PlaceOrder places an order. 172 | func (c *Client) PlaceOrder(orderParams OrderParams) (OrderResponse, error) { 173 | var ( 174 | orderResponse OrderResponse 175 | params map[string]interface{} 176 | err error 177 | ) 178 | 179 | params = structToMap(orderParams, "json") 180 | 181 | err = c.doEnvelope(http.MethodPost, URIPlaceOrder, params, nil, &orderResponse, true) 182 | return orderResponse, err 183 | } 184 | 185 | // ModifyOrder for modifying an order. 186 | func (c *Client) ModifyOrder(modifyOrderParams ModifyOrderParams) (OrderResponse, error) { 187 | var ( 188 | orderResponse OrderResponse 189 | params map[string]interface{} 190 | err error 191 | ) 192 | 193 | params = structToMap(modifyOrderParams, "json") 194 | 195 | err = c.doEnvelope(http.MethodPost, URIModifyOrder, params, nil, &orderResponse, true) 196 | return orderResponse, err 197 | } 198 | 199 | // CancelOrder for cancellation of an order. 200 | func (c *Client) CancelOrder(variety string, orderid string) (OrderResponse, error) { 201 | var ( 202 | orderResponse OrderResponse 203 | err error 204 | ) 205 | 206 | params := make(map[string]interface{}) 207 | params["variety"] = variety 208 | params["orderid"] = orderid 209 | 210 | err = c.doEnvelope(http.MethodPost, URICancelOrder, params, nil, &orderResponse, true) 211 | return orderResponse, err 212 | } 213 | 214 | // GetPositions gets user positions. 215 | func (c *Client) GetPositions() (Positions, error) { 216 | var positions Positions 217 | err := c.doEnvelope(http.MethodGet, URIGetPositions, nil, nil, &positions, true) 218 | return positions, err 219 | } 220 | 221 | // GetTradeBook gets user trades. 222 | func (c *Client) GetTradeBook() (Trades, error) { 223 | var trades Trades 224 | err := c.doEnvelope(http.MethodGet, URIGetTradeBook, nil, nil, &trades, true) 225 | return trades, err 226 | } 227 | 228 | // ConvertPosition converts position's product type. 229 | func (c *Client) ConvertPosition(convertPositionParams ConvertPositionParams) error { 230 | var ( 231 | params map[string]interface{} 232 | err error 233 | ) 234 | 235 | params = structToMap(convertPositionParams, "json") 236 | 237 | err = c.doEnvelope(http.MethodPost, URIConvertPosition, params, nil, nil, true) 238 | return err 239 | } 240 | -------------------------------------------------------------------------------- /orders_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (ts *TestSuite) TestGetOrders(t *testing.T) { 8 | t.Parallel() 9 | orders, err := ts.TestConnect.GetOrderBook() 10 | if err != nil { 11 | t.Errorf("Error while fetching orders. %v", err) 12 | } 13 | for _, order := range orders { 14 | if order.OrderID == "" { 15 | t.Errorf("Error while fetching order id in orders. %v", err) 16 | } 17 | } 18 | } 19 | 20 | func (ts *TestSuite) TestGetTrades(t *testing.T) { 21 | t.Parallel() 22 | trades, err := ts.TestConnect.GetTradeBook() 23 | if err != nil { 24 | t.Errorf("Error while fetching trades. %v", err) 25 | } 26 | for _, trade := range trades { 27 | if trade.OrderID == "" { 28 | t.Errorf("Error while fetching trade id in trades. %v", err) 29 | } 30 | } 31 | } 32 | 33 | func (ts *TestSuite) TestPlaceOrder(t *testing.T) { 34 | t.Parallel() 35 | params := OrderParams{"NORMAL", "SBIN-EQ", "3045", "BUY", "NSE", "LIMIT", "INTRADAY", "DAY", "19500", "0", "0", "1"} 36 | orderResponse, err := ts.TestConnect.PlaceOrder(params) 37 | if err != nil { 38 | t.Errorf("Error while placing order. %v", err) 39 | } 40 | if orderResponse.OrderID == "" { 41 | t.Errorf("No order id returned. Error %v", err) 42 | } 43 | } 44 | 45 | func (ts *TestSuite) TestModifyOrder(t *testing.T) { 46 | t.Parallel() 47 | params := ModifyOrderParams{"NORMAL", "test", "LIMIT", "INTRADAY", "DAY", "19400", "1","SBI-EQ","3045","NSE"} 48 | orderResponse, err := ts.TestConnect.ModifyOrder( params) 49 | if err != nil { 50 | t.Errorf("Error while updating order. %v", err) 51 | } 52 | if orderResponse.OrderID == "" { 53 | t.Errorf("No order id returned. Error %v", err) 54 | } 55 | } 56 | 57 | func (ts *TestSuite) TestCancelOrder(t *testing.T) { 58 | t.Parallel() 59 | parentOrderID := "test" 60 | 61 | orderResponse, err := ts.TestConnect.CancelOrder("NORMAL",parentOrderID) 62 | if err != nil { 63 | t.Errorf("Error while cancellation of an order. %v", err) 64 | } 65 | if orderResponse.OrderID == "" { 66 | t.Errorf("No order id returned. Error %v", err) 67 | } 68 | } 69 | 70 | func (ts *TestSuite) TestGetPositions(t *testing.T) { 71 | t.Parallel() 72 | positions, err := ts.TestConnect.GetPositions() 73 | if err != nil { 74 | t.Errorf("Error while fetching positions. %v", err) 75 | } 76 | for _, position := range positions { 77 | if position.Exchange == "" { 78 | t.Errorf("Error while fetching exchange in positions. %v", err) 79 | } 80 | } 81 | } 82 | 83 | func (ts *TestSuite) TestConvertPosition(t *testing.T) { 84 | t.Parallel() 85 | params := ConvertPositionParams{"NSE","SBIN-EQ","DELIVERY","MARGIN","BUY",1,"DAY"} 86 | err := ts.TestConnect.ConvertPosition(params) 87 | if err != nil { 88 | t.Errorf("Error while fetching positions. %v", err) 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /portfolio.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Holding is an individual holdings response. 8 | type Holding struct { 9 | Tradingsymbol string `json:"tradingsymbol"` 10 | Exchange string `json:"exchange"` 11 | ISIN string `json:"isin"` 12 | T1Quantity string `json:"t1quantity"` 13 | RealisedQuantity string `json:"realisedquantity"` 14 | Quantity string `json:"quantity"` 15 | AuthorisedQuantity string `json:"authorisedquantity"` 16 | ProfitAndLoss string `json:"profitandloss"` 17 | Product string `json:"product"` 18 | CollateralQuantity string `json:"collateralquantity"` 19 | CollateralType string `json:"collateraltype"` 20 | Haircut string `json:"haircut"` 21 | } 22 | 23 | // Holdings is a list of holdings 24 | type Holdings []Holding 25 | 26 | 27 | // GetHoldings gets a list of holdings. 28 | func (c *Client) GetHoldings() (Holdings, error) { 29 | var holdings Holdings 30 | err := c.doEnvelope(http.MethodGet, URIGetHoldings, nil, nil, &holdings, true) 31 | return holdings, err 32 | } -------------------------------------------------------------------------------- /portfolio_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (ts *TestSuite) TestGetHoldings(t *testing.T) { 8 | t.Parallel() 9 | holdings, err := ts.TestConnect.GetHoldings() 10 | if err != nil { 11 | t.Errorf("Error while fetching holdings. %v", err) 12 | } 13 | 14 | for _, holding := range holdings { 15 | if holding.Exchange == "" { 16 | t.Errorf("Error while fetching exchange in holdings. %v", err) 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /user.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // UserSession represents the response after a successful authentication. 8 | type UserSession struct { 9 | UserProfile 10 | UserSessionTokens 11 | } 12 | 13 | // UserSessionTokens represents response after renew access token. 14 | type UserSessionTokens struct { 15 | AccessToken string `json:"jwtToken"` 16 | RefreshToken string `json:"refreshToken"` 17 | FeedToken string `json:"feedToken"` 18 | } 19 | 20 | // UserProfile represents a user's personal and financial profile. 21 | type UserProfile struct { 22 | ClientCode string `json:"clientcode"` 23 | UserName string `json:"name"` 24 | Email string `json:"email"` 25 | Phone string `json:"mobileno"` 26 | Broker string `json:"broker"` 27 | Products []string `json:"products"` 28 | LastLoginTime string `json:"lastlogintime"` 29 | Exchanges []string `json:"exchanges"` 30 | } 31 | 32 | // GenerateSession gets a user session details in exchange of username and password. 33 | // Access token is automatically set if the session is retrieved successfully. 34 | // Do the token exchange with the `requestToken` obtained after the login flow, 35 | // and retrieve the `accessToken` required for all subsequent requests. The 36 | // response contains not just the `accessToken`, but metadata for the user who has authenticated. 37 | //totp used is required for 2 factor authentication 38 | func (c *Client) GenerateSession(totp string) (UserSession, error) { 39 | 40 | // construct url values 41 | params := make(map[string]interface{}) 42 | params["clientcode"] = c.clientCode 43 | params["password"] = c.password 44 | params["totp"] = totp 45 | 46 | var session UserSession 47 | err := c.doEnvelope(http.MethodPost, URILogin, params, nil, &session) 48 | // Set accessToken on successful session retrieve 49 | if err == nil && session.AccessToken != "" { 50 | c.SetAccessToken(session.AccessToken) 51 | } 52 | return session, err 53 | } 54 | 55 | // RenewAccessToken renews expired access token using valid refresh token. 56 | func (c *Client) RenewAccessToken(refreshToken string) (UserSessionTokens, error) { 57 | 58 | params := map[string]interface{}{} 59 | params["refreshToken"] = refreshToken 60 | 61 | var session UserSessionTokens 62 | err := c.doEnvelope(http.MethodPost, URIUserSessionRenew, params, nil, &session, true) 63 | 64 | // Set accessToken on successful session retrieve 65 | if err == nil && session.AccessToken != "" { 66 | c.SetAccessToken(session.AccessToken) 67 | } 68 | 69 | return session, err 70 | } 71 | 72 | // GetUserProfile gets user profile. 73 | func (c *Client) GetUserProfile() (UserProfile, error) { 74 | var userProfile UserProfile 75 | err := c.doEnvelope(http.MethodGet, URIUserProfile, nil, nil, &userProfile, true) 76 | return userProfile, err 77 | } 78 | 79 | // Logout from User Session. 80 | func (c *Client) Logout() (bool, error) { 81 | var status bool 82 | params := map[string]interface{}{} 83 | params["clientcode"] = c.clientCode 84 | err := c.doEnvelope(http.MethodPost, URILogout, params, nil, nil, true) 85 | if err == nil { 86 | status = true 87 | } 88 | return status, err 89 | } 90 | -------------------------------------------------------------------------------- /user_test.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func (ts *TestSuite) TestGenerateSession(t *testing.T) { 8 | t.Parallel() 9 | session, err := ts.TestConnect.GenerateSession() 10 | if err != nil { 11 | t.Errorf("Error while generating session. %v", err) 12 | } 13 | 14 | if session.AccessToken == "" { 15 | t.Errorf("Error while fetching access token. %v", err) 16 | } 17 | 18 | } 19 | 20 | func (ts *TestSuite) TestRenewAccessToken(t *testing.T) { 21 | t.Parallel() 22 | session, err := ts.TestConnect.RenewAccessToken("test") 23 | if err != nil { 24 | t.Errorf("Error while regenerating session. %v", err) 25 | } 26 | 27 | if session.AccessToken == "" { 28 | t.Errorf("Error while fetching new access token. %v", err) 29 | } 30 | 31 | } 32 | 33 | func (ts *TestSuite) TestUserProfile(t *testing.T) { 34 | t.Parallel() 35 | session, err := ts.TestConnect.GetUserProfile() 36 | if err != nil { 37 | t.Errorf("Error while fetching user profile. %v", err) 38 | } 39 | 40 | if session.ClientCode == "" { 41 | t.Errorf("Error while fetching client code. %v", err) 42 | } 43 | 44 | } 45 | 46 | func (ts *TestSuite) TestLogout(t *testing.T) { 47 | t.Parallel() 48 | resp, err := ts.TestConnect.Logout() 49 | if err != nil { 50 | t.Errorf("Error while calling log out api. %v", err) 51 | } 52 | 53 | if !resp { 54 | t.Errorf("Error while logging out. %v", err) 55 | } 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package smartapigo 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "net" 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | type Time struct { 14 | time.Time 15 | } 16 | 17 | // API endpoints 18 | const ( 19 | URILogin string = "rest/auth/angelbroking/user/v1/loginByPassword" 20 | URIUserSessionRenew string = "rest/auth/angelbroking/jwt/v1/generateTokens" 21 | URIUserProfile string = "rest/secure/angelbroking/user/v1/getProfile" 22 | URILogout string = "rest/secure/angelbroking/user/v1/logout" 23 | URIGetOrderBook string = "rest/secure/angelbroking/order/v1/getOrderBook" 24 | URIPlaceOrder string = "rest/secure/angelbroking/order/v1/placeOrder" 25 | URIModifyOrder string = "rest/secure/angelbroking/order/v1/modifyOrder" 26 | URICancelOrder string = "rest/secure/angelbroking/order/v1/cancelOrder" 27 | URIGetHoldings string = "rest/secure/angelbroking/portfolio/v1/getHolding" 28 | URIGetPositions string = "rest/secure/angelbroking/order/v1/getPosition" 29 | URIGetTradeBook string = "rest/secure/angelbroking/order/v1/getTradeBook" 30 | URILTP string = "rest/secure/angelbroking/order/v1/getLtpData" 31 | URIRMS string = "rest/secure/angelbroking/user/v1/getRMS" 32 | URIConvertPosition string = "rest/secure/angelbroking/order/v1/convertPosition" 33 | ) 34 | 35 | func structToMap(obj interface{}, tagName string) map[string]interface{} { 36 | var values reflect.Value 37 | switch obj.(type) { 38 | case OrderParams: 39 | { 40 | con := obj.(OrderParams) 41 | values = reflect.ValueOf(&con).Elem() 42 | } 43 | 44 | case ModifyOrderParams: 45 | { 46 | con := obj.(ModifyOrderParams) 47 | values = reflect.ValueOf(&con).Elem() 48 | } 49 | case LTPParams: 50 | { 51 | con := obj.(LTPParams) 52 | values = reflect.ValueOf(&con).Elem() 53 | } 54 | case ConvertPositionParams: 55 | { 56 | con := obj.(ConvertPositionParams) 57 | values = reflect.ValueOf(&con).Elem() 58 | } 59 | } 60 | 61 | tags := reflect.TypeOf(obj) 62 | params := make(map[string]interface{}) 63 | for i := 0; i < values.NumField(); i++ { 64 | params[tags.Field(i).Tag.Get(tagName)] = values.Field(i).Interface() 65 | } 66 | 67 | return params 68 | } 69 | 70 | func getIpAndMac() (string, string, string, error) { 71 | 72 | //---------------------- 73 | // Get the local machine IP address 74 | //---------------------- 75 | 76 | var localIp, currentNetworkHardwareName string 77 | 78 | localIp, err := getLocalIP() 79 | 80 | if err != nil { 81 | return "", "", "", err 82 | } 83 | 84 | // get all the system's or local machine's network interfaces 85 | 86 | interfaces, _ := net.Interfaces() 87 | for _, interf := range interfaces { 88 | 89 | if addrs, err := interf.Addrs(); err == nil { 90 | for _, addr := range addrs { 91 | 92 | // only interested in the name with current IP address 93 | if strings.Contains(addr.String(), localIp) { 94 | currentNetworkHardwareName = interf.Name 95 | } 96 | } 97 | } 98 | } 99 | 100 | // extract the hardware information base on the interface name 101 | // capture above 102 | netInterface, err := net.InterfaceByName(currentNetworkHardwareName) 103 | 104 | if err != nil { 105 | return "", "", "", err 106 | } 107 | 108 | macAddress := netInterface.HardwareAddr 109 | 110 | // verify if the MAC address can be parsed properly 111 | _, err = net.ParseMAC(macAddress.String()) 112 | 113 | if err != nil { 114 | return "", "", "", err 115 | } 116 | 117 | publicIp, err := getPublicIp() 118 | if err != nil { 119 | return "", "", "", err 120 | } 121 | 122 | return localIp, publicIp, macAddress.String(), nil 123 | 124 | } 125 | 126 | func getLocalIP() (string, error) { 127 | ifaces, err := net.Interfaces() 128 | if err != nil { 129 | return "", err 130 | } 131 | for _, iface := range ifaces { 132 | if iface.Flags&net.FlagUp == 0 { 133 | continue // interface down 134 | } 135 | if iface.Flags&net.FlagLoopback != 0 { 136 | continue // loopback interface 137 | } 138 | addrs, err := iface.Addrs() 139 | if err != nil { 140 | return "", err 141 | } 142 | for _, addr := range addrs { 143 | var ip net.IP 144 | switch v := addr.(type) { 145 | case *net.IPNet: 146 | ip = v.IP 147 | case *net.IPAddr: 148 | ip = v.IP 149 | } 150 | if ip == nil || ip.IsLoopback() { 151 | continue 152 | } 153 | ip = ip.To4() 154 | if ip == nil { 155 | continue // not an ipv4 address 156 | } 157 | return ip.String(), nil 158 | } 159 | } 160 | return "", errors.New("please check your network connection") 161 | } 162 | 163 | func getPublicIp() (string, error) { 164 | resp, err := http.Get("https://myexternalip.com/raw") 165 | if err != nil { 166 | return "", err 167 | } 168 | 169 | content, _ := ioutil.ReadAll(resp.Body) 170 | err = resp.Body.Close() 171 | if err != nil { 172 | return "", err 173 | } 174 | return string(content), nil 175 | } 176 | -------------------------------------------------------------------------------- /websocket/websocket.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "bytes" 5 | "compress/zlib" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "github.com/gorilla/websocket" 11 | "io/ioutil" 12 | "math" 13 | "net/url" 14 | "sync" 15 | "time" 16 | ) 17 | 18 | type SocketClient struct { 19 | Conn *websocket.Conn 20 | url url.URL 21 | callbacks callbacks 22 | autoReconnect bool 23 | reconnectMaxRetries int 24 | reconnectMaxDelay time.Duration 25 | connectTimeout time.Duration 26 | reconnectAttempt int 27 | scrips string 28 | feedToken string 29 | clientCode string 30 | } 31 | 32 | // callbacks represents callbacks available in ticker. 33 | type callbacks struct { 34 | onMessage func([]map[string]interface{}) 35 | onNoReconnect func(int) 36 | onReconnect func(int, time.Duration) 37 | onConnect func() 38 | onClose func(int, string) 39 | onError func(error) 40 | } 41 | 42 | const ( 43 | // Auto reconnect defaults 44 | // Default maximum number of reconnect attempts 45 | defaultReconnectMaxAttempts = 300 46 | // Auto reconnect min delay. Reconnect delay can't be less than this. 47 | reconnectMinDelay time.Duration = 5000 * time.Millisecond 48 | // Default auto reconnect delay to be used for auto reconnection. 49 | defaultReconnectMaxDelay time.Duration = 60000 * time.Millisecond 50 | // Connect timeout for initial server handshake. 51 | defaultConnectTimeout time.Duration = 7000 * time.Millisecond 52 | // Interval in which the connection check is performed periodically. 53 | connectionCheckInterval time.Duration = 10000 * time.Millisecond 54 | ) 55 | 56 | var ( 57 | // Default ticker url. 58 | tickerURL = url.URL{Scheme: "wss", Host: "wsfeeds.angelbroking.com", Path: "/NestHtml5Mobile/socket/stream"} 59 | ) 60 | 61 | // New creates a new ticker instance. 62 | func New(clientCode string, feedToken string, scrips string) *SocketClient { 63 | sc := &SocketClient{ 64 | clientCode: clientCode, 65 | feedToken: feedToken, 66 | url: tickerURL, 67 | autoReconnect: true, 68 | reconnectMaxDelay: defaultReconnectMaxDelay, 69 | reconnectMaxRetries: defaultReconnectMaxAttempts, 70 | connectTimeout: defaultConnectTimeout, 71 | scrips: scrips, 72 | } 73 | 74 | return sc 75 | } 76 | 77 | // SetRootURL sets ticker root url. 78 | func (s *SocketClient) SetRootURL(u url.URL) { 79 | s.url = u 80 | } 81 | 82 | // SetAccessToken set access token. 83 | func (s *SocketClient) SetFeedToken(feedToken string) { 84 | s.feedToken = feedToken 85 | } 86 | 87 | // SetConnectTimeout sets default timeout for initial connect handshake 88 | func (s *SocketClient) SetConnectTimeout(val time.Duration) { 89 | s.connectTimeout = val 90 | } 91 | 92 | // SetAutoReconnect enable/disable auto reconnect. 93 | func (s *SocketClient) SetAutoReconnect(val bool) { 94 | s.autoReconnect = val 95 | } 96 | 97 | // SetReconnectMaxDelay sets maximum auto reconnect delay. 98 | func (s *SocketClient) SetReconnectMaxDelay(val time.Duration) error { 99 | if val > reconnectMinDelay { 100 | return fmt.Errorf("ReconnectMaxDelay can't be less than %fms", reconnectMinDelay.Seconds()*1000) 101 | } 102 | 103 | s.reconnectMaxDelay = val 104 | return nil 105 | } 106 | 107 | // SetReconnectMaxRetries sets maximum reconnect attempts. 108 | func (s *SocketClient) SetReconnectMaxRetries(val int) { 109 | s.reconnectMaxRetries = val 110 | } 111 | 112 | // OnConnect callback. 113 | func (s *SocketClient) OnConnect(f func()) { 114 | s.callbacks.onConnect = f 115 | } 116 | 117 | // OnError callback. 118 | func (s *SocketClient) OnError(f func(err error)) { 119 | s.callbacks.onError = f 120 | } 121 | 122 | // OnClose callback. 123 | func (s *SocketClient) OnClose(f func(code int, reason string)) { 124 | s.callbacks.onClose = f 125 | } 126 | 127 | // OnMessage callback. 128 | func (s *SocketClient) OnMessage(f func(message []map[string]interface{})) { 129 | s.callbacks.onMessage = f 130 | } 131 | 132 | // OnReconnect callback. 133 | func (s *SocketClient) OnReconnect(f func(attempt int, delay time.Duration)) { 134 | s.callbacks.onReconnect = f 135 | } 136 | 137 | // OnNoReconnect callback. 138 | func (s *SocketClient) OnNoReconnect(f func(attempt int)) { 139 | s.callbacks.onNoReconnect = f 140 | } 141 | 142 | // Serve starts the connection to ticker server. Since its blocking its recommended to use it in go routine. 143 | func (s *SocketClient) Serve() { 144 | 145 | for { 146 | // If reconnect attempt exceeds max then close the loop 147 | if s.reconnectAttempt > s.reconnectMaxRetries { 148 | s.triggerNoReconnect(s.reconnectAttempt) 149 | return 150 | } 151 | // If its a reconnect then wait exponentially based on reconnect attempt 152 | if s.reconnectAttempt > 0 { 153 | nextDelay := time.Duration(math.Pow(2, float64(s.reconnectAttempt))) * time.Second 154 | if nextDelay > s.reconnectMaxDelay { 155 | nextDelay = s.reconnectMaxDelay 156 | } 157 | 158 | s.triggerReconnect(s.reconnectAttempt, nextDelay) 159 | 160 | // Close the previous connection if exists 161 | if s.Conn != nil { 162 | s.Conn.Close() 163 | } 164 | } 165 | // create a dialer 166 | d := websocket.DefaultDialer 167 | d.HandshakeTimeout = s.connectTimeout 168 | d.TLSClientConfig = &tls.Config{ 169 | InsecureSkipVerify: true, 170 | } 171 | conn, _, err := d.Dial(s.url.String(), nil) 172 | if err != nil { 173 | s.triggerError(err) 174 | // If auto reconnect is enabled then try reconneting else return error 175 | if s.autoReconnect { 176 | s.reconnectAttempt++ 177 | continue 178 | } 179 | return 180 | } 181 | 182 | err = conn.WriteMessage(websocket.TextMessage, []byte(`{"task":"cn","channel":"","token":"`+s.feedToken+`","user": "`+s.clientCode+`","acctid":"`+s.clientCode+`"}`)) 183 | if err != nil { 184 | s.triggerError(err) 185 | return 186 | } 187 | 188 | _, message, err := conn.ReadMessage() 189 | if err != nil { 190 | s.triggerError(err) 191 | return 192 | } 193 | sDec, _ := base64.StdEncoding.DecodeString(string(message)) 194 | val, err := readSegment(sDec) 195 | var result []map[string]interface{} 196 | err = json.Unmarshal(val, &result) 197 | if err != nil { 198 | s.triggerError(err) 199 | return 200 | } 201 | if len(result) == 0 { 202 | s.triggerError(fmt.Errorf("Invalid Message")) 203 | return 204 | } 205 | 206 | if _, ok := result[0]["ak"]; !ok { 207 | s.triggerError(fmt.Errorf("Invalid Message")) 208 | return 209 | } 210 | 211 | if val, ok := result[0]["ak"]; ok { 212 | if val == "nk" { 213 | s.triggerError(fmt.Errorf("Invalid feed token or client code")) 214 | return 215 | } 216 | } 217 | 218 | // Close the connection when its done. 219 | defer s.Conn.Close() 220 | 221 | // Assign the current connection to the instance. 222 | s.Conn = conn 223 | 224 | // Trigger connect callback. 225 | s.triggerConnect() 226 | 227 | // Resubscribe to stored tokens 228 | if s.reconnectAttempt > 0 { 229 | _ = s.Resubscribe() 230 | } 231 | 232 | // Reset auto reconnect vars 233 | s.reconnectAttempt = 0 234 | 235 | // Set on close handler 236 | s.Conn.SetCloseHandler(s.handleClose) 237 | 238 | var wg sync.WaitGroup 239 | Restart := make(chan bool, 1) 240 | // Receive ticker data in a go routine. 241 | wg.Add(1) 242 | go s.readMessage(&wg, Restart) 243 | 244 | // Run watcher to check last ping time and reconnect if required 245 | if s.autoReconnect { 246 | wg.Add(1) 247 | go s.checkConnection(&wg, Restart) 248 | } 249 | 250 | // Wait for go routines to finish before doing next reconnect 251 | wg.Wait() 252 | } 253 | } 254 | 255 | func (s *SocketClient) handleClose(code int, reason string) error { 256 | s.triggerClose(code, reason) 257 | return nil 258 | } 259 | 260 | // Trigger callback methods 261 | func (s *SocketClient) triggerError(err error) { 262 | if s.callbacks.onError != nil { 263 | s.callbacks.onError(err) 264 | } 265 | } 266 | 267 | func (s *SocketClient) triggerClose(code int, reason string) { 268 | if s.callbacks.onClose != nil { 269 | s.callbacks.onClose(code, reason) 270 | } 271 | } 272 | 273 | func (s *SocketClient) triggerConnect() { 274 | if s.callbacks.onConnect != nil { 275 | s.callbacks.onConnect() 276 | } 277 | } 278 | 279 | func (s *SocketClient) triggerReconnect(attempt int, delay time.Duration) { 280 | if s.callbacks.onReconnect != nil { 281 | s.callbacks.onReconnect(attempt, delay) 282 | } 283 | } 284 | 285 | func (s *SocketClient) triggerNoReconnect(attempt int) { 286 | if s.callbacks.onNoReconnect != nil { 287 | s.callbacks.onNoReconnect(attempt) 288 | } 289 | } 290 | 291 | func (s *SocketClient) triggerMessage(message []map[string]interface{}) { 292 | if s.callbacks.onMessage != nil { 293 | s.callbacks.onMessage(message) 294 | } 295 | } 296 | 297 | // Periodically check for last ping time and initiate reconnect if applicable. 298 | func (s *SocketClient) checkConnection(wg *sync.WaitGroup, Restart chan bool) { 299 | defer wg.Done() 300 | switch { 301 | case <-Restart: 302 | return 303 | } 304 | } 305 | 306 | // readMessage reads the data in a loop. 307 | func (s *SocketClient) readMessage(wg *sync.WaitGroup, Restart chan bool) { 308 | defer wg.Done() 309 | for { 310 | _, msg, err := s.Conn.ReadMessage() 311 | if err != nil { 312 | s.triggerError(fmt.Errorf("Error reading data: %v", err)) 313 | Restart <- true 314 | return 315 | } 316 | 317 | sDec, _ := base64.StdEncoding.DecodeString(string(msg)) 318 | val, err := readSegment(sDec) 319 | if err != nil { 320 | s.triggerError(err) 321 | return 322 | } 323 | 324 | var finalMessage []map[string]interface{} 325 | err = json.Unmarshal(val, &finalMessage) 326 | if err != nil { 327 | s.triggerError(err) 328 | return 329 | } 330 | 331 | if len(finalMessage) == 0 { 332 | continue 333 | } 334 | 335 | if val, ok := finalMessage[0]["ak"]; ok { 336 | if val == "nk" { 337 | s.triggerError(fmt.Errorf("Invalid feed token or client code")) 338 | } 339 | continue 340 | } 341 | 342 | // Trigger message. 343 | s.triggerMessage(finalMessage) 344 | 345 | } 346 | } 347 | 348 | // Close tries to close the connection gracefully. If the server doesn't close it 349 | func (s *SocketClient) Close() error { 350 | return s.Conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) 351 | } 352 | 353 | // Subscribe subscribes tick for the given list of tokens. 354 | func (s *SocketClient) Subscribe() error { 355 | err := s.Conn.WriteMessage(websocket.TextMessage, []byte(`{"task":"mw","channel":"`+s.scrips+`","token":"`+s.feedToken+`","user": "`+s.clientCode+`","acctid":"`+s.clientCode+`"}`)) 356 | if err != nil { 357 | s.triggerError(err) 358 | return err 359 | } 360 | 361 | return nil 362 | } 363 | 364 | func (s *SocketClient) Resubscribe() error { 365 | err := s.Subscribe() 366 | return err 367 | } 368 | 369 | func readSegment(data []byte) ([]byte, error) { 370 | b := bytes.NewReader(data) 371 | z, err := zlib.NewReader(b) 372 | if err != nil { 373 | return nil, err 374 | } 375 | defer z.Close() 376 | p, err := ioutil.ReadAll(z) 377 | if err != nil { 378 | return nil, err 379 | } 380 | return p, nil 381 | } 382 | --------------------------------------------------------------------------------