├── LICENSE ├── websocket.go ├── README.md └── bitstamp.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ajph 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 | -------------------------------------------------------------------------------- /websocket.go: -------------------------------------------------------------------------------- 1 | package bitstamp 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "runtime" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var _socketurl string = "wss://ws.pusherapp.com/app/de504dc5763aeef9ff52?protocol=7&client=js&version=2.1.6&flash=false" 13 | 14 | type WebSocket struct { 15 | ws *websocket.Conn 16 | quit chan bool 17 | Stream chan *Event 18 | Errors chan error 19 | } 20 | 21 | type Event struct { 22 | Event string `json:"event"` 23 | Channel string `json:"channel"` 24 | Data interface{} `json:"data"` 25 | } 26 | 27 | func (s *WebSocket) Close() { 28 | s.quit <- true 29 | } 30 | 31 | func (s *WebSocket) Subscribe(channel string) { 32 | a := &Event{ 33 | Event: "pusher:subscribe", 34 | Data: map[string]interface{}{ 35 | "channel": channel, 36 | }, 37 | } 38 | s.ws.WriteJSON(a) 39 | } 40 | 41 | func (s *WebSocket) SendTextMessage(message []byte) { 42 | s.ws.WriteMessage(websocket.TextMessage, message) 43 | } 44 | 45 | func (s *WebSocket) Ping() { 46 | a := &Event{ 47 | Event: "pusher:ping", 48 | } 49 | s.ws.WriteJSON(a) 50 | } 51 | 52 | func (s *WebSocket) Pong() { 53 | a := &Event{ 54 | Event: "pusher:pong", 55 | } 56 | s.ws.WriteJSON(a) 57 | } 58 | 59 | func NewWebSocket(t time.Duration) (*WebSocket, error) { 60 | var err error 61 | s := &WebSocket{ 62 | quit: make(chan bool, 1), 63 | Stream: make(chan *Event), 64 | Errors: make(chan error), 65 | } 66 | 67 | // set up websocket 68 | s.ws, _, err = websocket.DefaultDialer.Dial(_socketurl, nil) 69 | if err != nil { 70 | return nil, fmt.Errorf("error dialing websocket: %s", err) 71 | } 72 | 73 | go func() { 74 | defer s.ws.Close() 75 | for { 76 | runtime.Gosched() 77 | s.ws.SetReadDeadline(time.Now().Add(t)) 78 | select { 79 | case <-s.quit: 80 | return 81 | default: 82 | var message []byte 83 | var err error 84 | _, message, err = s.ws.ReadMessage() 85 | if err != nil { 86 | s.Errors <- err 87 | continue 88 | } 89 | e := &Event{} 90 | err = json.Unmarshal(message, e) 91 | if err != nil { 92 | s.Errors <- err 93 | continue 94 | } 95 | s.Stream <- e 96 | } 97 | } 98 | }() 99 | 100 | return s, nil 101 | } 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | bitstamp-go 2 | =========== 3 | 4 | A client implementation of the Bitstamp API, including websockets, in Golang. 5 | 6 | Example Usage 7 | ----- 8 | 9 | ```go 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "log" 15 | "time" 16 | 17 | "github.com/ajph/bitstamp-go" 18 | ) 19 | 20 | const WS_TIMEOUT = 10 * time.Second 21 | 22 | func handleEvent(e *bitstamp.Event, Ws *bitstamp.WebSocket) { 23 | switch e.Event { 24 | // pusher stuff 25 | case "pusher:connection_established": 26 | log.Println("Connected") 27 | case "pusher_internal:subscription_succeeded": 28 | log.Println("Subscribed") 29 | case "pusher:pong": 30 | // ignore 31 | case "pusher:ping": 32 | Ws.Pong() 33 | 34 | // bitstamp 35 | case "trade": 36 | fmt.Printf("%#v\n", e.Data) 37 | 38 | // other 39 | default: 40 | log.Printf("Unknown event: %#v\n", e) 41 | } 42 | } 43 | 44 | func main() { 45 | 46 | // setup bitstamp api 47 | bitstamp.SetAuth("123456", "key", "secret") 48 | 49 | // get balance 50 | balances, err := bitstamp.AccountBalance() 51 | if err != nil { 52 | fmt.Printf("Can't get balance using bitstamp API: %s\n", err) 53 | return 54 | } 55 | fmt.Println("\nAvailable Balances:") 56 | fmt.Printf("USD %f\n", balances.UsdAvailable) 57 | fmt.Printf("BTC %f\n", balances.BtcAvailable) 58 | fmt.Printf("FEE %f\n\n", balances.BtcUsdFee) 59 | 60 | // attempt to place a buy order 61 | // BuyLimitOrder(pair string, amount float64, price float64, amountPrecision, pricePrecision int) 62 | order, err := bitstamp.BuyLimitOrder("btcusd", 0.5, 600.00, 16, 16) 63 | if err != nil { 64 | log.Printf("Error placing buy order: %s", err) 65 | return 66 | } 67 | fmt.Printf("Place oder %d", order.Id) 68 | 69 | var Ws *bitstamp.WebSocket 70 | // websocket read loop 71 | for { 72 | // connect 73 | log.Println("Dialing...") 74 | var err error 75 | Ws, err = bitstamp.NewWebSocket(WS_TIMEOUT) 76 | if err != nil { 77 | log.Printf("Error connecting: %s", err) 78 | time.Sleep(1 * time.Second) 79 | continue 80 | } 81 | Ws.Subscribe("live_trades") 82 | 83 | // read data 84 | L: 85 | for { 86 | select { 87 | case ev := <-Ws.Stream: 88 | handleEvent(ev, Ws) 89 | 90 | case err := <-Ws.Errors: 91 | log.Printf("Socket error: %s, reconnecting...", err) 92 | Ws.Close() 93 | break L 94 | 95 | case <-time.After(10 * time.Second): 96 | Ws.Ping() 97 | 98 | } 99 | } 100 | } 101 | 102 | } 103 | ``` 104 | 105 | Todo 106 | ---- 107 | - Documentation 108 | - Tests 109 | -------------------------------------------------------------------------------- /bitstamp.go: -------------------------------------------------------------------------------- 1 | package bitstamp 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "net/url" 13 | "reflect" 14 | "strconv" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | var _debug bool = false 20 | 21 | var _cliId, _key, _secret string 22 | 23 | var _url string = "https://www.bitstamp.net/api/v2" 24 | 25 | type ErrorResult struct { 26 | Status string `json:"status,string"` 27 | Reason string `json:"reason,string"` 28 | Code string `json:"code,string"` 29 | } 30 | 31 | type AccountBalanceResult struct { 32 | UsdBalance float64 `json:"usd_balance,string"` 33 | BtcBalance float64 `json:"btc_balance,string"` 34 | EurBalance float64 `json:"eur_balance,string"` 35 | XrpBalance float64 `json:"xrp_balance,string"` 36 | LtcBalance float64 `json:"ltc_balance,string"` 37 | EthBalance float64 `json:"eth_balance,string"` 38 | BchBalance float64 `json:"bch_balance,string"` 39 | UsdReserved float64 `json:"usd_reserved,string"` 40 | BtcReserved float64 `json:"btc_reserved,string"` 41 | EurReserved float64 `json:"eur_reserved,string"` 42 | XrpReserved float64 `json:"xrp_reserved,string"` 43 | LtcReserved float64 `json:"ltc_reserved,string"` 44 | EthReserved float64 `json:"eth_reserved,string"` 45 | BchReserved float64 `json:"bch_reserved,string"` 46 | UsdAvailable float64 `json:"usd_available,string"` 47 | BtcAvailable float64 `json:"btc_available,string"` 48 | EurAvailable float64 `json:"eur_available,string"` 49 | XrpAvailable float64 `json:"xrp_available,string"` 50 | LtcAvailable float64 `json:"ltc_available,string"` 51 | EthAvailable float64 `json:"eth_available,string"` 52 | BchAvailable float64 `json:"bch_available,string"` 53 | BtcUsdFee float64 `json:"btcusd_fee,string"` 54 | BtcEurFee float64 `json:"btceur_fee,string"` 55 | EurUsdFee float64 `json:"eurusd_fee,string"` 56 | XrpUsdFee float64 `json:"xrpusd_fee,string"` 57 | XrpEurFee float64 `json:"xrpeur_fee,string"` 58 | XrpBtcFee float64 `json:"xrpbtc_fee,string"` 59 | LtcUsdFee float64 `json:"ltcusd_fee,string"` 60 | LtcEurFee float64 `json:"ltceur_fee,string"` 61 | LtcBtcFee float64 `json:"ltcbtc_fee,string"` 62 | EthUsdFee float64 `json:"ethusd_fee,string"` 63 | EthEurFee float64 `json:"etheur_fee,string"` 64 | EthBtcFee float64 `json:"ethbtc_fee,string"` 65 | BchUsdFee float64 `json:"bchusd_fee,string"` 66 | BchEurFee float64 `json:"bcheur_fee,string"` 67 | BchBtcFee float64 `json:"bchbtc_fee,string"` 68 | } 69 | 70 | type UserTransactionsResult struct { 71 | Fee float64 `json:"fee"` 72 | Id int64 `json:"id"` 73 | OrderId int64 `json:"order_id"` 74 | Usd float64 `json:"usd"` 75 | Eur float64 `json:"eur"` 76 | Btc float64 `json:"btc"` 77 | Xrp float64 `json:"xrp"` 78 | Eth float64 `json:"eth"` 79 | Ltc float64 `json:"ltc"` 80 | BtcUsd float64 `json:"btc_usd"` 81 | BtcEur float64 `json:"btc_eur"` 82 | EthUsd float64 `json:"eth_usd"` 83 | EthEur float64 `json:"eth_eur"` 84 | XrpUsd float64 `json:"xrp_usd"` 85 | XrpEur float64 `json:"xrp_eur"` 86 | LtcUsd float64 `json:"ltc_usd"` 87 | LtcEur float64 `json:"ltc_eur"` 88 | DateTime string `json:"datetime"` 89 | Type int `json:"type,string"` 90 | } 91 | 92 | type TickerResult struct { 93 | Last float64 `json:"last,string"` 94 | High float64 `json:"high,string"` 95 | Low float64 `json:"low,string"` 96 | Vwap float64 `json:"vwap,string"` 97 | Volume float64 `json:"volume,string"` 98 | Bid float64 `json:"bid,string"` 99 | Ask float64 `json:"ask,string"` 100 | Timestamp string `json:"timestamp"` 101 | Open float64 `json:"open,string"` 102 | } 103 | 104 | type BuyOrderResult struct { 105 | Id int64 `json:"id,string"` 106 | DateTime string `json:"datetime"` 107 | Type int `json:"type,string"` 108 | Price float64 `json:"price,string"` 109 | Amount float64 `json:"amount,string"` 110 | } 111 | 112 | type SellOrderResult struct { 113 | Id int64 `json:"id,string"` 114 | DateTime string `json:"datetime"` 115 | Type int `json:"type,string"` 116 | Price float64 `json:"price,string"` 117 | Amount float64 `json:"amount,string"` 118 | } 119 | 120 | type OrderBookResult struct { 121 | Timestamp string `json:"timestamp"` 122 | Bids []OrderBookItem `json:"bids"` 123 | Asks []OrderBookItem `json:"asks"` 124 | } 125 | 126 | type OrderBookItem struct { 127 | Price float64 128 | Amount float64 129 | } 130 | 131 | type OrderStatusResult struct { 132 | Status string `json:"status"` 133 | } 134 | 135 | type OpenOrder struct { 136 | Id int64 `json:"id,string"` 137 | DateTime string `json:"datetime"` 138 | Type int `json:"type,string"` 139 | Price float64 `json:"price,string"` 140 | Amount float64 `json:"amount,string"` 141 | CurrencyPair string `json:"currency_pair"` 142 | } 143 | 144 | func SetUrl(url string) { 145 | _url = url 146 | } 147 | 148 | func SetAuth(clientId, key, secret string) { 149 | _cliId = clientId 150 | _key = key 151 | _secret = secret 152 | } 153 | 154 | func SetDebug(debug bool) { 155 | _debug = debug 156 | } 157 | 158 | // privateQuery submits an http.Request with key, sig & nonce 159 | func privateQuery(path string, values url.Values, v interface{}) error { 160 | // parse the bitstamp URL 161 | endpoint, err := url.Parse(_url) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | // set the endpoint for this request 167 | endpoint.Path += path 168 | 169 | // add required key, signature & nonce to values 170 | nonce := strconv.FormatInt(time.Now().UnixNano(), 10) 171 | mac := hmac.New(sha256.New, []byte(_secret)) 172 | mac.Write([]byte(nonce + _cliId + _key)) 173 | values.Set("key", _key) 174 | values.Set("signature", strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))) 175 | values.Set("nonce", nonce) 176 | 177 | // encode the url.Values in the body 178 | reqBody := strings.NewReader(values.Encode()) 179 | 180 | // create the request 181 | //log.Println(endpoint.String(), values) 182 | req, err := http.NewRequest("POST", endpoint.String(), reqBody) 183 | if err != nil { 184 | return err 185 | } 186 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 187 | 188 | // submit the http request 189 | r, err := http.DefaultClient.Do(req) 190 | if err != nil { 191 | return err 192 | } 193 | 194 | if r.StatusCode != http.StatusOK { 195 | return fmt.Errorf("Didn't receive 200 OK") 196 | } 197 | 198 | // if no result interface, return 199 | if v == nil { 200 | return nil 201 | } 202 | 203 | // read the body of the http message into a byte array 204 | body, err := ioutil.ReadAll(r.Body) 205 | defer r.Body.Close() 206 | if err != nil { 207 | return err 208 | } 209 | 210 | // is this an error? 211 | if len(body) == 0 { 212 | return fmt.Errorf("Response body 0 length") 213 | } 214 | e := make(map[string]interface{}) 215 | err = json.Unmarshal(body, &e) 216 | if bsEr, ok := e["error"]; ok { 217 | return fmt.Errorf("%v", bsEr) 218 | } 219 | 220 | // Check for status == error 221 | err_result := ErrorResult{} 222 | json.Unmarshal(body, &err_result) 223 | if err_result.Status == "error" { 224 | return fmt.Errorf("%#v", err_result) 225 | } 226 | 227 | if _debug { 228 | log.Println(string(body)) 229 | } 230 | //parse the JSON response into the response object 231 | return json.Unmarshal(body, v) 232 | } 233 | 234 | // UnmarshalJSON takes a json array and converts it into an OrderBookItem. 235 | func (o *OrderBookItem) UnmarshalJSON(data []byte) error { 236 | tmp_struct := struct { 237 | p string 238 | v string 239 | }{} 240 | 241 | err := json.Unmarshal(data, &[]interface{}{&tmp_struct.p, &tmp_struct.v}) 242 | if err != nil { 243 | return err 244 | } 245 | 246 | if o.Price, err = strconv.ParseFloat(tmp_struct.p, 64); err != nil { 247 | return err 248 | } 249 | 250 | if o.Amount, err = strconv.ParseFloat(tmp_struct.v, 64); err != nil { 251 | return err 252 | } 253 | return nil 254 | } 255 | 256 | // bitstamp API is not consistant in its way of sending numeric values of the 257 | // same key, sometimes they're strings, sometimes ints, sometimes floats 258 | func (u *UserTransactionsResult) UnmarshalJSON(data []byte) error { 259 | t := make(map[string]interface{}, 0) 260 | 261 | err := json.Unmarshal(data, &t) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | r := reflect.ValueOf(u) 267 | // loop through Unmashalled k / v's returned from bitstamp and then 268 | // loop through our fields to figure out what kind of type we want 269 | for k, v := range t { 270 | for i := 0; i < r.Elem().NumField(); i++ { 271 | if tag, ok := r.Elem().Type().Field(i).Tag.Lookup("json"); ok { 272 | // tag matches the key 273 | if tag == k { 274 | switch r.Elem().Field(i).Kind() { 275 | // our struct wants a float 276 | case reflect.Float64: 277 | switch v.(type) { 278 | // sent value is a float or an int, both match 279 | case float64, int: 280 | r.Elem().Field(i).SetFloat(v.(float64)) 281 | case string: 282 | f, err := strconv.ParseFloat(v.(string), 64) 283 | if err != nil { 284 | return err 285 | } 286 | r.Elem().Field(i).SetFloat(f) 287 | } 288 | case reflect.Int: 289 | r.Elem().Field(i).SetInt(v.(int64)) 290 | case reflect.String: 291 | r.Elem().Field(i).SetString(v.(string)) 292 | } 293 | } 294 | } 295 | } 296 | } 297 | // int,string will not match (type) 298 | (*u).Type, err = strconv.Atoi(t["type"].(string)) 299 | if err != nil { 300 | return err 301 | } 302 | 303 | return nil 304 | } 305 | 306 | func AccountBalance() (*AccountBalanceResult, error) { 307 | balance := &AccountBalanceResult{} 308 | err := privateQuery("/balance/", url.Values{}, balance) 309 | if err != nil { 310 | return nil, err 311 | } 312 | return balance, nil 313 | } 314 | 315 | func UserTransactions() (*[]UserTransactionsResult, error) { 316 | result := &[]UserTransactionsResult{} 317 | err := privateQuery("/user_transactions/", url.Values{}, result) 318 | if err != nil { 319 | return nil, err 320 | } 321 | return result, nil 322 | } 323 | 324 | func OrderBook(pair string) (*OrderBookResult, error) { 325 | orderBook := &OrderBookResult{} 326 | err := privateQuery("/order_book/"+pair+"/", url.Values{}, orderBook) 327 | if err != nil { 328 | return nil, err 329 | } 330 | return orderBook, nil 331 | } 332 | 333 | func Ticker(pair string) (*TickerResult, error) { 334 | ticker := &TickerResult{} 335 | err := privateQuery("/ticker/"+pair+"/", url.Values{}, ticker) 336 | if err != nil { 337 | return nil, err 338 | } 339 | return ticker, nil 340 | } 341 | 342 | func BuyLimitOrder(pair string, amount float64, price float64, amountPrecision, pricePrecision int) (*BuyOrderResult, error) { 343 | // set params 344 | var v = url.Values{} 345 | v.Add("amount", strconv.FormatFloat(amount, 'f', amountPrecision, 64)) 346 | v.Add("price", strconv.FormatFloat(price, 'f', pricePrecision, 64)) 347 | 348 | // make request 349 | result := &BuyOrderResult{} 350 | err := privateQuery("/buy/"+pair+"/", v, result) 351 | if err != nil { 352 | return nil, err 353 | } 354 | return result, nil 355 | } 356 | 357 | func BuyMarketOrder(pair string, amount float64) (*BuyOrderResult, error) { 358 | // set params 359 | var v = url.Values{} 360 | v.Add("amount", strconv.FormatFloat(amount, 'f', 8, 64)) 361 | 362 | // make request 363 | result := &BuyOrderResult{} 364 | err := privateQuery("/buy/market/"+pair+"/", v, result) 365 | if err != nil { 366 | return nil, err 367 | } 368 | return result, nil 369 | } 370 | 371 | func SellLimitOrder(pair string, amount float64, price float64, amountPrecision, pricePrecision int) (*SellOrderResult, error) { 372 | // set params 373 | var v = url.Values{} 374 | v.Add("amount", strconv.FormatFloat(amount, 'f', amountPrecision, 64)) 375 | v.Add("price", strconv.FormatFloat(price, 'f', pricePrecision, 64)) 376 | 377 | // make request 378 | result := &SellOrderResult{} 379 | err := privateQuery("/sell/"+pair+"/", v, result) 380 | if err != nil { 381 | return nil, err 382 | } 383 | return result, nil 384 | } 385 | 386 | func SellMarketOrder(pair string, amount float64) (*SellOrderResult, error) { 387 | // set params 388 | var v = url.Values{} 389 | v.Add("amount", strconv.FormatFloat(amount, 'f', 8, 64)) 390 | 391 | // make request 392 | result := &SellOrderResult{} 393 | err := privateQuery("/sell/market/"+pair+"/", v, result) 394 | if err != nil { 395 | return nil, err 396 | } 397 | return result, nil 398 | } 399 | 400 | func CancelOrder(orderId int64) { 401 | // set params 402 | var v = url.Values{} 403 | v.Add("id", strconv.FormatInt(orderId, 10)) 404 | 405 | // make request 406 | privateQuery("/cancel_order/", v, nil) 407 | } 408 | 409 | func OpenOrders() (*[]OpenOrder, error) { 410 | // make request 411 | result := &[]OpenOrder{} 412 | err := privateQuery("/open_orders/all/", url.Values{}, result) 413 | if err != nil { 414 | return nil, err 415 | } 416 | return result, nil 417 | } 418 | 419 | func OrderStatus(orderId int64) (*OrderStatusResult, error) { 420 | // set params 421 | var v = url.Values{} 422 | v.Add("id", strconv.FormatInt(orderId, 10)) 423 | 424 | // make request 425 | result := &OrderStatusResult{} 426 | err := privateQuery("/order_status/", v, result) 427 | if err != nil { 428 | return nil, err 429 | } 430 | return result, nil 431 | } 432 | --------------------------------------------------------------------------------