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