Skip to content

Commit

Permalink
Simplify centralized trade algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
sjanel committed Feb 19, 2023
1 parent d6b075f commit 4510b0c
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 95 deletions.
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

0 comments on commit 4510b0c

Please sign in to comment.