Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify centralized trade algorithm #379

Merged
merged 1 commit into from
Feb 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/common/include/exchangeprivateapi.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
131 changes: 66 additions & 65 deletions src/api/common/src/exchangeprivateapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<MonetaryAmount> 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<UserRefInt>(TimestampToS(timerStart) % static_cast<int64_t>(std::numeric_limits<UserRefInt>::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<MonetaryAmount> 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<MonetaryAmount> 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);
}
}
}
Expand Down
91 changes: 91 additions & 0 deletions src/api/common/test/exchangeprivateapi_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<OrderIdView>(unmatchedPlacedOrderInfo1.orderId), tradeContext))
.WillOnce(testing::Return(unmatchedPlacedOrderInfo1.orderInfo));

// Emergency reached - cancel order
EXPECT_CALL(exchangePrivate, cancelOrder(static_cast<OrderIdView>(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<OrderIdView>(unmatchedPlacedOrderInfo1.orderId), tradeContext))
.WillOnce(testing::Return(partialMatchOrderInfo));

// Emergency reached - cancel order
EXPECT_CALL(exchangePrivate, cancelOrder(static_cast<OrderIdView>(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();
}
Expand Down
4 changes: 2 additions & 2 deletions src/api/exchanges/src/krakenprivateapi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string_view>();
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());
Expand Down
6 changes: 3 additions & 3 deletions src/tech/src/unitsparser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ int64_t ParseNumberOfBytes(std::string_view sizeStr) {
if (endPos == std::string_view::npos) {
endPos = sizeStr.size();
}
int64_t v = FromString<int64_t>(std::string_view(sizeStr.begin(), sizeStr.begin() + endPos));
if (v < 0) {
int64_t nbBytes = FromString<int64_t>(std::string_view(sizeStr.begin(), sizeStr.begin() + endPos));
if (nbBytes < 0) {
throw exception("Number of bytes cannot be negative");
}
int64_t multiplier = 1;
Expand Down Expand Up @@ -40,7 +40,7 @@ int64_t ParseNumberOfBytes(std::string_view sizeStr) {
}
}

return v * multiplier;
return nbBytes * multiplier;
}

} // namespace cct
Loading