diff --git a/src/api/common/include/exchangeprivateapi.hpp b/src/api/common/include/exchangeprivateapi.hpp index 5dca9090..7ca6966d 100644 --- a/src/api/common/include/exchangeprivateapi.hpp +++ b/src/api/common/include/exchangeprivateapi.hpp @@ -156,7 +156,7 @@ class ExchangePrivate : public ExchangeBase { virtual ReceivedWithdrawInfo isWithdrawReceived(const InitiatedWithdrawInfo &initiatedWithdrawInfo, const SentWithdrawInfo &sentWithdrawInfo); - TradedAmounts marketTrade(MonetaryAmount from, const TradeOptions &options, Market mk); + TradedAmounts marketTrade(MonetaryAmount from, const TradeOptions &tradeOptions, Market mk); ExchangePublic &_exchangePublic; CachedResultVault &_cachedResultVault{_exchangePublic._cachedResultVault}; diff --git a/src/api/common/src/exchangeprivateapi.cpp b/src/api/common/src/exchangeprivateapi.cpp index 877f5123..1b2628ac 100644 --- a/src/api/common/src/exchangeprivateapi.cpp +++ b/src/api/common/src/exchangeprivateapi.cpp @@ -80,115 +80,116 @@ TradedAmounts ExchangePrivate::trade(MonetaryAmount from, CurrencyCode toCurrenc return tradedAmounts; } -TradedAmounts ExchangePrivate::marketTrade(MonetaryAmount from, const TradeOptions &options, Market mk) { +TradedAmounts ExchangePrivate::marketTrade(MonetaryAmount from, const TradeOptions &tradeOptions, Market mk) { const CurrencyCode fromCurrency = from.currencyCode(); - - std::optional optPrice = _exchangePublic.computeAvgOrderPrice(mk, from, options.priceOptions()); const CurrencyCode toCurrency = mk.opposite(fromCurrency); - TradedAmounts totalTradedAmounts(fromCurrency, toCurrency); - if (!optPrice) { - log::error("Impossible to compute {} average price on {}", _exchangePublic.name(), mk); - return totalTradedAmounts; - } - MonetaryAmount price = *optPrice; const TimePoint timerStart = Clock::now(); const UserRefInt userRef = static_cast(TimestampToS(timerStart) % static_cast(std::numeric_limits::max())); - const bool noEmergencyTime = options.maxTradeTime() == Duration::max(); const TradeSide side = fromCurrency == mk.base() ? TradeSide::kSell : TradeSide::kBuy; TradeContext tradeContext(mk, side, userRef); - TradeInfo tradeInfo(tradeContext, options); - PlaceOrderInfo placeOrderInfo = placeOrderProcess(from, price, tradeInfo); + TradeInfo tradeInfo(tradeContext, tradeOptions); + TradeOptions &options = tradeInfo.options; + const bool placeSimulatedRealOrder = exchangeInfo().placeSimulateRealOrder(); - if (placeOrderInfo.isClosed()) { - log::debug("Order {} closed with traded amounts {}", placeOrderInfo.orderId, placeOrderInfo.tradedAmounts()); - return placeOrderInfo.tradedAmounts(); - } + enum class NextAction : int8_t { kPlaceInitialOrder, kPlaceLimitOrder, kPlaceMarketOrder, kWait }; - TimePoint lastPriceUpdateTime = Clock::now(); - MonetaryAmount lastPrice = price; + TimePoint lastPriceUpdateTime; + MonetaryAmount price; + MonetaryAmount lastPrice; + + OrderId orderId; + + TradedAmounts totalTradedAmounts(fromCurrency, toCurrency); + + NextAction nextAction = NextAction::kPlaceInitialOrder; while (true) { - OrderInfo orderInfo = queryOrderInfo(placeOrderInfo.orderId, tradeContext); + switch (nextAction) { + case NextAction::kWait: + // Do nothing + break; + case NextAction::kPlaceMarketOrder: + options.switchToTakerStrategy(); + [[fallthrough]]; + case NextAction::kPlaceInitialOrder: { + std::optional optAvgPrice = + _exchangePublic.computeAvgOrderPrice(mk, from, options.priceOptions()); + if (!optAvgPrice) { + log::error("Impossible to compute {} average price on {}", _exchangePublic.name(), mk); + // It's fine to return from there as we don't have a pending order still opened + return totalTradedAmounts; + } + price = *optAvgPrice; + [[fallthrough]]; + } + case NextAction::kPlaceLimitOrder: + [[fallthrough]]; + default: { + PlaceOrderInfo placeOrderInfo = placeOrderProcess(from, price, tradeInfo); + + orderId = std::move(placeOrderInfo.orderId); + + if (placeOrderInfo.isClosed()) { + totalTradedAmounts += placeOrderInfo.tradedAmounts(); + log::debug("Order {} closed with last traded amounts {}", orderId, placeOrderInfo.tradedAmounts()); + return totalTradedAmounts; + } + + lastPrice = price; + lastPriceUpdateTime = Clock::now(); + nextAction = NextAction::kWait; + break; + } + } + + OrderInfo orderInfo = queryOrderInfo(orderId, tradeContext); if (orderInfo.isClosed) { totalTradedAmounts += orderInfo.tradedAmounts; - log::debug("Order {} closed with last traded amounts {}", placeOrderInfo.orderId, orderInfo.tradedAmounts); + log::debug("Order {} closed with last traded amounts {}", orderId, orderInfo.tradedAmounts); break; } - enum class NextAction : int8_t { kPlaceMarketOrder, kNewOrderLimitPrice, kWait }; - NextAction nextAction = NextAction::kWait; - TimePoint nowTime = Clock::now(); - const bool reachedEmergencyTime = - !noEmergencyTime && timerStart + options.maxTradeTime() < nowTime + std::chrono::seconds(1); + const bool reachedEmergencyTime = options.maxTradeTime() < TimeInS(1) + nowTime - timerStart; bool updatePriceNeeded = false; if (!options.isFixedPrice() && !reachedEmergencyTime && - lastPriceUpdateTime + options.minTimeBetweenPriceUpdates() < nowTime) { + options.minTimeBetweenPriceUpdates() < nowTime - lastPriceUpdateTime) { // Let's see if we need to change the price if limit price has changed. - optPrice = _exchangePublic.computeLimitOrderPrice(mk, fromCurrency, options.priceOptions()); - if (optPrice) { - price = *optPrice; + std::optional optLimitPrice = + _exchangePublic.computeLimitOrderPrice(mk, fromCurrency, options.priceOptions()); + if (optLimitPrice) { + price = *optLimitPrice; updatePriceNeeded = (side == TradeSide::kSell && price < lastPrice) || (side == TradeSide::kBuy && price > lastPrice); } } if (reachedEmergencyTime || updatePriceNeeded) { - // Cancel - log::debug("Cancel order {}", placeOrderInfo.orderId); - OrderInfo cancelledOrderInfo = cancelOrder(placeOrderInfo.orderId, tradeContext); + log::debug("Cancel order {}", orderId); + OrderInfo cancelledOrderInfo = cancelOrder(orderId, tradeContext); totalTradedAmounts += cancelledOrderInfo.tradedAmounts; from -= cancelledOrderInfo.tradedAmounts.tradedFrom; if (from == 0) { - log::debug("Order {} matched with last traded amounts {} while cancelling", placeOrderInfo.orderId, + log::debug("Order {} matched with last traded amounts {} while cancelling", orderId, cancelledOrderInfo.tradedAmounts); break; } if (reachedEmergencyTime) { // timeout. Action depends on Strategy - if (timerStart + options.maxTradeTime() < nowTime) { - log::warn("Time out reached, stop from there"); - break; - } log::info("Emergency time reached, {} trade", options.timeoutActionStr()); - if (options.placeMarketOrderAtTimeout()) { + if (options.placeMarketOrderAtTimeout() && !options.isTakerStrategy(placeSimulatedRealOrder)) { nextAction = NextAction::kPlaceMarketOrder; } else { break; } } else { // updatePriceNeeded - nextAction = NextAction::kNewOrderLimitPrice; - } - if (nextAction != NextAction::kWait) { - if (nextAction == NextAction::kPlaceMarketOrder) { - tradeInfo.options.switchToTakerStrategy(); - optPrice = _exchangePublic.computeAvgOrderPrice(mk, from, tradeInfo.options.priceOptions()); - if (!optPrice) { - throw exception("Impossible to compute new average order price"); - } - price = *optPrice; - log::info("Reached emergency time, make a last taker order at price {}", price); - } else { - lastPriceUpdateTime = Clock::now(); - log::info("Limit price changed from {} to {}, update order", lastPrice, price); - } - - lastPrice = price; - - // Compute new volume (price is either not needed in taker order, or already recomputed) - placeOrderInfo = placeOrderProcess(from, price, tradeInfo); - - if (placeOrderInfo.isClosed()) { - totalTradedAmounts += placeOrderInfo.tradedAmounts(); - log::debug("Order {} closed with last traded amounts {}", placeOrderInfo.orderId, - placeOrderInfo.tradedAmounts()); - break; - } + nextAction = NextAction::kPlaceLimitOrder; + log::info("Limit price changed from {} to {}, update order", lastPrice, price); } } } diff --git a/src/api/common/test/exchangeprivateapi_test.cpp b/src/api/common/test/exchangeprivateapi_test.cpp index 578385f0..48dbba17 100644 --- a/src/api/common/test/exchangeprivateapi_test.cpp +++ b/src/api/common/test/exchangeprivateapi_test.cpp @@ -260,6 +260,97 @@ TEST_F(ExchangePrivateTest, SimulatedOrderShouldNotCallPlaceOrder) { EXPECT_EQ(exchangePrivate.trade(from, market.quote(), tradeOptions), TradedAmounts(from, toAmount)); } +TEST_F(ExchangePrivateTest, MakerTradeQuoteToBaseEmergencyTakerTrade) { + tradeBaseExpectCalls(); + + MonetaryAmount from(10000, market.quote()); + MonetaryAmount pri1(bidPrice1); + TradeSide side = TradeSide::kBuy; + TradeContext tradeContext(market, side); + + MonetaryAmount vol1(from / pri1, market.base()); + + TradeOptions tradeOptions(TradeTimeoutAction::kForceMatch, TradeMode::kReal, Duration::zero(), Duration::zero(), + TradeTypePolicy::kForceMultiTrade); + TradeInfo tradeInfo(tradeContext, tradeOptions); + + EXPECT_CALL(exchangePublic, queryOrderBook(market, testing::_)) + .Times(2) + .WillRepeatedly(testing::Return(marketOrderBook1)); + + TradedAmounts zeroTradedAmounts(from.currencyCode(), market.base()); + OrderInfo unmatchedOrderInfo(zeroTradedAmounts, false); + PlaceOrderInfo unmatchedPlacedOrderInfo1(unmatchedOrderInfo, OrderId("Order # 0")); + + // Place first order + EXPECT_CALL(exchangePrivate, placeOrder(from, vol1, pri1, tradeInfo)) + .WillOnce(testing::Return(unmatchedPlacedOrderInfo1)); + + EXPECT_CALL(exchangePrivate, + queryOrderInfo(static_cast(unmatchedPlacedOrderInfo1.orderId), tradeContext)) + .WillOnce(testing::Return(unmatchedPlacedOrderInfo1.orderInfo)); + + // Emergency reached - cancel order + EXPECT_CALL(exchangePrivate, cancelOrder(static_cast(unmatchedPlacedOrderInfo1.orderId), tradeContext)) + .WillOnce(testing::Return(OrderInfo(zeroTradedAmounts, false))); + + // Place taker order + tradeInfo.options.switchToTakerStrategy(); + + MonetaryAmount pri2 = *marketOrderBook1.computeAvgPriceForTakerAmount(from); + MonetaryAmount vol2(from / pri2, market.base()); + + PlaceOrderInfo matchedPlacedOrderInfo2(OrderInfo(TradedAmounts(from, vol2), true), OrderId("Order # 1")); + EXPECT_CALL(exchangePrivate, placeOrder(from, vol2, pri2, tradeInfo)) + .WillOnce(testing::Return(matchedPlacedOrderInfo2)); + + EXPECT_EQ(exchangePrivate.trade(from, market.base(), tradeOptions), TradedAmounts(from, vol2)); +} + +TEST_F(ExchangePrivateTest, MakerTradeQuoteToBaseTimeout) { + tradeBaseExpectCalls(); + + MonetaryAmount from(5000, market.quote()); + MonetaryAmount pri1(bidPrice1); + TradeSide side = TradeSide::kBuy; + TradeContext tradeContext(market, side); + + MonetaryAmount vol1(from / pri1, market.base()); + + TradeOptions tradeOptions(TradeTimeoutAction::kCancel, TradeMode::kReal, Duration::zero(), Duration::zero(), + TradeTypePolicy::kForceMultiTrade); + TradeInfo tradeInfo(tradeContext, tradeOptions); + + EXPECT_CALL(exchangePublic, queryOrderBook(market, testing::_)).WillOnce(testing::Return(marketOrderBook1)); + + TradedAmounts zeroTradedAmounts(from.currencyCode(), market.base()); + OrderInfo unmatchedOrderInfo(zeroTradedAmounts, false); + PlaceOrderInfo unmatchedPlacedOrderInfo1(unmatchedOrderInfo, OrderId("Order # 0")); + + // Place first order, no match at place + EXPECT_CALL(exchangePrivate, placeOrder(from, vol1, pri1, tradeInfo)) + .WillOnce(testing::Return(unmatchedPlacedOrderInfo1)); + + MonetaryAmount partialMatchedFrom = from / 3; + MonetaryAmount partialMatchedTo(partialMatchedFrom / bidPrice1, market.base()); + + TradedAmounts partialMatchedTradedAmounts(partialMatchedFrom, partialMatchedTo); + + OrderInfo partialMatchOrderInfo(partialMatchedTradedAmounts, false); + + EXPECT_CALL(exchangePrivate, + queryOrderInfo(static_cast(unmatchedPlacedOrderInfo1.orderId), tradeContext)) + .WillOnce(testing::Return(partialMatchOrderInfo)); + + // Emergency reached - cancel order + EXPECT_CALL(exchangePrivate, cancelOrder(static_cast(unmatchedPlacedOrderInfo1.orderId), tradeContext)) + .WillOnce(testing::Return(partialMatchOrderInfo)); + + // No action expected after emergency reached + + EXPECT_EQ(exchangePrivate.trade(from, market.base(), tradeOptions), partialMatchedTradedAmounts); +} + inline bool operator==(const InitiatedWithdrawInfo &lhs, const InitiatedWithdrawInfo &rhs) { return lhs.withdrawId() == rhs.withdrawId(); } diff --git a/src/api/exchanges/src/krakenprivateapi.cpp b/src/api/exchanges/src/krakenprivateapi.cpp index acc35689..60c339b1 100644 --- a/src/api/exchanges/src/krakenprivateapi.cpp +++ b/src/api/exchanges/src/krakenprivateapi.cpp @@ -69,8 +69,8 @@ json PrivateQuery(CurlHandle& curlHandle, const APIKey& apiKey, std::string_view } if (errorIt != ret.end() && !errorIt->empty()) { std::string_view msg = errorIt->front().get(); - if (method.ends_with("CancelOrder") && msg == "EOrder:Unknown order") { - log::warn("Unknown order from Kraken CancelOrder. Assuming closed order"); + if (method.ends_with("CancelOrder") && msg.ends_with("Unknown order")) { + log::warn("No data for order, probably expired"); ret = json::parse(R"({" error ":[]," result ":{" count ":1}})"); } else { log::error("Full Kraken json error: '{}'", ret.dump()); diff --git a/src/tech/src/unitsparser.cpp b/src/tech/src/unitsparser.cpp index 7b8c9064..bad5dc99 100644 --- a/src/tech/src/unitsparser.cpp +++ b/src/tech/src/unitsparser.cpp @@ -10,8 +10,8 @@ int64_t ParseNumberOfBytes(std::string_view sizeStr) { if (endPos == std::string_view::npos) { endPos = sizeStr.size(); } - int64_t v = FromString(std::string_view(sizeStr.begin(), sizeStr.begin() + endPos)); - if (v < 0) { + int64_t nbBytes = FromString(std::string_view(sizeStr.begin(), sizeStr.begin() + endPos)); + if (nbBytes < 0) { throw exception("Number of bytes cannot be negative"); } int64_t multiplier = 1; @@ -40,7 +40,7 @@ int64_t ParseNumberOfBytes(std::string_view sizeStr) { } } - return v * multiplier; + return nbBytes * multiplier; } } // namespace cct \ No newline at end of file diff --git a/src/tech/test/flatkeyvaluestring_test.cpp b/src/tech/test/flatkeyvaluestring_test.cpp index da3f72aa..6654a5e9 100644 --- a/src/tech/test/flatkeyvaluestring_test.cpp +++ b/src/tech/test/flatkeyvaluestring_test.cpp @@ -95,8 +95,8 @@ TEST(FlatKeyValueStringTest, WithNullTerminatingCharAsSeparator) { EXPECT_EQ(kvPairs.str(), std::string_view("tata:abc\0huhu:haha\0&newField:&&newValue&&"sv)); int kvPairPos = 0; - for (const auto &[k, v] : kvPairs) { - const char *kvPairPtr = k.data(); + for (const auto &[key, val] : kvPairs) { + const char *kvPairPtr = key.data(); switch (kvPairPos++) { case 0: ASSERT_STREQ(kvPairPtr, "tata:abc"); @@ -137,40 +137,40 @@ TEST_F(CurlOptionsCase1, Get) { } TEST_F(CurlOptionsCase1, Iterator) { - int i = 0; - for (const auto &[k, v] : kvPairs) { - switch (i++) { + int itPos = 0; + for (const auto &[key, val] : kvPairs) { + switch (itPos++) { case 0: - EXPECT_EQ(k, "units"); - EXPECT_EQ(v, "0.11176"); + EXPECT_EQ(key, "units"); + EXPECT_EQ(val, "0.11176"); break; case 1: - EXPECT_EQ(k, "price"); - EXPECT_EQ(v, "357.78"); + EXPECT_EQ(key, "price"); + EXPECT_EQ(val, "357.78"); break; case 2: - EXPECT_EQ(k, "777"); - EXPECT_EQ(v, "encoredutravail?"); + EXPECT_EQ(key, "777"); + EXPECT_EQ(val, "encoredutravail?"); break; case 3: - EXPECT_EQ(k, "hola"); - EXPECT_EQ(v, "quetal"); + EXPECT_EQ(key, "hola"); + EXPECT_EQ(val, "quetal"); break; case 4: - EXPECT_EQ(k, "array1"); - EXPECT_EQ(v, "val1,,"); + EXPECT_EQ(key, "array1"); + EXPECT_EQ(val, "val1,,"); break; case 5: - EXPECT_EQ(k, "array2"); - EXPECT_EQ(v, ",val1,val2,value,"); + EXPECT_EQ(key, "array2"); + EXPECT_EQ(val, ",val1,val2,value,"); break; case 6: - EXPECT_EQ(k, "emptyArray"); - EXPECT_EQ(v, ","); + EXPECT_EQ(key, "emptyArray"); + EXPECT_EQ(val, ","); break; } } - EXPECT_EQ(i, 7); + EXPECT_EQ(itPos, 7); } TEST_F(CurlOptionsCase1, ConvertToJson) { @@ -209,8 +209,8 @@ TEST_F(CurlOptionsCase1, ConvertToJson) { TEST_F(CurlOptionsCase1, AppendIntegralValues) { kvPairs.append("price1", 1957386078376L); EXPECT_EQ(kvPairs.get("price1"), "1957386078376"); - int8_t s = -116; - kvPairs.append("testu", s); + int8_t val = -116; + kvPairs.append("testu", val); EXPECT_EQ(kvPairs.get("testu"), "-116"); } @@ -223,8 +223,8 @@ TEST_F(CurlOptionsCase1, SetIntegralValues) { EXPECT_EQ( kvPairs.str(), "units=0.11176&price=357.78&777=-666&hola=quetal&array1=val1,,&array2=,val1,val2,value,&emptyArray=,&price1=42"); - int8_t s = -116; - kvPairs.set("testu", s); + int8_t val = -116; + kvPairs.set("testu", val); EXPECT_EQ(kvPairs.get("testu"), "-116"); }