1 //  This file is part of Qt Bitcoin Trader
2 //      https://github.com/JulyIGHOR/QtBitcoinTrader
3 //  Copyright (C) 2013-2021 July Ighor <julyighor@gmail.com>
4 //
5 //  This program is free software: you can redistribute it and/or modify
6 //  it under the terms of the GNU General Public License as published by
7 //  the Free Software Foundation, either version 3 of the License, or
8 //  (at your option) any later version.
9 //
10 //  In addition, as a special exception, the copyright holders give
11 //  permission to link the code of portions of this program with the
12 //  OpenSSL library under certain conditions as described in each
13 //  individual source file, and distribute linked combinations including
14 //  the two.
15 //
16 //  You must obey the GNU General Public License in all respects for all
17 //  of the code used other than OpenSSL. If you modify file(s) with this
18 //  exception, you may extend this exception to your version of the
19 //  file(s), but you are not obligated to do so. If you do not wish to do
20 //  so, delete this exception statement from your version. If you delete
21 //  this exception statement from all source files in the program, then
22 //  also delete it here.
23 //
24 //  This program is distributed in the hope that it will be useful,
25 //  but WITHOUT ANY WARRANTY; without even the implied warranty of
26 //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
27 //  GNU General Public License for more details.
28 //
29 //  You should have received a copy of the GNU General Public License
30 //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
31 
32 #include "timesync.h"
33 #include "exchange_bittrex.h"
34 
Exchange_Bittrex(const QByteArray & pRestSign,const QByteArray & pRestKey)35 Exchange_Bittrex::Exchange_Bittrex(const QByteArray &pRestSign, const QByteArray &pRestKey)
36     : Exchange(),
37       isFirstAccInfo(true),
38       lastTickerTime(),
39       lastTradesId(0),
40       lastHistoryTime(0),
41       privateNonce((TimeSync::getTimeT() - 1371854884) * 10),
42       lastCanceledId(),
43       julyHttp(nullptr),
44       depthAsks(nullptr),
45       depthBids(nullptr),
46       lastDepthAsksMap(),
47       lastDepthBidsMap()
48 {
49     clearOpenOrdersOnCurrencyChanged = true;
50     clearHistoryOnCurrencyChanged = true;
51     calculatingFeeMode = 3;
52     baseValues.exchangeName = "Bittrex";
53     baseValues.currentPair.name = "LTC/BTC";
54     baseValues.currentPair.setSymbol("LTC/BTC");
55     baseValues.currentPair.currRequestPair = "BTC-LTC";
56     baseValues.currentPair.priceDecimals = 3;
57     minimumRequestIntervalAllowed = 500;
58     baseValues.currentPair.priceMin = qPow(0.1, baseValues.currentPair.priceDecimals);
59     baseValues.currentPair.tradeVolumeMin = 0.01;
60     baseValues.currentPair.tradePriceMin = 0.1;
61     forceDepthLoad = false;
62     tickerOnly = false;
63     setApiKeySecret(pRestKey, pRestSign);
64 
65     currencyMapFile = "Bittrex";
66     defaultCurrencyParams.currADecimals = 8;
67     defaultCurrencyParams.currBDecimals = 8;
68     defaultCurrencyParams.currABalanceDecimals = 8;
69     defaultCurrencyParams.currBBalanceDecimals = 8;
70     defaultCurrencyParams.priceDecimals = 3;
71     defaultCurrencyParams.priceMin = qPow(0.1, baseValues.currentPair.priceDecimals);
72 
73     supportsLoginIndicator = false;
74     supportsAccountVolume = false;
75 
76     connect(this, &Exchange::threadFinished, this, &Exchange_Bittrex::quitThread, Qt::DirectConnection);
77 }
78 
~Exchange_Bittrex()79 Exchange_Bittrex::~Exchange_Bittrex()
80 {
81 }
82 
quitThread()83 void Exchange_Bittrex::quitThread()
84 {
85     clearValues();
86 
87 
88         delete depthAsks;
89 
90 
91         delete depthBids;
92 
93 
94         delete julyHttp;
95 }
96 
clearVariables()97 void Exchange_Bittrex::clearVariables()
98 {
99     isFirstAccInfo = true;
100     lastTickerTime.clear();
101     lastTradesId = 0;
102     lastHistoryTime = 0;
103     lastCanceledId.clear();
104     Exchange::clearVariables();
105     lastHistory.clear();
106     lastOrders.clear();
107     reloadDepth();
108     emit accFeeChanged(baseValues.currentPair.symbol, 0.25);
109 }
110 
clearValues()111 void Exchange_Bittrex::clearValues()
112 {
113     clearVariables();
114 
115     if (julyHttp)
116         julyHttp->clearPendingData();
117 }
118 
reloadDepth()119 void Exchange_Bittrex::reloadDepth()
120 {
121     lastDepthBidsMap.clear();
122     lastDepthAsksMap.clear();
123     lastDepthData.clear();
124     Exchange::reloadDepth();
125 }
126 
dataReceivedAuth(const QByteArray & data,int reqType,int pairChangeCount)127 void Exchange_Bittrex::dataReceivedAuth(const QByteArray& data, int reqType, int pairChangeCount)
128 {
129     if (pairChangeCount != m_pairChangeCount)
130         return;
131 
132     if (debugLevel)
133         logThread->writeLog("RCV: " + data);
134 
135     if (data.size() && data.at(0) == QLatin1Char('<'))
136         return;
137 
138     bool success = data.startsWith("{\"success\":true");
139     QString errorString;
140 
141     if (!success)
142     {
143         errorString = getMidData("\"message\":\"", "\"", &data);
144 
145         if (debugLevel)
146             logThread->writeLog("Invalid data:" + data, 2);
147     }
148     else switch (reqType)
149     {
150         case 103: //ticker
151             {
152                 double tickerHigh = getMidData("\"High\":", ",", &data).toDouble();
153 
154                 if (tickerHigh > 0.0 && !qFuzzyCompare(tickerHigh, lastTickerHigh))
155                 {
156                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "High", tickerHigh);
157                     lastTickerHigh = tickerHigh;
158                 }
159 
160                 double tickerLow = getMidData("\"Low\":", ",", &data).toDouble();
161 
162                 if (tickerLow > 0.0 && !qFuzzyCompare(tickerLow, lastTickerLow))
163                 {
164                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Low", tickerLow);
165                     lastTickerLow = tickerLow;
166                 }
167 
168                 double tickerSell = getMidData("\"Bid\":", ",", &data).toDouble();
169 
170                 if (tickerSell > 0.0 && !qFuzzyCompare(tickerSell, lastTickerSell))
171                 {
172                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Sell", tickerSell);
173                     lastTickerSell = tickerSell;
174                 }
175 
176                 double tickerBuy = getMidData("\"Ask\":", ",", &data).toDouble();
177 
178                 if (tickerBuy > 0.0 && !qFuzzyCompare(tickerBuy, lastTickerBuy))
179                 {
180                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Buy", tickerBuy);
181                     lastTickerBuy = tickerBuy;
182                 }
183 
184                 double tickerVolume = getMidData("\"Volume\":", ",", &data).toDouble();
185 
186                 if (tickerVolume > 0.0 && !qFuzzyCompare(tickerVolume, lastTickerVolume))
187                 {
188                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Volume", tickerVolume);
189                     lastTickerVolume = tickerVolume;
190                 }
191 
192                 QByteArray tickerTime = getMidData("\"TimeStamp\":\"", "\"", &data);
193 
194                 if (tickerTime != lastTickerTime)
195                 {
196                     lastTickerTime = tickerTime;
197                     double tickerLastDouble = getMidData("\"Last\":", ",", &data).toDouble();
198 
199                     if (tickerLastDouble > 0.0 && !qFuzzyCompare(tickerLastDouble, lastTickerLast))
200                     {
201                         IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Last", tickerLastDouble);
202                         lastTickerLast = tickerLastDouble;
203                     }
204                 }
205             }
206             break;//ticker
207 
208         case 109: //trades
209             {
210                 qint64 time10Min = TimeSync::getTimeT() - 600;
211                 QStringList tradeList = QString(data).split("},{");
212                 auto* newTradesItems = new QList<TradesItem>;
213 
214                 for (int n = tradeList.size() - 1; n >= 0; --n)
215                 {
216                     QByteArray tradeData = tradeList.at(n).toLatin1();
217                     qint64 currentTid = getMidData("\"Id\":", ",", &tradeData).toLongLong();
218 
219                     if (currentTid <= lastTradesId)
220                         continue;
221 
222                     lastTradesId = currentTid;
223                     TradesItem newItem;
224 
225                     QDateTime date = QDateTime::fromString(getMidData("\"TimeStamp\":\"", "\"", &tradeData), Qt::ISODate);
226                     date.setTimeSpec(Qt::UTC);
227                     newItem.date = date.toSecsSinceEpoch();
228 
229                     if (newItem.date < time10Min)
230                         continue;
231 
232                     newItem.amount = getMidData("\"Quantity\":", ",", &tradeData).toDouble();
233                     newItem.price  = getMidData("\"Price\":",    ",", &tradeData).toDouble();
234                     newItem.symbol = baseValues.currentPair.symbol;
235                     newItem.orderType = getMidData("\"OrderType\":\"", "\"", &tradeData) == "BUY" ? 1 : -1;
236 
237                     if (newItem.isValid())
238                         (*newTradesItems) << newItem;
239                     else if (debugLevel)
240                         logThread->writeLog("Invalid trades fetch data line:" + tradeData, 2);
241                 }
242 
243                 if (!newTradesItems->empty())
244                     emit addLastTrades(baseValues.currentPair.symbol, newTradesItems);
245                 else
246                     delete newTradesItems;
247             }
248             break;//trades
249 
250         case 111: //depth
251             {
252                 emit depthRequestReceived();
253 
254                 if (data != lastDepthData)
255                 {
256                     lastDepthData = data;
257                     depthAsks = new QList<DepthItem>;
258                     depthBids = new QList<DepthItem>;
259 
260                     QMap<double, double> currentAsksMap;
261                     QStringList asksList = QString(getMidData("\"sell\":[{\"Quantity\":", "}]}}", &data)).split("},{\"Quantity\":");
262                     double groupedPrice = 0.0;
263                     double groupedVolume = 0.0;
264                     int rowCounter = 0;
265 
266                     for (int n = 0; n < asksList.size(); n++)
267                     {
268                         if (baseValues.depthCountLimit && rowCounter >= baseValues.depthCountLimit)
269                             break;
270 
271                         QStringList currentPair = asksList.at(n).split(",\"Rate\":");
272 
273                         if (currentPair.size() != 2)
274                             continue;
275 
276                         double priceDouble = currentPair.last().toDouble();
277                         double amount      = currentPair.first().toDouble();
278 
279                         if (baseValues.groupPriceValue > 0.0)
280                         {
281                             if (n == 0)
282                             {
283                                 emit depthFirstOrder(baseValues.currentPair.symbol, priceDouble, amount, true);
284                                 groupedPrice = baseValues.groupPriceValue * static_cast<int>(priceDouble / baseValues.groupPriceValue);
285                                 groupedVolume = amount;
286                             }
287                             else
288                             {
289                                 bool matchCurrentGroup = priceDouble < groupedPrice + baseValues.groupPriceValue;
290 
291                                 if (matchCurrentGroup)
292                                     groupedVolume += amount;
293 
294                                 if (!matchCurrentGroup || n == asksList.size() - 1)
295                                 {
296                                     depthSubmitOrder(baseValues.currentPair.symbol,
297                                                      &currentAsksMap, groupedPrice + baseValues.groupPriceValue, groupedVolume, true);
298                                     rowCounter++;
299                                     groupedVolume = amount;
300                                     groupedPrice += baseValues.groupPriceValue;
301                                 }
302                             }
303                         }
304                         else
305                         {
306                             depthSubmitOrder(baseValues.currentPair.symbol,
307                                              &currentAsksMap, priceDouble, amount, true);
308                             rowCounter++;
309                         }
310                     }
311 
312                     QList<double> currentAsksList = lastDepthAsksMap.keys();
313 
314                     for (int n = 0; n < currentAsksList.size(); n++)
315                         if (qFuzzyIsNull(currentAsksMap.value(currentAsksList.at(n), 0)))
316                             depthUpdateOrder(baseValues.currentPair.symbol,
317                                              currentAsksList.at(n), 0.0, true);
318 
319                     lastDepthAsksMap = currentAsksMap;
320 
321                     QMap<double, double> currentBidsMap;
322                     QStringList bidsList = QString(getMidData("\"buy\":[{\"Quantity\":", "}],", &data)).split("},{\"Quantity\":");
323                     groupedPrice = 0.0;
324                     groupedVolume = 0.0;
325                     rowCounter = 0;
326 
327                     for (int n = 0; n < bidsList.size(); n++)
328                     {
329                         if (baseValues.depthCountLimit && rowCounter >= baseValues.depthCountLimit)
330                             break;
331 
332                         QStringList currentPair = bidsList.at(n).split(",\"Rate\":");
333 
334                         if (currentPair.size() != 2)
335                             continue;
336 
337                         double priceDouble = currentPair.last().toDouble();
338                         double amount      = currentPair.first().toDouble();
339 
340                         if (baseValues.groupPriceValue > 0.0)
341                         {
342                             if (n == 0)
343                             {
344                                 emit depthFirstOrder(baseValues.currentPair.symbol, priceDouble, amount, false);
345                                 groupedPrice = baseValues.groupPriceValue * static_cast<int>(priceDouble / baseValues.groupPriceValue);
346                                 groupedVolume = amount;
347                             }
348                             else
349                             {
350                                 bool matchCurrentGroup = priceDouble > groupedPrice - baseValues.groupPriceValue;
351 
352                                 if (matchCurrentGroup)
353                                     groupedVolume += amount;
354 
355                                 if (!matchCurrentGroup || n == asksList.size() - 1)
356                                 {
357                                     depthSubmitOrder(baseValues.currentPair.symbol,
358                                                      &currentBidsMap, groupedPrice - baseValues.groupPriceValue, groupedVolume, false);
359                                     rowCounter++;
360                                     groupedVolume = amount;
361                                     groupedPrice -= baseValues.groupPriceValue;
362                                 }
363                             }
364                         }
365                         else
366                         {
367                             depthSubmitOrder(baseValues.currentPair.symbol,
368                                              &currentBidsMap, priceDouble, amount, false);
369                             rowCounter++;
370                         }
371                     }
372 
373                     QList<double> currentBidsList = lastDepthBidsMap.keys();
374 
375                     for (int n = 0; n < currentBidsList.size(); n++)
376                         if (qFuzzyIsNull(currentBidsMap.value(currentBidsList.at(n), 0)))
377                             depthUpdateOrder(baseValues.currentPair.symbol,
378                                              currentBidsList.at(n), 0.0, false);
379 
380                     lastDepthBidsMap = currentBidsMap;
381 
382                     emit depthSubmitOrders(baseValues.currentPair.symbol, depthAsks, depthBids);
383                     depthAsks = nullptr;
384                     depthBids = nullptr;
385                 }
386             }
387             break;
388 
389         case 202: //info
390             {
391                 QByteArray btcBalance = getMidData("\"Currency\":\"" + baseValues.currentPair.currAStr + "\"", "}", &data);
392                 QByteArray usdBalance = getMidData("\"Currency\":\"" + baseValues.currentPair.currBStr + "\"", "}", &data);
393                 btcBalance = getMidData("\"Available\":", ",", &btcBalance);
394                 usdBalance = getMidData("\"Available\":", ",", &usdBalance);
395 
396                 if (checkValue(btcBalance, lastBtcBalance))
397                     emit accBtcBalanceChanged(baseValues.currentPair.symbol, lastBtcBalance);
398 
399                 if (checkValue(usdBalance, lastUsdBalance))
400                     emit accUsdBalanceChanged(baseValues.currentPair.symbol, lastUsdBalance);
401             }
402             break;//info
403 
404         case 204://orders
405             {
406                 if (lastOrders != data)
407                 {
408                     lastOrders = data;
409 
410                     if (data == "{\"success\":true,\"message\":\"\",\"result\":[]}")
411                     {
412                         emit ordersIsEmpty();
413                         break;
414                     }
415 
416                     QStringList ordersList = QString(data).split("},{");
417                     auto* orders = new QList<OrderItem>;
418 
419                     for (int n = 0; n < ordersList.size(); ++n)
420                     {
421                         OrderItem currentOrder;
422                         QByteArray currentOrderData = ordersList.at(n).toLatin1();
423 
424                         //0=Canceled, 1=Open, 2=Pending, 3=Post-Pending
425                         if (currentOrderData.contains("\"Closed\":null"))
426                             currentOrder.status = 1;
427                         else
428                             currentOrder.status = 0;
429 
430                         QDateTime date = QDateTime::fromString(getMidData("\"Opened\":\"", "\"", &currentOrderData), Qt::ISODate);
431                         date.setTimeSpec(Qt::UTC);
432                         currentOrder.date   = date.toSecsSinceEpoch();
433                         currentOrder.oid    = getMidData("\"OrderUuid\":\"", "\"", &currentOrderData);
434                         currentOrder.type   = getMidData("\"OrderType\":\"", "\"", &currentOrderData) == "LIMIT_SELL";
435                         currentOrder.amount = getMidData("\"Quantity\":",    ",",  &currentOrderData).toDouble();
436                         currentOrder.price  = getMidData("\"Limit\":",       ",",  &currentOrderData).toDouble();
437                         QList<QByteArray> p = getMidData("\"Exchange\":\"",  "\"", &currentOrderData).split('-');
438                         currentOrder.symbol = p.last() + '/' + p.first();
439 
440                         if (currentOrder.isValid())
441                             (*orders) << currentOrder;
442                     }
443 
444                     if (!orders->empty())
445                         emit orderBookChanged(baseValues.currentPair.symbol, orders);
446                     else
447                         delete orders;
448                 }
449 
450                 break;//orders
451             }
452 
453         case 305: //order/cancel
454             if (!lastCanceledId.isEmpty())
455             {
456                 emit orderCanceled(baseValues.currentPair.symbol, lastCanceledId);
457                 lastCanceledId.clear();
458             }
459 
460             break;//order/cancel
461 
462         case 306:
463             if (debugLevel)
464                 logThread->writeLog("Buy OK: " + data, 2);
465 
466             break;//order/buy
467 
468         case 307:
469             if (debugLevel)
470                 logThread->writeLog("Sell OK: " + data, 2);
471 
472             break;//order/sell
473 
474         case 208: //history
475             {
476                 if (data.size() < 50)
477                     break;
478 
479                 if (lastHistory != data)
480                 {
481                     lastHistory = data;
482 
483                     QStringList historyList = QString(data).split("},{");
484                     qint64 maxTime = 0;
485                     auto* historyItems = new QList<HistoryItem>;
486 
487                     for (int n = 0; n < historyList.size(); ++n)
488                     {
489                         QByteArray logData(historyList.at(n).toLatin1());
490 
491                         QDateTime date = QDateTime::fromString(getMidData("\"Closed\":\"", "\"", &logData), Qt::ISODate);
492                         date.setTimeSpec(Qt::UTC);
493                         qint64 dateInt = date.toMSecsSinceEpoch();
494 
495                         if (dateInt <= lastHistoryTime)
496                             break;
497 
498                         if (dateInt > maxTime)
499                             maxTime = dateInt;
500 
501                         HistoryItem currentHistoryItem;
502 
503                         if (getMidData("\"OrderType\":\"", "\"", &logData) == "LIMIT_SELL")
504                             currentHistoryItem.type = 1;
505                         else
506                             currentHistoryItem.type = 2;
507 
508                         QList<QByteArray> pair         = getMidData("\"Exchange\":\"",   "\"", &logData).split('-');
509                         currentHistoryItem.symbol      = pair.last() + '/' + pair.first();
510                         currentHistoryItem.price       = getMidData("\"PricePerUnit\":", ",",  &logData).toDouble();
511                         currentHistoryItem.volume      = getMidData("\"Quantity\":",     ",",  &logData).toDouble();
512                         currentHistoryItem.dateTimeInt = dateInt / 1000;
513 
514                         if (currentHistoryItem.isValid())
515                             (*historyItems) << currentHistoryItem;
516                     }
517 
518                     if (maxTime > lastHistoryTime)
519                         lastHistoryTime = maxTime;
520 
521                     emit historyChanged(historyItems);
522                 }
523 
524                 break;//money/wallet/history
525             }
526 
527         default:
528             break;
529     }
530 
531     if (reqType >= 200 && reqType < 300)
532     {
533         static int authErrorCount = 0;
534 
535         if (!success)
536         {
537             authErrorCount++;
538 
539             if (authErrorCount < 3)
540                 return;
541 
542             if (debugLevel)
543                 logThread->writeLog("API error: " + errorString.toLatin1() + " ReqType: " + QByteArray::number(reqType), 2);
544 
545             if (!errorString.isEmpty())
546                 emit showErrorMessage(errorString);
547         }
548         else
549             authErrorCount = 0;
550     }
551     else if (reqType < 200)
552     {
553         static int errorCount = 0;
554 
555         if (!success)
556         {
557             errorCount++;
558 
559             if (errorCount < 3)
560                 return;
561 
562             if (debugLevel)
563                 logThread->writeLog("API error: " + errorString.toLatin1() + " ReqType: " + QByteArray::number(reqType), 2);
564 
565             if (!errorString.isEmpty())
566                 emit showErrorMessage("I:>" + errorString);
567         }
568         else
569             errorCount = 0;
570     }
571 }
572 
depthUpdateOrder(const QString & symbol,double price,double amount,bool isAsk)573 void Exchange_Bittrex::depthUpdateOrder(const QString& symbol, double price, double amount, bool isAsk)
574 {
575     if (symbol != baseValues.currentPair.symbol)
576         return;
577 
578     if (isAsk)
579     {
580         if (depthAsks == nullptr)
581             return;
582 
583         DepthItem newItem;
584         newItem.price = price;
585         newItem.volume = amount;
586 
587         if (newItem.isValid())
588             (*depthAsks) << newItem;
589     }
590     else
591     {
592         if (depthBids == nullptr)
593             return;
594 
595         DepthItem newItem;
596         newItem.price = price;
597         newItem.volume = amount;
598 
599         if (newItem.isValid())
600             (*depthBids) << newItem;
601     }
602 }
603 
depthSubmitOrder(const QString & symbol,QMap<double,double> * currentMap,double priceDouble,double amount,bool isAsk)604 void Exchange_Bittrex::depthSubmitOrder(const QString& symbol, QMap<double, double>* currentMap, double priceDouble,
605                                     double amount, bool isAsk)
606 {
607     if (symbol != baseValues.currentPair.symbol)
608         return;
609 
610     if (priceDouble == 0.0 || amount == 0.0)
611         return;
612 
613     if (isAsk)
614     {
615         (*currentMap)[priceDouble] = amount;
616 
617         if (!qFuzzyCompare(lastDepthAsksMap.value(priceDouble, 0.0), amount))
618             depthUpdateOrder(symbol, priceDouble, amount, true);
619     }
620     else
621     {
622         (*currentMap)[priceDouble] = amount;
623 
624         if (!qFuzzyCompare(lastDepthBidsMap.value(priceDouble, 0.0), amount))
625             depthUpdateOrder(symbol, priceDouble, amount, false);
626     }
627 }
628 
isReplayPending(int reqType)629 bool Exchange_Bittrex::isReplayPending(int reqType)
630 {
631     if (julyHttp == nullptr)
632         return false;
633 
634     return julyHttp->isReqTypePending(reqType);
635 }
636 
secondSlot()637 void Exchange_Bittrex::secondSlot()
638 {
639     static int sendCounter = 0;
640 
641     switch (sendCounter)
642     {
643         case 0:
644             if (!isReplayPending(103))
645                 sendToApi(103, "getmarketsummary?market=" + baseValues.currentPair.currRequestPair);
646 
647             break;
648 
649         case 1:
650             if (!isReplayPending(202))
651                 sendToApi(202, "account/getbalances?", true);
652 
653             break;
654 
655         case 2:
656             if (!isReplayPending(109))
657                 sendToApi(109, "getmarkethistory?market=" + baseValues.currentPair.currRequestPair);
658 
659             break;
660 
661         case 3:
662             if (!tickerOnly && !isReplayPending(204))
663                 sendToApi(204, "market/getopenorders?market=" + baseValues.currentPair.currRequestPair + "&", true);
664 
665             break;
666 
667         case 4:
668             if (isDepthEnabled() && (forceDepthLoad || !isReplayPending(111)))
669             {
670                 emit depthRequested();
671                 sendToApi(111, "getorderbook?type=both&market=" + baseValues.currentPair.currRequestPair);
672                 forceDepthLoad = false;
673             }
674 
675             break;
676 
677         case 5:
678             if (lastHistory.isEmpty())
679                 getHistory(false);
680 
681             break;
682 
683         default:
684             break;
685     }
686 
687     if (sendCounter++ >= 5)
688         sendCounter = 0;
689 
690     Exchange::secondSlot();
691 }
692 
getHistory(bool force)693 void Exchange_Bittrex::getHistory(bool force)
694 {
695     if (tickerOnly)
696         return;
697 
698     if (force)
699         lastHistory.clear();
700 
701     if (!isReplayPending(208))
702         sendToApi(208, "account/getorderhistory?market=" + baseValues.currentPair.currRequestPair + "&", true);
703 }
704 
buy(const QString & symbol,double apiBtcToBuy,double apiPriceToBuy)705 void Exchange_Bittrex::buy(const QString& symbol, double apiBtcToBuy, double apiPriceToBuy)
706 {
707     if (tickerOnly)
708         return;
709 
710     CurrencyPairItem pairItem;
711     pairItem = baseValues.currencyPairMap.value(symbol, pairItem);
712 
713     if (pairItem.symbol.isEmpty())
714         return;
715 
716     QByteArray data = "market=" + pairItem.currRequestPair + "&quantity=" +
717             JulyMath::byteArrayFromDouble(apiBtcToBuy, pairItem.currADecimals, 0) + "&rate=" +
718             JulyMath::byteArrayFromDouble(apiPriceToBuy, pairItem.priceDecimals, 0) + "&";
719 
720     if (debugLevel)
721         logThread->writeLog("Buy: " + data, 2);
722 
723     sendToApi(306, "market/buylimit?" + data, true);
724 }
725 
sell(const QString & symbol,double apiBtcToSell,double apiPriceToSell)726 void Exchange_Bittrex::sell(const QString& symbol, double apiBtcToSell, double apiPriceToSell)
727 {
728     if (tickerOnly)
729         return;
730 
731     CurrencyPairItem pairItem;
732     pairItem = baseValues.currencyPairMap.value(symbol, pairItem);
733 
734     if (pairItem.symbol.isEmpty())
735         return;
736 
737     QByteArray data = "market=" + pairItem.currRequestPair + "&quantity=" +
738             JulyMath::byteArrayFromDouble(apiBtcToSell, pairItem.currADecimals, 0) + "&rate=" +
739             JulyMath::byteArrayFromDouble(apiPriceToSell, pairItem.priceDecimals, 0) + "&";
740 
741     if (debugLevel)
742         logThread->writeLog("Sell: " + data, 2);
743 
744     sendToApi(307, "market/selllimit?" + data, true);
745 }
746 
cancelOrder(const QString &,const QByteArray & order)747 void Exchange_Bittrex::cancelOrder(const QString& /*unused*/, const QByteArray& order)
748 {
749     if (tickerOnly)
750         return;
751 
752     lastCanceledId  = order;
753     QByteArray data = "uuid=" + order + "&";
754 
755     if (debugLevel)
756         logThread->writeLog("Cancel order: " + data, 2);
757 
758     sendToApi(305, "market/cancel?" + data, true);
759 }
760 
sendToApi(int reqType,const QByteArray & method,bool auth)761 void Exchange_Bittrex::sendToApi(int reqType, const QByteArray &method, bool auth)
762 {
763     if (julyHttp == nullptr)
764     {
765         if (domain.isEmpty() || port == 0)
766             julyHttp = new JulyHttp("bittrex.com", "apisign:", this);
767         else
768         {
769             julyHttp = new JulyHttp(domain, "apisign:", this, useSsl);
770             julyHttp->setPortForced(port);
771         }
772 
773         connect(julyHttp, SIGNAL(anyDataReceived()), baseValues_->mainWindow_, SLOT(anyDataReceived()));
774         connect(julyHttp, SIGNAL(apiDown(bool)), baseValues_->mainWindow_, SLOT(setApiDown(bool)));
775         connect(julyHttp, SIGNAL(setDataPending(bool)), baseValues_->mainWindow_, SLOT(setDataPending(bool)));
776         connect(julyHttp, SIGNAL(errorSignal(QString)), baseValues_->mainWindow_, SLOT(showErrorMessage(QString)));
777         connect(julyHttp, SIGNAL(sslErrorSignal(const QList<QSslError>&)), this, SLOT(sslErrors(const QList<QSslError>&)));
778         connect(julyHttp, SIGNAL(dataReceived(QByteArray, int, int)), this, SLOT(dataReceivedAuth(const QByteArray&, int, int)));
779     }
780 
781     if (auth)
782     {
783         QByteArray path = "/api/v1.1/" + method + "apikey=" + getApiKey() + "&nonce=" + QByteArray::number(++privateNonce);
784         QByteArray url  = "https://bittrex.com" + path;
785         QByteArray sign = hmacSha512(getApiSign(), url).toHex();
786 
787         julyHttp->sendData(reqType, m_pairChangeCount, "GET " + path, "", sign + "\r\n\r\n");
788     }
789     else
790     {
791         julyHttp->sendData(reqType, m_pairChangeCount, "GET /api/v1.1/public/" + method);
792     }
793 }
794 
sslErrors(const QList<QSslError> & errors)795 void Exchange_Bittrex::sslErrors(const QList<QSslError>& errors)
796 {
797     QStringList errorList;
798 
799     for (int n = 0; n < errors.size(); n++)
800         errorList << errors.at(n).errorString();
801 
802     if (debugLevel)
803         logThread->writeLog(errorList.join(" ").toLatin1(), 2);
804 
805     emit showErrorMessage("SSL Error: " + errorList.join(" "));
806 }
807