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 "iniengine.h"
33 #include "timesync.h"
34 #include "exchange_bitfinex.h"
35 
Exchange_Bitfinex(const QByteArray & pRestSign,const QByteArray & pRestKey)36 Exchange_Bitfinex::Exchange_Bitfinex(const QByteArray& pRestSign, const QByteArray& pRestKey)
37     : Exchange()
38 {
39     orderBookItemIsDedicatedOrder = true;
40     clearHistoryOnCurrencyChanged = true;
41     isLastTradesTypeSupported = false;
42     calculatingFeeMode = 1;
43     historyLastTimestamp = "0";
44     lastTradesDate = 0;
45     tickerLastDate = 0;
46     isFirstAccInfo = true;
47     lastInfoReceived = false;
48     apiDownCounter = 0;
49     secondPart = 0;
50     baseValues.exchangeName = "Bitfinex";
51 
52     setApiKeySecret(pRestKey, pRestSign);
53 
54     depthAsks = nullptr;
55     depthBids = nullptr;
56     forceDepthLoad = false;
57     julyHttp = nullptr;
58     tickerOnly = false;
59 
60     currencyMapFile = "Bitfinex";
61     baseValues.currentPair.name = "BTC/USD";
62     baseValues.currentPair.setSymbol("BTCUSD");
63     baseValues.currentPair.currRequestPair = "btcusd";
64     baseValues.currentPair.priceDecimals = 5;
65     minimumRequestIntervalAllowed = 500;
66     baseValues.currentPair.priceMin = qPow(0.1, baseValues.currentPair.priceDecimals);
67     baseValues.currentPair.tradeVolumeMin = 0.01;
68     baseValues.currentPair.tradePriceMin = 0.1;
69     defaultCurrencyParams.currADecimals = 8;
70     defaultCurrencyParams.currBDecimals = 5;
71     defaultCurrencyParams.currABalanceDecimals = 8;
72     defaultCurrencyParams.currBBalanceDecimals = 5;
73     defaultCurrencyParams.priceDecimals = 5;
74     defaultCurrencyParams.priceMin = qPow(0.1, baseValues.currentPair.priceDecimals);
75 
76     supportsLoginIndicator = false;
77     supportsAccountVolume = false;
78 
79     privateNonce = (TimeSync::getTimeT() - 1371854884) * 10;
80 
81     connect(this, &Exchange::threadFinished, this, &Exchange_Bitfinex::quitThread, Qt::DirectConnection);
82 }
83 
~Exchange_Bitfinex()84 Exchange_Bitfinex::~Exchange_Bitfinex()
85 {
86 }
87 
quitThread()88 void Exchange_Bitfinex::quitThread()
89 {
90     clearValues();
91 
92 
93         delete depthAsks;
94 
95 
96         delete depthBids;
97 
98 
99         delete julyHttp;
100 }
101 
clearVariables()102 void Exchange_Bitfinex::clearVariables()
103 {
104     historyLastTimestamp = "0";
105     isFirstAccInfo = true;
106     lastTickerHigh = 0.0;
107     lastTickerLow = 0.0;
108     lastTickerSell = 0.0;
109     lastTickerBuy = 0.0;
110     lastTickerVolume = 0.0;
111     lastVolume = 0.0;
112     secondPart = 0;
113     apiDownCounter = 0;
114     lastHistory.clear();
115     lastOrders.clear();
116     reloadDepth();
117     lastInfoReceived = false;
118     tickerLastDate = 0;
119     lastTradesDate = 0;
120     lastTradesDateCache = "0";
121     lastHistoryId = 0LL;
122 }
123 
clearValues()124 void Exchange_Bitfinex::clearValues()
125 {
126     clearVariables();
127 
128     if (julyHttp)
129         julyHttp->clearPendingData();
130 }
131 
secondSlot()132 void Exchange_Bitfinex::secondSlot()
133 {
134     static int sendCounter = 0;
135 
136     switch (sendCounter)
137     {
138     case 0:
139         if (!isReplayPending(103))
140             sendToApi(103, "pubticker/" + baseValues.currentPair.currRequestPair, false, true);
141 
142         break;
143 
144     case 1:
145         if (!isReplayPending(202))
146             sendToApi(202, "balances", true, true);
147 
148         break;
149 
150     case 2:
151         if (!isReplayPending(109))
152             sendToApi(109, "trades/" + baseValues.currentPair.currRequestPair + "?timestamp=" + lastTradesDateCache +
153                       "&limit_trades=200"/*astTradesDateCache*/, false, true);
154 
155         break;
156 
157     case 3:
158         if (!tickerOnly && !isReplayPending(204))
159             sendToApi(204, "orders", true, true);
160 
161         break;
162 
163     case 4:
164         if (isDepthEnabled() && (forceDepthLoad || !isReplayPending(111)))
165         {
166             emit depthRequested();
167             sendToApi(111, "book/" + baseValues.currentPair.currRequestPair + "?limit_bids=" + baseValues.depthCountLimitStr +
168                       "&limit_asks=" + baseValues.depthCountLimitStr, false, true);
169             forceDepthLoad = false;
170         }
171 
172         break;
173 
174     case 5:
175         if (lastHistory.isEmpty())
176         {
177             if (!isReplayPending(208))
178                 sendToApi(208, "mytrades", true, true,
179                           ", \"symbol\": \"" + baseValues.currentPair.currRequestPair + "\", \"timestamp\": " + historyLastTimestamp +
180                           ", \"limit_trades\": 200");
181 
182             if (!isReplayPending(209))
183                 sendToApi(209, "account_infos", true, true);
184         }
185 
186         break;
187 
188     default:
189         break;
190     }
191 
192     if (sendCounter++ >= 5)
193         sendCounter = 0;
194 
195     Exchange::secondSlot();
196 }
197 
isReplayPending(int reqType)198 bool Exchange_Bitfinex::isReplayPending(int reqType)
199 {
200     if (julyHttp == nullptr)
201         return false;
202 
203     return julyHttp->isReqTypePending(reqType);
204 }
205 
getHistory(bool force)206 void Exchange_Bitfinex::getHistory(bool force)
207 {
208     if (tickerOnly)
209         return;
210 
211     if (force)
212         lastHistory.clear();
213 
214     if (!isReplayPending(208))
215         sendToApi(208, "mytrades", true, true,
216                   ", \"symbol\": \"" + baseValues.currentPair.currRequestPair + "\", \"timestamp\": " + historyLastTimestamp +
217                   ", \"limit_trades\": 100");
218 
219     if (!isReplayPending(209))
220         sendToApi(209, "account_infos", true, true);
221 }
222 
buy(const QString & symbol,double apiBtcToBuy,double apiPriceToBuy)223 void Exchange_Bitfinex::buy(const QString& symbol, double apiBtcToBuy, double apiPriceToBuy)
224 {
225     if (tickerOnly)
226         return;
227 
228     CurrencyPairItem pairItem;
229     pairItem = baseValues.currencyPairMap.value(symbol.toUpper(), pairItem);
230 
231     if (pairItem.symbol.isEmpty())
232         return;
233 
234     QByteArray orderType = "limit";
235 
236     if (pairItem.currRequestSecond == "exchange")
237         orderType.prepend("exchange ");
238 
239     QByteArray params = ", \"symbol\": \"" + pairItem.currRequestPair + "\", \"amount\": \"";
240     params += JulyMath::textFromDouble(apiBtcToBuy, pairItem.currADecimals);
241     params += "\", \"price\": \"";
242     params += JulyMath::textFromDouble(apiPriceToBuy, pairItem.priceDecimals);
243     params += "\", \"exchange\": \"all\", \"side\": \"buy\", \"type\": \"" + orderType + "\"";
244 
245     if (debugLevel)
246         logThread->writeLog("Buy: " + params, 1);
247 
248     sendToApi(306, "order/new", true, true, params);
249 }
250 
sell(const QString & symbol,double apiBtcToSell,double apiPriceToSell)251 void Exchange_Bitfinex::sell(const QString& symbol, double apiBtcToSell, double apiPriceToSell)
252 {
253     if (tickerOnly)
254         return;
255 
256     CurrencyPairItem pairItem;
257     pairItem = baseValues.currencyPairMap.value(symbol.toUpper(), pairItem);
258 
259     if (pairItem.symbol.isEmpty())
260         return;
261 
262     QByteArray orderType = "limit";
263 
264     if (pairItem.currRequestSecond == "exchange")
265         orderType.prepend("exchange ");
266 
267     QByteArray params = ", \"symbol\": \"" + pairItem.currRequestPair + "\", \"amount\": \"";
268     params += JulyMath::textFromDouble(apiBtcToSell, pairItem.currADecimals);
269     params += "\", \"price\": \"";
270     params += JulyMath::textFromDouble(apiPriceToSell, pairItem.priceDecimals);
271     params += "\", \"exchange\": \"all\", \"side\": \"sell\", \"type\": \"" + orderType + "\"";
272 
273     if (debugLevel)
274         logThread->writeLog("Sell: " + params, 1);
275 
276     sendToApi(307, "order/new", true, true, params);
277 }
278 
cancelOrder(const QString &,const QByteArray & order)279 void Exchange_Bitfinex::cancelOrder(const QString& /*unused*/, const QByteArray& order)
280 {
281     if (tickerOnly)
282         return;
283 
284     if (debugLevel)
285         logThread->writeLog("Cancel order: " + order, 1);
286 
287     sendToApi(305, "order/cancel", true, true, ", \"order_id\": " + order);
288 }
289 
sendToApi(int reqType,const QByteArray & method,bool auth,bool sendNow,QByteArray commands)290 void Exchange_Bitfinex::sendToApi(int reqType, const QByteArray& method, bool auth, bool sendNow, QByteArray commands)
291 {
292     if (julyHttp == nullptr)
293     {
294         if (domain.isEmpty() || port == 0)
295             julyHttp = new JulyHttp("api.bitfinex.com", "X-BFX-APIKEY: " + getApiKey() + "\r\n", this);
296         else
297         {
298             julyHttp = new JulyHttp(domain, "X-BFX-APIKEY: " + getApiKey() + "\r\n", this, useSsl);
299             julyHttp->setPortForced(port);
300         }
301 
302         connect(julyHttp, SIGNAL(anyDataReceived()), baseValues_->mainWindow_, SLOT(anyDataReceived()));
303         connect(julyHttp, SIGNAL(setDataPending(bool)), baseValues_->mainWindow_, SLOT(setDataPending(bool)));
304         connect(julyHttp, SIGNAL(apiDown(bool)), baseValues_->mainWindow_, SLOT(setApiDown(bool)));
305         connect(julyHttp, SIGNAL(errorSignal(QString)), baseValues_->mainWindow_, SLOT(showErrorMessage(QString)));
306         connect(julyHttp, SIGNAL(sslErrorSignal(const QList<QSslError>&)), this, SLOT(sslErrors(const QList<QSslError>&)));
307         connect(julyHttp, SIGNAL(dataReceived(QByteArray, int, int)), this, SLOT(dataReceivedAuth(const QByteArray&, int, int)));
308     }
309 
310     if (auth)
311     {
312         QByteArray postData = "{\"request\": \"/v1/" + method + "\", \"nonce\": \"" + QByteArray::number(++privateNonce) + "\"";
313         postData.append(commands);
314         postData.append("}");
315         QByteArray payload = postData.toBase64();
316         QByteArray forHash = hmacSha384(getApiSign(), payload).toHex();
317 
318         if (sendNow)
319             julyHttp->sendData(reqType, m_pairChangeCount, "POST /v1/" + method, postData,
320                                "X-BFX-PAYLOAD: " + payload + "\r\nX-BFX-SIGNATURE: " + forHash + "\r\n");
321         else
322             julyHttp->prepareData(reqType, m_pairChangeCount, "POST /v1/" + method, postData,
323                                   "X-BFX-PAYLOAD: " + payload + "\r\nX-BFX-SIGNATURE: " + forHash + "\r\n");
324     }
325     else
326     {
327         if (sendNow)
328             julyHttp->sendData(reqType, m_pairChangeCount, "GET /v1/" + method);
329         else
330             julyHttp->prepareData(reqType, m_pairChangeCount, "GET /v1/" + method);
331     }
332 }
333 
depthUpdateOrder(const QString & symbol,double price,double amount,bool isAsk)334 void Exchange_Bitfinex::depthUpdateOrder(const QString& symbol, double price, double amount, bool isAsk)
335 {
336     if (symbol != baseValues.currentPair.symbol)
337         return;
338 
339     if (isAsk)
340     {
341         if (depthAsks == nullptr)
342             return;
343 
344         DepthItem newItem;
345         newItem.price = price;
346         newItem.volume = amount;
347 
348         if (newItem.isValid())
349             (*depthAsks) << newItem;
350     }
351     else
352     {
353         if (depthBids == nullptr)
354             return;
355 
356         DepthItem newItem;
357         newItem.price = price;
358         newItem.volume = amount;
359 
360         if (newItem.isValid())
361             (*depthBids) << newItem;
362     }
363 }
364 
depthSubmitOrder(const QString & symbol,QMap<double,double> * currentMap,double priceDouble,double amount,bool isAsk)365 void Exchange_Bitfinex::depthSubmitOrder(const QString& symbol, QMap<double, double>* currentMap, double priceDouble,
366         double amount, bool isAsk)
367 {
368     if (symbol != baseValues.currentPair.symbol)
369         return;
370 
371     if (priceDouble == 0.0 || amount == 0.0)
372         return;
373 
374     if (orderBookItemIsDedicatedOrder)
375         amount += currentMap->value(priceDouble, 0.0);
376 
377     if (isAsk)
378     {
379         (*currentMap)[priceDouble] = amount;
380 
381         if (lastDepthAsksMap.value(priceDouble, 0.0) != amount)
382             depthUpdateOrder(symbol, priceDouble, amount, true);
383     }
384     else
385     {
386         (*currentMap)[priceDouble] = amount;
387 
388         if (lastDepthBidsMap.value(priceDouble, 0.0) != amount)
389             depthUpdateOrder(symbol, priceDouble, amount, false);
390     }
391 }
392 
reloadDepth()393 void Exchange_Bitfinex::reloadDepth()
394 {
395     lastDepthBidsMap.clear();
396     lastDepthAsksMap.clear();
397     lastDepthData.clear();
398     Exchange::reloadDepth();
399 }
400 
dataReceivedAuth(const QByteArray & data,int reqType,int pairChangeCount)401 void Exchange_Bitfinex::dataReceivedAuth(const QByteArray& data, int reqType, int pairChangeCount)
402 {
403     if (pairChangeCount != m_pairChangeCount)
404         return;
405 
406     if (debugLevel)
407         logThread->writeLog("RCV: " + data);
408 
409     if (data.size() && data.at(0) == QLatin1Char('<'))
410         return;
411 
412     bool success = (data.startsWith("{") || data.startsWith("[")) && !data.startsWith("{\"message\"");
413 
414     switch (reqType)
415     {
416     case 103: //ticker
417         if (!success)
418             break;
419 
420         if (data.startsWith("{\"mid\":"))
421         {
422             QByteArray tickerSell = getMidData("bid\":\"", "\"", &data);
423 
424             if (!tickerSell.isEmpty())
425             {
426                 double newTickerSell = tickerSell.toDouble();
427 
428                 if (newTickerSell != lastTickerSell)
429                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Sell", newTickerSell);
430 
431                 lastTickerSell = newTickerSell;
432             }
433 
434             QByteArray tickerBuy = getMidData("ask\":\"", "\"", &data);
435 
436             if (!tickerBuy.isEmpty())
437             {
438                 double newTickerBuy = tickerBuy.toDouble();
439 
440                 if (newTickerBuy != lastTickerBuy)
441                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Buy", newTickerBuy);
442 
443                 lastTickerBuy = newTickerBuy;
444             }
445 
446             qint64 tickerNow = getMidData("timestamp\":\"", ".", &data).toUInt();
447 
448             if (tickerLastDate < tickerNow)
449             {
450                 QByteArray tickerLast = getMidData("last_price\":\"", "\"", &data);
451                 double newTickerLast = tickerLast.toDouble();
452 
453                 if (newTickerLast > 0.0)
454                 {
455                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Last", newTickerLast);
456                     tickerLastDate = tickerNow;
457                 }
458             }
459 
460             QByteArray tickerHigh = getMidData("high\":\"", "\"", &data);
461 
462             if (!tickerHigh.isEmpty())
463             {
464                 double newTickerHigh = tickerHigh.toDouble();
465 
466                 if (newTickerHigh != lastTickerHigh)
467                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "High", newTickerHigh);
468 
469                 lastTickerHigh = newTickerHigh;
470             }
471 
472             QByteArray tickerLow = getMidData("\"low\":\"", "\"", &data);
473 
474             if (!tickerLow.isEmpty())
475             {
476                 double newTickerLow = tickerLow.toDouble();
477 
478                 if (newTickerLow != lastTickerLow)
479                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Low", newTickerLow);
480 
481                 lastTickerLow = newTickerLow;
482             }
483 
484             QByteArray tickerVolume = getMidData("\"volume\":\"", "\"", &data);
485 
486             if (!tickerVolume.isEmpty())
487             {
488                 double newTickerVolume = tickerVolume.toDouble();
489 
490                 if (newTickerVolume != lastTickerVolume)
491                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Volume", newTickerVolume);
492 
493                 lastTickerVolume = newTickerVolume;
494             }
495         }
496         else if (debugLevel)
497             logThread->writeLog("Invalid ticker fast data:" + data, 2);
498 
499         break;//ticker
500 
501     case 109: //money/trades/fetch
502         if (success && data.size() > 32)
503         {
504             QStringList tradeList = QString(data).split("},{");
505             auto* newTradesItems = new QList<TradesItem>;
506 
507             for (int n = tradeList.size() - 1; n >= 0; n--)
508             {
509                 QByteArray tradeData = tradeList.at(n).toLatin1();
510                 qint64 currentTradeDate = getMidData("timestamp\":", ",", &tradeData).toLongLong();
511 
512                 if (lastTradesDate >= currentTradeDate || currentTradeDate == 0)
513                     continue;
514 
515                 TradesItem newItem;
516                 newItem.amount = getMidData("\"amount\":\"", "\",", &tradeData).toDouble();
517                 newItem.price = getMidData("\"price\":\"", "\",", &tradeData).toDouble();
518                 newItem.orderType = getMidData("\"type\":\"", "\"", &tradeData) == "sell" ? 1 : -1;
519 
520                 newItem.symbol = baseValues.currentPair.symbol;
521                 newItem.date = currentTradeDate;
522 
523                 if (newItem.isValid())
524                     (*newTradesItems) << newItem;
525                 else if (debugLevel)
526                     logThread->writeLog("Invalid trades fetch data line:" + tradeData, 2);
527 
528                 if (n == 0)
529                 {
530                     IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Last", newItem.price);
531                     tickerLastDate = currentTradeDate;
532                     lastTradesDate = currentTradeDate;
533                     lastTradesDateCache = QByteArray::number(tickerLastDate + 1);
534                 }
535             }
536 
537             if (!newTradesItems->empty())
538                 emit addLastTrades(baseValues.currentPair.symbol, newTradesItems);
539             else
540                 delete newTradesItems;
541         }
542 
543         break;
544 
545     case 111: //depth
546         if (data.startsWith("{\"bids\""))
547         {
548             emit depthRequestReceived();
549 
550             if (lastDepthData != data)
551             {
552                 lastDepthData = data;
553 
554                 depthAsks = new QList<DepthItem>;
555                 depthBids = new QList<DepthItem>;
556 
557                 QMap<double, double> currentAsksMap;
558 
559                 QStringList asksList = QString(getMidData("asks\":[{", "}]", &data)).split("},{");
560                 double groupedPrice = 0.0;
561                 double groupedVolume = 0.0;
562                 int rowCounter = 0;
563 
564                 for (int n = 0; n < asksList.size(); n++)
565                 {
566                     if (baseValues.depthCountLimit && rowCounter >= baseValues.depthCountLimit)
567                         break;
568 
569                     QByteArray currentRow = asksList.at(n).toLatin1();
570                     double priceDouble = getMidData("price\":\"", "\"", &currentRow).toDouble();
571                     double amount = getMidData("amount\":\"", "\"", &currentRow).toDouble();
572 
573                     if (n == 0)
574                         IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Buy", priceDouble);
575 
576                     if (baseValues.groupPriceValue > 0.0)
577                     {
578                         if (n == 0)
579                         {
580                             emit depthFirstOrder(baseValues.currentPair.symbol, priceDouble, amount, true);
581                             groupedPrice = baseValues.groupPriceValue * (int)(priceDouble / baseValues.groupPriceValue);
582                             groupedVolume = amount;
583                         }
584                         else
585                         {
586                             bool matchCurrentGroup = priceDouble < groupedPrice + baseValues.groupPriceValue;
587 
588                             if (matchCurrentGroup)
589                                 groupedVolume += amount;
590 
591                             if (!matchCurrentGroup || n == asksList.size() - 1)
592                             {
593                                 depthSubmitOrder(baseValues.currentPair.symbol,
594                                                  &currentAsksMap, groupedPrice + baseValues.groupPriceValue, groupedVolume, true);
595                                 rowCounter++;
596                                 groupedVolume = amount;
597                                 groupedPrice += baseValues.groupPriceValue;
598                             }
599                         }
600                     }
601                     else
602                     {
603                         depthSubmitOrder(baseValues.currentPair.symbol,
604                                          &currentAsksMap, priceDouble, amount, true);
605                         rowCounter++;
606                     }
607                 }
608 
609                 QList<double> currentAsksList = lastDepthAsksMap.keys();
610 
611                 for (int n = 0; n < currentAsksList.size(); n++)
612                     if (currentAsksMap.value(currentAsksList.at(n), 0) == 0)
613                         depthUpdateOrder(baseValues.currentPair.symbol,
614                                          currentAsksList.at(n), 0.0, true); //Remove price
615 
616                 lastDepthAsksMap = currentAsksMap;
617 
618                 QMap<double, double> currentBidsMap;
619                 QStringList bidsList = QString(getMidData("bids\":[{", "}]", &data)).split("},{");
620                 groupedPrice = 0.0;
621                 groupedVolume = 0.0;
622                 rowCounter = 0;
623 
624                 for (int n = 0; n < bidsList.size(); n++)
625                 {
626                     if (baseValues.depthCountLimit && rowCounter >= baseValues.depthCountLimit)
627                         break;
628 
629                     QByteArray currentRow = bidsList.at(n).toLatin1();
630                     double priceDouble = getMidData("price\":\"", "\"", &currentRow).toDouble();
631                     double amount = getMidData("amount\":\"", "\"", &currentRow).toDouble();
632 
633                     if (n == 0)
634                         IndicatorEngine::setValue(baseValues.exchangeName, baseValues.currentPair.symbol, "Sell", priceDouble);
635 
636                     if (baseValues.groupPriceValue > 0.0)
637                     {
638                         if (n == 0)
639                         {
640                             emit depthFirstOrder(baseValues.currentPair.symbol, priceDouble, amount, false);
641                             groupedPrice = baseValues.groupPriceValue * (int)(priceDouble / baseValues.groupPriceValue);
642                             groupedVolume = amount;
643                         }
644                         else
645                         {
646                             bool matchCurrentGroup = priceDouble > groupedPrice + baseValues.groupPriceValue;
647 
648                             if (matchCurrentGroup)
649                                 groupedVolume += amount;
650 
651                             if (!matchCurrentGroup || n == bidsList.size() - 1)
652                             {
653                                 depthSubmitOrder(baseValues.currentPair.symbol,
654                                                  &currentBidsMap, groupedPrice - baseValues.groupPriceValue, groupedVolume, false);
655                                 rowCounter++;
656                                 groupedVolume = amount;
657                                 groupedPrice -= baseValues.groupPriceValue;
658                             }
659                         }
660                     }
661                     else
662                     {
663                         depthSubmitOrder(baseValues.currentPair.symbol,
664                                          &currentBidsMap, priceDouble, amount, false);
665                         rowCounter++;
666                     }
667                 }
668 
669                 QList<double> currentBidsList = lastDepthBidsMap.keys();
670 
671                 for (int n = 0; n < currentBidsList.size(); n++)
672                     if (currentBidsMap.value(currentBidsList.at(n), 0) == 0)
673                         depthUpdateOrder(baseValues.currentPair.symbol,
674                                          currentBidsList.at(n), 0.0, false); //Remove price
675 
676                 lastDepthBidsMap = currentBidsMap;
677 
678                 for (int n = depthAsks->size() - 1; n > 0; n--)
679                     if (depthAsks->at(n).price == depthAsks->at(n - 1).price)
680                         depthAsks->removeAt(--n);
681 
682                 for (int n = depthBids->size() - 1; n > 0; n--)
683                     if (depthBids->at(n).price == depthBids->at(n - 1).price)
684                         depthBids->removeAt(--n);
685 
686                 emit depthSubmitOrders(baseValues.currentPair.symbol, depthAsks, depthBids);
687                 depthAsks = nullptr;
688                 depthBids = nullptr;
689             }
690         }
691         else if (debugLevel)
692             logThread->writeLog("Invalid depth data:" + data, 2);
693 
694         break;
695 
696     case 202: //info
697         {
698             if (!success)
699                 break;
700 
701             if (data.startsWith("[{\"type\""))
702             {
703                 lastInfoReceived = true;
704 
705                 if (debugLevel)
706                     logThread->writeLog("Info: " + data);
707 
708                 QByteArray btcBalance;
709                 QByteArray usdBalance;
710 
711                 QStringList balances = QString(data).split("},{");
712 
713                 for (int n = 0; n < balances.size(); n++)
714                 {
715                     QByteArray currentBalance = balances.at(n).toLatin1();
716                     QByteArray balanceType = getMidData("type\":\"", "\"", &currentBalance);
717 
718                     if (balanceType != baseValues.currentPair.currRequestSecond)
719                         continue;
720 
721                     QByteArray balanceCurrency = getMidData("currency\":\"", "\"", &currentBalance);
722 
723                     if (btcBalance.isEmpty() && balanceCurrency == baseValues.currentPair.currAStrLow)
724                         btcBalance = getMidData("available\":\"", "\"", &currentBalance);
725 
726                     if (usdBalance.isEmpty() && balanceCurrency == baseValues.currentPair.currBStrLow)
727                     {
728                         usdBalance = getMidData("available\":\"", "\"", &currentBalance);
729                     }
730                 }
731 
732                 if (checkValue(btcBalance, lastBtcBalance))
733                     emit accBtcBalanceChanged(baseValues.currentPair.symbolSecond(), lastBtcBalance);
734 
735                 if (checkValue(usdBalance, lastUsdBalance))
736                     emit accUsdBalanceChanged(baseValues.currentPair.symbolSecond(), lastUsdBalance);
737             }
738             else if (debugLevel)
739                 logThread->writeLog("Invalid Info data:" + data, 2);
740         }
741         break;//info
742 
743     case 204://orders
744         if (!success)
745             break;
746 
747         if (data.size() <= 30)
748         {
749             lastOrders.clear();
750             emit ordersIsEmpty();
751             break;
752         }
753 
754         if (lastOrders != data)
755         {
756             lastOrders = data;
757             QStringList ordersList = QString(data).split("},{");
758             auto* orders = new QList<OrderItem>;
759             QByteArray filterType = "limit";
760 
761             if (baseValues.currentPair.currRequestSecond == "exchange")
762                 filterType.prepend("exchange ");
763 
764             for (int n = 0; n < ordersList.size(); n++)
765             {
766                 if (!ordersList.at(n).contains("\"type\":\"" + filterType + "\""))
767                     continue;
768 
769                 QByteArray currentOrderData = ordersList.at(n).toLatin1();
770                 OrderItem currentOrder;
771                 currentOrder.oid = getMidData("\"id\":", ",", &currentOrderData);
772                 currentOrder.date = getMidData("timestamp\":\"", "\"", &currentOrderData).split('.').first().toUInt();
773                 currentOrder.type = getMidData("side\":\"", "\"", &currentOrderData).toLower() == "sell";
774 
775                 bool isCanceled = getMidData("is_cancelled\":", ",", &currentOrderData) == "true";
776 
777                 //0=Canceled, 1=Open, 2=Pending, 3=Post-Pending
778                 if (isCanceled)
779                     currentOrder.status = 0;
780                 else
781                     currentOrder.status = 1;
782 
783                 currentOrder.amount = getMidData("remaining_amount\":\"", "\"", &currentOrderData).toDouble();
784                 currentOrder.price = getMidData("price\":\"", "\"", &currentOrderData).toDouble();
785                 currentOrder.symbol = IniEngine::getSymbolByRequest(getMidData("symbol\":\"", "\"", &currentOrderData));
786 
787                 if (currentOrder.isValid())
788                     (*orders) << currentOrder;
789             }
790 
791             emit orderBookChanged(baseValues.currentPair.symbol, orders);
792 
793             lastInfoReceived = false;
794         }
795 
796         break;//orders
797 
798     //case 210: //positions
799     //  {
800     //      data="[{\"id\":72119,\"symbol\":\"btcusd\",\"status\":\"ACTIVE\",\"base\":\"804.7899\",\"amount\":\"0.001\",\"timestamp\":\"1389624548.0\",\"swap\":\"0.0\",\"pl\":\"-0.0055969\"},{\"id\":72120,\"symbol\":\"ltcbtc\",\"status\":\"ACTIVE\",\"base\":\"0.02924999\",\"amount\":\"0.001\",\"timestamp\":\"1389624559.0\",\"swap\":\"0.0\",\"pl\":\"-0.00000067280018\"},{\"id\":72122,\"symbol\":\"ltcusd\",\"status\":\"ACTIVE\",\"base\":\"23.23\",\"amount\":\"0.001\",\"timestamp\":\"1389624576.0\",\"swap\":\"0.0\",\"pl\":\"-0.00016465\"}]";
801 
802 
803 
804     //  }//positions
805     case 305: //order/cancel
806         {
807             if (!success)
808                 break;
809 
810             QByteArray oid = getMidData("\"id\":", ",", &data);
811 
812             if (!oid.isEmpty())
813                 emit orderCanceled(baseValues.currentPair.symbol, oid);
814             else if (debugLevel)
815                 logThread->writeLog("Invalid Order/Cancel data:" + data, 2);
816         }
817         break;//order/cancel
818 
819     case 306: //order/buy
820         if (!success || !debugLevel)
821             break;
822 
823         if (data.startsWith("{\"id\""))
824             logThread->writeLog("Buy OK: " + data);
825         else
826             logThread->writeLog("Invalid Order Buy Data:" + data);
827 
828         break;//order/buy
829 
830     case 307: //order/sell
831         if (!success || !debugLevel)
832             break;
833 
834         if (data.startsWith("{\"id\""))
835             logThread->writeLog("Sell OK: " + data);
836         else
837             logThread->writeLog("Invalid Order Sell Data:" + data);
838 
839         break;//order/sell
840 
841     case 208: //money/wallet/history
842         if (!success)
843             break;
844 
845         if (data.startsWith("["))
846         {
847             if (lastHistory != data)
848             {
849                 lastHistory = data;
850 
851                 auto* historyItems = new QList<HistoryItem>;
852                 bool firstTimestampReceived = false;
853                 QStringList dataList = QString(data).split("},{");
854                 qint64 maxId = 0LL;
855 
856                 for (int n = 0; n < dataList.size(); n++)
857                 {
858                     QByteArray curLog(dataList.at(n).toLatin1() + "}");
859                     qint64 currentId = getMidData("tid\":", ",", &curLog).toLongLong();
860 
861                     if (currentId <= lastHistoryId)
862                         break;
863 
864                     if (currentId > maxId)
865                         maxId = currentId;
866 
867                     QByteArray currentTimeStamp = getMidData("\"timestamp\":\"", "\"", &curLog).split('.').first();
868 
869                     if (!firstTimestampReceived && !currentTimeStamp.isEmpty())
870                     {
871                         historyLastTimestamp = currentTimeStamp;
872                         firstTimestampReceived = true;
873                     }
874 
875                     HistoryItem currentHistoryItem;
876                     QByteArray logType = getMidData("\"type\":\"", "\"", &curLog);
877 
878                     if (logType == "Sell")
879                         currentHistoryItem.type = 1;
880                     else if (logType == "Buy")
881                         currentHistoryItem.type = 2;
882                     else if (logType == "fee")
883                         currentHistoryItem.type = 3;
884                     else if (logType == "deposit")
885                         currentHistoryItem.type = 4;
886                     else if (logType == "withdraw")
887                         currentHistoryItem.type = 5;
888 
889                     if (currentHistoryItem.type)
890                     {
891                         currentHistoryItem.price = getMidData("\"price\":\"", "\"", &curLog).toDouble();
892                         currentHistoryItem.volume = getMidData("\"amount\":\"", "\"", &curLog).toDouble();
893                         currentHistoryItem.dateTimeInt = currentTimeStamp.toLongLong();
894                         currentHistoryItem.symbol = baseValues.currentPair.symbol;
895                         currentHistoryItem.currRequestSecond = baseValues.currentPair.currRequestSecond;
896 
897                         if (currentHistoryItem.isValid())
898                         {
899                             currentHistoryItem.description += getMidData("exchange\":\"", "\"", &curLog);
900                             (*historyItems) << currentHistoryItem;
901                         }
902                     }
903                 }
904 
905                 if (maxId > lastHistoryId)
906                     lastHistoryId = maxId;
907 
908                 emit historyChanged(historyItems);
909             }
910         }
911         else if (debugLevel)
912             logThread->writeLog("Invalid History data:" + data.left(200), 2);
913 
914         break;//money/wallet/history
915 
916     case 209: //fee
917         if (!success)
918             break;
919 
920         if (data.startsWith("[{\""))
921         {
922             bool feeInit = false;
923             double newFee(0.0);
924 
925             QStringList feeList = QString(getMidData("[{\"pairs\":\"", "}]}]", &data)).split("},{\"pairs\":\"");
926 
927             for (int n = 0; n < feeList.size(); n++)
928             {
929                 if (!feeList.at(n).startsWith(baseValues.currentPair.currAStr))
930                     continue;
931 
932                 QByteArray currentFeeData = feeList.at(n).toLatin1();
933                 newFee = getMidData("taker_fees\":\"", "\"", &currentFeeData).toDouble();
934 
935                 feeInit = true;
936                 break;
937             }
938 
939             if (!feeInit)
940             {
941                 newFee = getMidData("taker_fees\":\"", "\"", &data).toDouble();
942             }
943 
944             if (!qFuzzyCompare(newFee + 1.0, lastFee + 1.0))
945                 emit accFeeChanged(baseValues.currentPair.symbol, newFee);
946 
947             lastFee = newFee;
948         }
949 
950         break;//fee
951 
952     default:
953         break;
954     }
955 
956     if (reqType >= 200 && reqType < 300)
957     {
958         static int authErrorCount = 0;
959 
960         if (!success)
961         {
962             authErrorCount++;
963 
964             if (authErrorCount > 2)
965             {
966                 QString authErrorString = getMidData("message\":\"", "\"", &data);
967 
968                 if (debugLevel)
969                     logThread->writeLog("API error: " + authErrorString.toLatin1() + " ReqType: " + QByteArray::number(reqType), 2);
970 
971                 if (authErrorString == "Could not find a key matching the given X-BFX-APIKEY.")
972                     authErrorString = julyTr("TRUNAUTHORIZED", "Invalid API key.");
973                 else if (authErrorString == "Nonce is too small.")
974                     authErrorString = julyTr("THIS_PROFILE_ALREADY_USED", "Invalid nonce parameter.");
975 
976                 if (!authErrorString.isEmpty())
977                     emit showErrorMessage(authErrorString);
978             }
979         }
980         else
981             authErrorCount = 0;
982     }
983 
984     static int errorCount = 0;
985 
986     if (!success)
987     {
988         errorCount++;
989 
990         if (errorCount < 3)
991             return;
992 
993         QString errorString = getMidData("\"message\":\"", "\"", &data);
994 
995         if (errorString.isEmpty())
996             errorString = data;
997 
998         if (debugLevel)
999             logThread->writeLog(errorString.toLatin1(), 2);
1000 
1001         if (errorString.isEmpty())
1002             return;
1003 
1004         errorString.append("<br>" + QString::number(reqType));
1005 
1006         if (errorString.contains("X-BFX-SIGNATURE") || errorString.contains("X-BFX-APIKEY"))
1007             errorString.prepend("I:>");
1008 
1009         if (reqType < 300)
1010             emit showErrorMessage(errorString);
1011     }
1012     else
1013         errorCount = 0;
1014 }
1015