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("\"", "\"", ¤tRow).toDouble();
529 double amount = getMidData(", \"", "\"", ¤tRow).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 ¤tAsksMap, 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 ¤tAsksMap, 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("\"", "\"", ¤tRow).toDouble();
589 double amount = getMidData(", \"", "\"", ¤tRow).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 ¤tBidsMap, 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 ¤tBidsMap, 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\": \"", "\",", ¤tOrderData);
705
706 QByteArray dateTimeData = getMidData("\"datetime\": \"", "\"", ¤tOrderData);
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\": \"", "\",", ¤tOrderData) == "1";
711 currentOrder.status = 1;
712 currentOrder.amount = getMidData("\"amount\": \"", "\"", ¤tOrderData).toDouble();
713 currentOrder.price = getMidData("\"price\": \"", "\"", ¤tOrderData).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