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\":\"", "\"", ¤tRow).toDouble();
571 double amount = getMidData("amount\":\"", "\"", ¤tRow).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 ¤tAsksMap, 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 ¤tAsksMap, 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\":\"", "\"", ¤tRow).toDouble();
631 double amount = getMidData("amount\":\"", "\"", ¤tRow).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 ¤tBidsMap, 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 ¤tBidsMap, 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\":\"", "\"", ¤tBalance);
717
718 if (balanceType != baseValues.currentPair.currRequestSecond)
719 continue;
720
721 QByteArray balanceCurrency = getMidData("currency\":\"", "\"", ¤tBalance);
722
723 if (btcBalance.isEmpty() && balanceCurrency == baseValues.currentPair.currAStrLow)
724 btcBalance = getMidData("available\":\"", "\"", ¤tBalance);
725
726 if (usdBalance.isEmpty() && balanceCurrency == baseValues.currentPair.currBStrLow)
727 {
728 usdBalance = getMidData("available\":\"", "\"", ¤tBalance);
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\":", ",", ¤tOrderData);
772 currentOrder.date = getMidData("timestamp\":\"", "\"", ¤tOrderData).split('.').first().toUInt();
773 currentOrder.type = getMidData("side\":\"", "\"", ¤tOrderData).toLower() == "sell";
774
775 bool isCanceled = getMidData("is_cancelled\":", ",", ¤tOrderData) == "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\":\"", "\"", ¤tOrderData).toDouble();
784 currentOrder.price = getMidData("price\":\"", "\"", ¤tOrderData).toDouble();
785 currentOrder.symbol = IniEngine::getSymbolByRequest(getMidData("symbol\":\"", "\"", ¤tOrderData));
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\":\"", "\"", ¤tFeeData).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