This repository has been archived by the owner on Apr 16, 2021. It is now read-only.
/
client.go
549 lines (448 loc) · 14.3 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
package bittrex
//go:generate go-bindata -pkg internal -nometadata -o ./internal/bindata.go test-fixtures
//go:generate go fmt ./internal/bindata.go
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/carterjones/signalr"
"github.com/carterjones/signalr/hubs"
"github.com/pkg/errors"
)
// Client provides an interface by which people can call the Bittrex API
// endpoints (both the REST and the WebSocket APIs).
type Client struct {
// HTTPClient is optional. It can be used to inject a custom underlying HTTP
// client to perform HTTP operations.
HTTPClient *http.Client
// HTTPTransport is optional. It can be used to inject a custom underlying
// HTTP transport into the underlying HTTP client.
HTTPTransport *http.Transport
// MaxRetries indicates the maximum number of times to attempt an HTTP
// operation before failing.
MaxRetries int
// RetryWaitDuration is used to determine how long to wait between retries
// of various operations.
RetryWaitDuration time.Duration
// A Bittrex-supplied API key and secret.
APIKey string
APISecret string
// The underlying SignalR client.
signalrC *signalr.Client
// The current message ID that is sent to the SignalR client with each
// message.
currentMsgID int
// Started indicates if the underlying SignalR client has been started.
started bool
startedMux sync.Mutex
// tradeHandlers holds all of the registered trade handler functions.
tradeHandlers []TradeHandler
tradeHandlersMux sync.Mutex
// HostAddr address is the address of the Bittrex server providing the service
// we're using. Using this variable allows us to more simply test
// functionality in this package.
HostAddr string
}
// Markets gets the markets that are traded on Bittrex.
func (c *Client) Markets() ([]Market, error) {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
err := rc.doV1_1("public/getmarkets", false)
if err != nil {
err = errors.Wrap(err, "public/getmarkets failed")
return []Market{}, err
}
var ms []Market
err = json.Unmarshal(*rc.res.Result, &ms)
if err != nil {
return []Market{}, errors.Wrap(err, "json unmarshal failed")
}
return ms, nil
}
// Balances gets the balances held for all currencies in the account.
func (c *Client) Balances() ([]Balance, error) {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
err := rc.doV1_1("account/getbalances", true)
if err != nil {
return []Balance{}, errors.Wrap(err, "account/getbalances failed")
}
var bs []Balance
err = json.Unmarshal(*rc.res.Result, &bs)
if err != nil {
return []Balance{}, errors.Wrap(err, "json unmarshal failed")
}
return bs, nil
}
// OrderHistory gets the latest orders made through Bittrex for the user's
// account.
func (c *Client) OrderHistory() ([]Order, error) {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
err := rc.doV1_1("account/getorderhistory", true)
if err != nil {
return []Order{}, errors.Wrap(err, "account/getorderhistory failed")
}
var orders []Order
err = json.Unmarshal(*rc.res.Result, &orders)
if err != nil {
return []Order{}, errors.Wrap(err, "json unmarshal failed")
}
return orders, nil
}
// LimitSell sends a request to create a limit sell order.
func (c *Client) LimitSell(market string, quantity, rate float64) error {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
api := "market/selllimit"
// Prepare the parameters.
rc.params = map[string]string{
"market": market,
"quantity": strconv.FormatFloat(quantity, 'f', 8, 64),
"rate": strconv.FormatFloat(rate, 'f', 8, 64),
}
// If the result is successful, no error will be thrown. The only bit of
// information returned is the uuid of the sell order, which we don't care
// about. Therefore we ignore the response.
err := rc.doV1_1(api, true)
if err != nil {
return errors.Wrap(err, "market/selllimit failed")
}
return nil
}
// LimitBuy sends a request to create a limit buy order.
func (c *Client) LimitBuy(market string, quantity, rate float64) error {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
api := "market/buylimit"
// Prepare the parameters.
rc.params = map[string]string{
"market": market,
"quantity": strconv.FormatFloat(quantity, 'f', 8, 64),
"rate": strconv.FormatFloat(rate, 'f', 8, 64),
}
// If the result is successful, no error will be thrown. The only bit of
// information returned is the uuid of the sell order, which we don't care
// about. Therefore we ignore the response.
err := rc.doV1_1(api, true)
if err != nil {
return errors.Wrap(err, "market/buylimit failed")
}
return nil
}
// Cancel sends a request to cancel an order with the specified UUID.
func (c *Client) Cancel(orderUUID string) error {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
api := "market/cancel"
// Prepare the parameters.
rc.params = map[string]string{
"uuid": orderUUID,
}
// If the result is successful, no error will be thrown. The only bit of
// information returned is the uuid of the sell order, which we don't care
// about. Therefore we ignore the response.
err := rc.doV1_1(api, true)
if err != nil {
return errors.Wrap(err, "market/cancel failed")
}
return nil
}
// Subscribe sends a request to Bittrex to start sending us the market data for
// the indicated market.
func (c *Client) Subscribe(market string, errHandler ErrHandler) error {
err := c.websocketReady(errHandler)
if err != nil {
return errors.Wrap(err, "underlying signalr client is not ready")
}
msgs := []interface{}{market}
msgID := c.currentMsgID
c.currentMsgID++
hcm := hubs.ClientMsg{
H: "corehub",
M: "SubscribeToExchangeDeltas",
A: msgs,
I: msgID,
}
err = c.signalrC.Send(hcm)
if err != nil {
return errors.Wrap(err, "failed to send via signalr client")
}
return nil
}
// Register saves the specified trade handler to a slice of handlers that will
// be run against each incoming trade.
func (c *Client) Register(h TradeHandler) {
c.tradeHandlersMux.Lock()
defer c.tradeHandlersMux.Unlock()
c.tradeHandlers = append(c.tradeHandlers, h)
}
func (c *Client) websocketReady(errHandler ErrHandler) error {
// Return if no SignalR client exists.
if c.signalrC == nil {
return errors.New("underlying signalr client is not initialized")
}
// Protect the started flag.
c.startedMux.Lock()
defer c.startedMux.Unlock()
// Return if the client has already been started.
if c.started {
return nil
}
// Prepare a message channel.
msgs := make(chan signalr.Message)
// Initialize the SignalR client.
msgHandler := func(msg signalr.Message) { msgs <- msg }
err := c.signalrC.Run(msgHandler, signalr.ErrHandler(errHandler))
if err != nil {
return errors.Wrap(err, "failed to start the underlying SignalR client")
}
// Process the messages.
go c.processMessages(msgs, errHandler)
c.started = true
return nil
}
// Ticks gets a little under 10 days of candle data (14365 minutes) at the one
// minute interval. However, it is usually missing the last few minutes, so a
// way to monitor trades (such as the Trades() function) is still required to
// get up-to-the-minute data.
//
// As a general approach when monitoring data for strategic analysis, this
// function should be called once a minute for a few minutes until the
// timestamps from the live trade data (Trades() function) overlap with the data
// returned by this function. You shouldn't have to wait more than about 5-10
// minutes for this to occur; at that point you likely only need to use live
// trade data and perhaps then use this function as the source of truth for
// validating your data.
//
// Intervals provided by the underlying Bittrex API are: day, hour, thirtyMin,
// fiveMin, and oneMin.
func (c *Client) Ticks(market string) ([]Tick, error) {
rc := newRestCall(c.HTTPClient, c.HTTPTransport, c.APIKey, c.APISecret, c.HostAddr)
interval := "oneMin"
// Set the parameters.
rc.params = map[string]string{
"marketName": market,
"tickInterval": interval,
}
// Perform the API call.
err := rc.doV2_0("pub/market/GetTicks")
if err != nil {
return []Tick{}, errors.Wrap(err, "pub/market/GetTicks failed")
}
// Extract the result into a usable variable.
result := rc.res.Result
// Return if no result was returned. This can happen even in successful
// situations. For example, the JSON response may look like this:
//
// {"success":true,"message":"","result":null}
if result == nil {
return []Tick{}, nil
}
// Convert the results.
var ts []Tick
err = json.Unmarshal(*result, &ts)
if err != nil {
return []Tick{}, errors.Wrap(err, "json unmarshal failed")
}
return ts, nil
}
// SetCustomID assigns the specified id to the underlying SignalR client.
func (c *Client) SetCustomID(id string) error {
if c.signalrC == nil {
return errors.New("bittrex client has not been initialized")
}
c.signalrC.CustomID = id
return nil
}
// ProcessCandles monitors the trade data for all subscribed markets and
// produces candle data for the specified interval.
func (c *Client) ProcessCandles(interval time.Duration, candleHandler CandleHandler) {
// Register a handler to funnel each trade to a trades channel.
trades := make(chan Trade)
c.Register(func(t Trade) { trades <- t })
// Create a holding place for the candles for this interval.
candles := make(map[string]Candle)
candlesMux := sync.Mutex{}
// Start a goroutine that updates candle values as each trade comes in.
go func() {
for {
t := <-trades
candlesMux.Lock()
var candle Candle
var ok bool
if candle, ok = candles[t.Market()]; !ok {
// If the candle does not already exist, then create one.
candles[t.Market()] = Candle{
Market: t.Market(),
Open: t.Price,
High: t.Price,
Low: t.Price,
Close: t.Price,
Volume: t.Quantity,
}
// Move to the next trade.
candlesMux.Unlock()
continue
}
// If the candle does exist, then modify it as necessary.
// Set the open value if none is set.
if candle.Open == 0.0 {
candle.Open = t.Price
}
// Set the high value.
if t.Price > candle.High {
candle.High = t.Price
}
// Set the low value.
if t.Price < candle.Low {
candle.Low = t.Price
}
// Set the last price. This will eventually be used as the close
// value.
candle.Close = t.Price
// Increase the volume.
candle.Volume += t.Quantity
// Save the new value.
candles[t.Market()] = candle
candlesMux.Unlock()
}
}()
// Start a goroutine that waits for the specified interval, prints the
// current candle values, and then resets the candle map.
go func() {
for {
// Wait for the specified interval.
<-time.After(interval)
// Save the current time.
now := time.Now()
// Lock the map.
candlesMux.Lock()
// Iterate over the candles for this interval.
for k, v := range candles {
// Set the time to now minus the specified interval. This
// represents when the interval started.
v.Time = now.Add(-1 * interval)
// Process the candle.
go candleHandler(v)
// Update the candle values so they carry over data to the next
// interval.
v.Open = v.Close
v.High = v.Close
v.Low = v.Close
candles[k] = v
}
// Unlock the map.
candlesMux.Unlock()
}
}()
}
// New creates a new Bittrex client.
func New(apiKey, apiSecret string) *Client {
c := new(Client)
// Set the API key and secret.
c.APIKey = apiKey
c.APISecret = apiSecret
// Set the default host address.
c.HostAddr = "https://bittrex.com"
// Set up the underlying SignalR client.
signalrC := signalr.New(
"socket.bittrex.com",
"1.5",
"/signalr",
`[{"name":"c2"}]`,
nil,
)
// Set the user agent to one that looks like a browser.
signalrC.Headers["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
// Set the retry duration default.
signalrC.RetryWaitDuration = 10 * time.Second
// Save the SignalR client.
c.signalrC = signalrC
return c
}
// TradeHandler processes a trade.
type TradeHandler func(t Trade)
// CandleHandler processes a candle.
type CandleHandler func(c Candle)
// ErrHandler processes an error.
type ErrHandler func(err error)
// This processes SignalR messages.
func (c *Client) processMessages(msgs chan signalr.Message, errHandler ErrHandler) {
for msg := range msgs {
if !c.processMessage(msg, errHandler) {
return
}
}
}
// Process a single SignalR message.
func (c *Client) processMessage(msg signalr.Message, errHandler ErrHandler) bool {
// Assume that the message is successfully processed. Prove otherwise.
ok := true
// Within each SignalR message is a slice of Bittrex messages.
for _, bittrexMsg := range msg.M {
// Verify the Bittrex message type is updateExchangeState.
if bittrexMsg.M != "updateExchangeState" {
continue
}
// Process each of the arguments.
for _, arg := range bittrexMsg.A {
ok = c.processBittrexMsgArg(arg, errHandler)
if !ok {
return false
}
}
}
return ok
}
// Process a single argument from a Bittrex message.
func (c *Client) processBittrexMsgArg(arg interface{}, errHandler ErrHandler) bool {
data, err := json.Marshal(arg)
if err != nil {
go errHandler(errors.Wrap(err, "json marshal failed"))
return false
}
var eu exchangeUpdate
err = json.Unmarshal(data, &eu)
if err != nil {
go errHandler(errors.Wrap(err, "json unmarshal failed"))
return false
}
for _, t := range eu.Fills {
marketParts := strings.Split(eu.MarketName, "-")
bc := marketParts[0]
mc := marketParts[1]
tType := InvalidTradeType
switch t.OrderType {
case "BUY":
tType = BuyType
case "SELL":
tType = SellType
default:
go errHandler(errors.Errorf("invalid trade type: %v", t.OrderType))
return false
}
// Parse the time.
var parsedTime time.Time
parsedTime, err = t.time()
if err != nil {
go errHandler(errors.Wrap(err, "time parse error"))
return false
}
// Create a new trade.
t := Trade{
BaseCurrency: bc,
MarketCurrency: mc,
Type: tType,
Price: t.Rate,
Quantity: t.Quantity,
Time: parsedTime,
}
// Process the trade using the trade handlers.
c.tradeHandlersMux.Lock()
for _, h := range c.tradeHandlers {
go h(t)
}
c.tradeHandlersMux.Unlock()
}
// If we made it to this point, then the argument was successfully
// processed.
return true
}