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