diff --git a/README.md b/README.md index 6f6e4711..c73469c9 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,12 @@ Main features: - [Usage](#usage) - [Input / output](#input--output) - [Format](#format) - - [Unique command](#unique-command) + - [Simple usage](#simple-usage) - [Multiple commands](#multiple-commands) + - [Piping commands](#piping-commands) - [Logging](#logging) - [Activity history](#activity-history) + - [Parallel requests](#parallel-requests) - [Public requests](#public-requests) - [Health check](#health-check) - [Markets](#markets) @@ -71,15 +73,28 @@ Main features: - [Last trades](#last-trades) - [Conversion path](#conversion-path) - [Private requests](#private-requests) - - [How to target keys on exchanges](#how-to-target-keys-on-exchanges) + - [Selecting private keys on exchanges](#selecting-private-keys-on-exchanges) - [Balance](#balance) - - [Single Trade](#single-trade) + - [Trade](#trade) + - [Syntax](#syntax) + - [Standard - full information](#standard---full-information) + - [With information from previous 'piped' command](#with-information-from-previous-piped-command) - [Options](#options) - - [Single trade all](#single-trade-all) + - [Trade timeout](#trade-timeout) + - [Trade asynchronous mode](#trade-asynchronous-mode) + - [Trade Price Strategy](#trade-price-strategy) + - [Trade Price Picker](#trade-price-picker) + - [Absolute price](#absolute-price) + - [Relative price](#relative-price) + - [Trade all](#trade-all) - [Examples with explanation](#examples-with-explanation) - [Multi Trade](#multi-trade) - [Trade simulation](#trade-simulation) - [Buy / Sell](#buy--sell) + - [Syntax](#syntax-1) + - [Standard - full information](#standard---full-information-1) + - [Sell - with information from previous 'piped' command](#sell---with-information-from-previous-piped-command) + - [Behavior](#behavior) - [Examples with explanation](#examples-with-explanation-1) - [Deposit information](#deposit-information) - [Recent deposits](#recent-deposits) @@ -93,6 +108,9 @@ Main features: - [Withdraw refresh time](#withdraw-refresh-time) - [Withdraw asynchronous mode](#withdraw-asynchronous-mode) - [Dust sweeper](#dust-sweeper) + - [Syntax](#syntax-2) + - [Standard - full information](#standard---full-information-2) + - [Sell - with information from previous 'piped' command](#sell---with-information-from-previous-piped-command-1) - [Monitoring options](#monitoring-options) - [Repeat](#repeat) - [Examples of use cases](#examples-of-use-cases) @@ -132,13 +150,13 @@ See [CONFIG.md](CONFIG.md) ### Format -#### Unique command +#### Simple usage `coincenter` is a command line tool with ergonomic and easy to remember option schema. You will usually provide this kind of input: - Command name, without hyphen (check what are the available ones with `help`) - Followed by either: - - Nothing, meaning that command will be applied globally + - Nothing, meaning that command will be applied globally, when applicable - Amount with currency or any currency, separated by dash to specify pairs, or source - destination - Comma separated list of exchanges (all are considered if not provided) @@ -169,6 +187,26 @@ coincenter balance orderbook XRP-USDT,binance --cur EUR 2nd command ``` +#### Piping commands + +Some commands' input can be deduced from previous commands' output, a bit like piping commands in Linux works. +Input commands accepting previous commands' output are: +- Withdraw +- Trade +- Sell + +For example: + +``` + 1st command 3rd command + / \ / \ +coincenter buy 1500XLM,binance withdraw kraken sell + \ / + 2nd command +``` + +The 1500XLM will be considered for withdraw from Binance if the buy is completed, and the XLM arrived on Kraken considered for selling when the withdraw completes. + ### Logging `coincenter` uses [spdlog](https://github.com/gabime/spdlog) for logging. @@ -191,6 +229,13 @@ By default, it stores all types of trades and withdraws results, but the list is **Important**: those files contain important information so `coincenter` does not clean them automatically. You can move the old activity history files if they take to much space after a while. +## Parallel requests + +You may want to query several exchanges for a command at the same time. In this case, it's possible to ask `coincenter` to launch requests in parallel when it's possible. +By default, the number of requests in parallel is `1`. To increase it, change the value of the field `nbMaxParallelRequests` in `generalconfig.json` file (more information [here](CONFIG.md#options-description)). + +You will have a nice boost of speed when you query the same thing from multiple exchanges / or accounts. However, the logs may not be ordered anymore. + ## Public requests ### Health check @@ -297,7 +342,7 @@ coincenter conversion shib-sol These requests will require that you have your secret keys in `data/secret/secret.json` file, for each exchange used. You can check easily that it works correctly with the `balance` option. -### How to target keys on exchanges +### Selecting private keys on exchanges `coincenter` supports several keys per exchange. Here are both ways to target exchange(s) for `coincenter`: @@ -381,25 +426,74 @@ coincenter balance eur,kraken,bithumb By default, the balance displayed is the available one, with opened orders remaining unmatched amounts deduced. To include balance in use as well, `--in-use` should be added. -### Single Trade +### Trade + +A trade is a request to change a certain amount in a given currency into another currency on a list of considered exchanges (or all if none specified). +It can be tailored with a range of options: -A single trade per market / exchange can be done in a simple way supporting 3 parameterized price chooser strategies. -It is 'single' in the sense that trade is made in one step (see [Multi Trade](#multi-trade)), in an existing market of provided exchange(s) Similarly to other options, list of exchanges is optional. If empty, all exchanges having some balance for given start amount may be considered. Then, exchanges are sorted in decreasing order of available amounts, and are selected until total available reaches the desired one. +- Absolute start amount or percentage of available amount +- [Single / multi trade](#multi-trade) +- [Automatic price chooser strategy](#trade-price-strategy) +- [Custom price](#trade-price-picker) +- [Timeout](#trade-timeout) +- [Asynchronous mode](#trade-asynchronous-mode) +- Smart multi account behavior (see below) -Given start amount can be absolute, or relative with the `%` character. In the latter case, the total start amount will be computed from the total available amounts of the matching exchanges. Percentages should not be larger than `100` and may be decimal numbers. +Similarly to other options, list of exchanges is optional. +If empty, all exchanges having some balance for given start amount may be considered. +When several accounts are involved, the trade are performed on the exchanges providing the most available starting amount to trade in priority, and are selected until total available reaches the desired one. This behavior ensures the minimum number of independent trades to reach the desired goal. +Check the [examples](#examples-with-explanation) below for more precise information. + +Given start amount can be absolute, or relative with the `%` character. In the latter case, the total start amount will be computed from the total available amounts of the matching exchanges. This implies a call to `balance` on the considered exchanges before the actual trade. Percentages should not be larger than `100` and may be decimal numbers. If only one private exchange is given and start amount is absolute, `coincenter` will not query your funds prior to the trade to minimize response time, make sure that inputs are correct or program may throw an exception. -#### Options +#### Syntax -**Parallel requests** +A trade has two input flavors. -You may want to query several exchanges for a command at the same time. In this case, it's possible to ask `coincenter` to launch requests in parallel when it's possible. -By default, the number of requests in parallel is `1`. To increase it, change the value of the field `nbMaxParallelRequests` in `generalconfig.json` file (more information [here](CONFIG.md#options-description)). +##### Standard - full information -You will have a nice boost of speed when you query the same thing from multiple exchanges / or accounts. However, the logs may not be ordered anymore. +This is the most common trade input. You will need to provide, in order: + +- decimal, positive amount, absolute (default) or relative (percentage suffixed with `%`) +- first currency linked to this amount +- a dash `-` to help parser split the two currencies +- second currency destination of the wished trade +- optional list of exchanges where to perform the trade (if several accounts are matched, the amount will be split among them, with a designated strategy) -**Trade timeout** +``` +coincenter trade 35.5%ETH-USDT,kucoin +``` + +##### With information from previous 'piped' command + +This second flavor can be useful for piped commands. The unique trade input argument will be the *destination currency*. The amount and considered exchanges will be deduced from the output of the previous command. + +Example: + +You withdraw a certain amount of ETH from exchange kraken to kucoin. You want to trade the received amount on kucoin to USDT: + +``` +coincenter withdraw-apply 1.45ETH,kraken-kucoin trade USDT +``` + +If the previous command is a withdraw, input exchange list is a single one. + +Note that trades themselves can be piped, as the output has an amount and list of exchanges. + +Trade 15 % of my all available BTC to USDT (whatever the exchanges), and trade all the resulted USDT to ETH: + +``` +coincenter trade 15%BTC-USDT trade ETH +``` + +**Important note**: the piped command will be possible only if the previous command is *complete*. For a trade (or buy / sell), a successful command implies that all the initial amount has been traded from, that no remaining unmatched amount is left. For a withdraw, it means that it successfully arrived to the destination exchange. + + +#### Options + +##### Trade timeout A trade is **synchronous** by default with the created order of `coincenter` (it follows the lifetime of the order). The trade ends when one of the following events occur: @@ -413,7 +507,7 @@ Value can contain several units, but do not support decimal values. For instance Another example: `--timeout 3min30s504ms` -**Trade asynchronous mode** +##### Trade asynchronous mode Above option allows to control the full life time of a trade until it is either fully matched or cancelled (at timeout). If you want to only **fire and forget** an order placement, you can use `--async` flag. @@ -424,9 +518,9 @@ Note that it's not equivalent of choosing a **zero** trade timeout (above option - Order is not cancelled after the trade process in asynchronous mode, whereas it's either cancelled or matched directly in synchronous mode - Asynchronous trade is faster as it does not even query order information -**Trade Price Strategy** +##### Trade Price Strategy -Possible order price strategies: +Possible order price strategies, configurable with `--price-strategy`: - `maker`: order placed at limit price and regularly updated to limit price (default) - `taker`: order placed at market price, should be matched immediately @@ -434,7 +528,7 @@ Possible order price strategies: Use command line option `trade` to make a trade from a departure amount. -**Trade Price Picker** +##### Trade Price Picker In order to control more precisely the price of the order, `coincenter` supports custom price as well thanks to `--price` option. Note that this is not compatible with above **trade price strategy** option. `--price` expects either an integer (!= 0) representing a **relative** price compared to the limit price, or a monetary amount representing an **absolute** fixed price (decimal amount with the price currency). @@ -447,15 +541,16 @@ In fact, parser will recognize a relative price if the amount is without any dec | `34.6` | *absolute* price (engine will take the currency from the market of the trade) | | `25 XXX` | *absolute* price (engine will override the currency from the market of the trade and not consider `XXX`, no error will be raised) | -*Absolute price* +###### Absolute price When requesting an absolute price, the order will be placed exactly at this price. Depending on the order book and the limit prices, order may or may not be matched instantly. Double check the price before launching your trade command! + **Notes**: -- such an order will not be compatible with [multi trade](#multi-trade)) because an absolute price makes sense only for a specific market. However, if you ask a multi trade with a fixed price, no error will be raised, and `coincenter` will simply launch a single trade. +- Such an order will not be compatible with [multi trade](#multi-trade)) because an absolute price makes sense only for a specific market. However, if you ask a multi trade with a fixed price, no error will be raised, and `coincenter` will simply launch a single trade. - Order price will not be continuously updated over time -*Relative price* +###### Relative price The **relative price** makes it possible to easily settle a price anywhere in the orderbook. The chosen price is **relative** to the order book limit price - it represents how far away the price is related to the 'center' prices. The higher (or lower) the value, the more difficult it will be for the order to be bought - or sold. @@ -491,9 +586,15 @@ coincenter trade nB-A --price 3 The chosen price will be `0.42`. -#### Single trade all +#### Trade all + +Instead of providing a start amount to trade, specify a currency. So `trade-all XXX...` is equivalent to `trade 100%XXX`. -If you want to simply sell all available amount from a given currency, you can use `trade-all` flavor which takes a departure currency instead of an input amount. This command will ask to trade all available amount for all considered exchanges - use with care! +Example: Trade all available EUR on all exchanges to BTC + +``` +coincenter trade-all EUR-BTC +``` #### Examples with explanation @@ -549,7 +650,7 @@ In order to activate multi trades, there are two ways: - Default behavior is controlled by [multiTradeAllowedByDefault option](CONFIG.md#options-description) in the `exchangeconfig.json` file. If `true`, multi trade is allowed for the given exchange (or all exchanges if under `default` level). - It can be forced by adding command line flag `--multi-trade`. -In the case it's activated in the config file, `--no-multi-trade` can force deactivation of the multi trade if needed. +In the case it's activated in the config file, `--no-multi-trade` can force deactivation of the multi trade. #### Trade simulation @@ -557,7 +658,7 @@ Some exchanges (**Kraken** and **Binance** for instance) allow to actually query ### Buy / Sell -Trade family of commands require that you specify the *start amount* (with the start currency) and the *target currency*. You can use `buy` and `sell` options when you have a start amount (for *sell*) or a target amount (for *buy*) only. It's more easy to use, but `coincenter` needs to know which are the [preferred currencies](CONFIG.md#options-description) to which it can *sell* the start amount to, or use as start amount for a *buy*. +Trade family of commands require that you specify the *start amount* (with the start currency) and the *target currency*. You can use `buy` and `sell` commands when you have a start amount (for *sell*) or a target amount (for *buy*) only. It's more easy to use, but `coincenter` needs to know which are the [preferred currencies](CONFIG.md#options-description) to which it can *sell* the start amount to, or use as start amount for a *buy*. Sell option also supports percentage as start amount. In this case, the desired percentage of total available amount of given currency on matching exchanges (the ones specified after the `,` or all if none given as usual) will be sold. To complement this, `sell-all` option with a start currency instead of an amount is supported as well, which is syntactic sugar of a sell of `100%` of available amount. @@ -565,7 +666,54 @@ Buy a percentage is not available yet, simply because I am not sure about what s The list of preferred currencies should be filled prior to **buy / sell** command and is statically loaded at start of coincenter. It is an array of currencies ordered by decreasing priority, and they represent the currencies that can be used as target currency for a *sell*, and base currency for a *buy*. See [config](CONFIG.md#options-description) file to see how to set the preferred currencies. -**Behavior**: +#### Syntax + +A sell has two input flavors, whereas a buy only one. + +##### Standard - full information + +This is the most common buy/sell input. You will need to provide, in order: + +- decimal, positive amount, absolute (for buy and sell, default) or relative for a sell (percentage suffixed with `%`, incompatible with buy) +- currency linked to this amount +- optional list of exchanges where to perform the trade (if several accounts are matched, the amount will be split among them, with a designated strategy) + +Buy 500 SOL, considering all my accounts on Binance and Bithumb. +``` +coincenter buy 500SOL,binance,bithumb +``` + +Sell 25 % of all my available ETH on all accounts +``` +coincenter sell 25%ETH +``` + +##### Sell - with information from previous 'piped' command + +This second flavor can be useful for piped commands, for *sell* only. +You do not need to provide any information to the `sell` command. The amount and considered exchanges will be deduced from the output of the previous command. + +Examples: + +You withdraw a certain amount of XRP from exchange upbit to bithumb. You want to sell the received amount of XRP on bithumb: + +``` +coincenter withdraw-apply 500XRP,upbit-bithumb sell +``` + +Note that trades themselves can be piped, as the output has an amount and list of exchanges. + +Following example would be foolish, but possible by coincenter. + +Buy 1 ETH on any exchange then sell it: + +``` +coincenter buy 1ETH sell +``` + +**Important note**: as for [trade](#with-information-from-previous-piped-command), the piped command will be possible only if the previous command is *complete*. + +#### Behavior - All currencies present in the **preferred currencies** of a given exchange may be used to validate and perform trades. For instance, if you decide to include non stable cryptos (for instance, `BTC` as it is often involves in many pairs), it can be used as a base for a *buy*, or target for a *sell*. Thus as a recommendation, if you do add such currencies as *preferred*, prefer placing them after the fiats and stable coins. - Several trades may be performed on the same account for a **buy**. For instance: you have both `EUR` and `USDT` and they are both present in the *preferred currencies*, and the desired amount of target currency is high enough such that it would *eat* all of your `EUR` or `USDT`. @@ -695,7 +843,8 @@ cancels order Id OID1 only, on the exchange where it is found on (no error is ra ### Withdraw coin -It is possible to withdraw crypto currency with `coincenter` as well, in either a **synchronized** mode (withdraw will check that funds are well received at destination) or **asynchronous** mode. Either an absolute amount can be specified, or a percentage (`10xrp` or `25%xrp` for instance). `withdraw-apply-all` is a convenient command wrapper of `withdraw-apply 100%`. +It is possible to withdraw crypto currency with `coincenter` as well, in either a **synchronized** mode (withdraw will check that funds are well received at destination) or **asynchronous** mode, with command `withdraw-apply`. +Either an absolute amount can be specified, or a percentage (`10xrp` or `25%xrp` for instance). `withdraw-apply-all` is a convenient command wrapper of `withdraw-apply 100%`. Some exchanges require that external addresses are validated prior to their usage in the API (*Kraken* and *Huobi* for instance). @@ -750,6 +899,35 @@ Example: Attempts to clean amount of BTG on bithumb and upbit coincenter dust-sweeper btg,bithumb,upbit ``` +#### Syntax + +Withdrawal input parameters can be provided in two flavors. + +##### Standard - full information + +This is the most common withdraw. You will need to provide, in order: + +- decimal, positive amount, absolute (default) or relative (percentage suffixed with `%`) +- currency linked to this amount +- Exactly two exchange accounts separated by a dash '-' + +##### Sell - with information from previous 'piped' command + +This second flavor can be useful for piped commands. +If the previous command output implies only one exchange, then it will be considered the origin exchange for the withdraw. +The amount to be withdrawn is also taken from the output of previous command. +You will need to specify exactly one exchange which will be the destination of the withdrawal. + +Examples: + +You trade an amount of KRW on upbit into ADA, and the obtained ADA amount will be withdrawn to kraken: + +``` +coincenter trade 80%KRW-ADA,upbit withdraw-apply kraken +``` + +**Important note**: as for [trade](#with-information-from-previous-piped-command), the piped withdraw will be possible only if the previous trade is *complete*. + ## Monitoring options `coincenter` can export metrics to an external instance of `Prometheus` thanks to its implementation of [prometheus-cpp](https://github.com/jupp0r/prometheus-cpp) client. Refer to [Build with monitoring support](#build-with-monitoring-support) section to know how to build `coincenter` with it. diff --git a/src/api-objects/include/withdrawoptions.hpp b/src/api-objects/include/withdrawoptions.hpp index e7485f3e..0637681b 100644 --- a/src/api-objects/include/withdrawoptions.hpp +++ b/src/api-objects/include/withdrawoptions.hpp @@ -24,6 +24,8 @@ class WithdrawOptions { std::string_view withdrawSyncPolicyStr() const; + bool operator==(const WithdrawOptions &) const noexcept = default; + private: /// The waiting time between each query of withdraw info to check withdraw status from an exchange. /// A very small value is not relevant as withdraw time order of magnitude are minutes or hours diff --git a/src/api-objects/include/withdrawsordepositsconstraints.hpp b/src/api-objects/include/withdrawsordepositsconstraints.hpp index c322f0f5..032fce52 100644 --- a/src/api-objects/include/withdrawsordepositsconstraints.hpp +++ b/src/api-objects/include/withdrawsordepositsconstraints.hpp @@ -48,6 +48,8 @@ class WithdrawsOrDepositsConstraints { } bool isIdOnlyDependent() const { return _currencyIdTimeConstraintsBmp.isDepositIdOnlyDependent(); } + bool operator==(const WithdrawsOrDepositsConstraints &) const noexcept = default; + using trivially_relocatable = is_trivially_relocatable::type; private: diff --git a/src/api/interface/include/exchange.hpp b/src/api/interface/include/exchange.hpp index 24c50d9a..b897ef4f 100644 --- a/src/api/interface/include/exchange.hpp +++ b/src/api/interface/include/exchange.hpp @@ -23,6 +23,13 @@ class Exchange { std::string_view name() const { return _exchangePublic.name(); } std::string_view keyName() const { return apiPrivate().keyName(); } + ExchangeName createExchangeName() const { + if (hasPrivateAPI()) { + return {name(), keyName()}; + } + return ExchangeName(name()); + } + api::ExchangePublic &apiPublic() { return _exchangePublic; } const api::ExchangePublic &apiPublic() const { return _exchangePublic; } diff --git a/src/engine/CMakeLists.txt b/src/engine/CMakeLists.txt index 00a65b6a..3e5f241e 100644 --- a/src/engine/CMakeLists.txt +++ b/src/engine/CMakeLists.txt @@ -8,6 +8,13 @@ target_link_libraries(coincenter_engine PUBLIC coincenter_api-interface) target_link_libraries(coincenter_engine PUBLIC coincenter_objects) target_include_directories(coincenter_engine PUBLIC include) +add_unit_test( + coincentercommandfactory_test + test/coincentercommandfactory_test.cpp + LIBRARIES + coincenter_engine +) + add_unit_test( coincenteroptions_test test/coincenteroptions_test.cpp @@ -65,3 +72,10 @@ add_unit_test( LIBRARIES coincenter_engine ) + +add_unit_test( + transferablecommandresult_test + test/transferablecommandresult_test.cpp + LIBRARIES + coincenter_engine +) diff --git a/src/engine/include/coincenter.hpp b/src/engine/include/coincenter.hpp index 84e8d87d..73d9f456 100644 --- a/src/engine/include/coincenter.hpp +++ b/src/engine/include/coincenter.hpp @@ -2,22 +2,19 @@ #include #include -#include #include "apikeysprovider.hpp" #include "coincenterinfo.hpp" #include "commonapi.hpp" -#include "exchange.hpp" #include "exchangename.hpp" #include "exchangepool.hpp" #include "exchangesorchestrator.hpp" #include "fiatconverter.hpp" #include "metricsexporter.hpp" -#include "monitoringinfo.hpp" #include "ordersconstraints.hpp" #include "queryresultprinter.hpp" #include "queryresulttypes.hpp" -#include "tradedamounts.hpp" +#include "transferablecommandresult.hpp" namespace cct { @@ -132,7 +129,8 @@ class Coincenter { const FiatConverter &fiatConverter() const { return _fiatConverter; } private: - void processCommand(const CoincenterCommand &cmd); + TransferableCommandResultVector processCommand( + const CoincenterCommand &cmd, std::span previousTransferableResults); const CoincenterInfo &_coincenterInfo; api::CommonAPI _commonAPI; diff --git a/src/engine/include/coincentercommand.hpp b/src/engine/include/coincentercommand.hpp index dad817e9..fe736333 100644 --- a/src/engine/include/coincentercommand.hpp +++ b/src/engine/include/coincentercommand.hpp @@ -86,6 +86,8 @@ class CoincenterCommand { bool isPercentageAmount() const { return _isPercentageAmount; } bool withBalanceInUse() const { return _withBalanceInUse; } + bool operator==(const CoincenterCommand&) const noexcept = default; + using trivially_relocatable = std::integral_constant && is_trivially_relocatable_v>::type; diff --git a/src/engine/include/coincentercommandfactory.hpp b/src/engine/include/coincentercommandfactory.hpp new file mode 100644 index 00000000..28364cd6 --- /dev/null +++ b/src/engine/include/coincentercommandfactory.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "coincentercommand.hpp" +#include "coincentercommandtype.hpp" +#include "coincenteroptions.hpp" + +namespace cct { +class StringOptionParser; + +class CoincenterCommandFactory { + public: + CoincenterCommandFactory(const CoincenterCmdLineOptions &cmdLineOptions, const CoincenterCommand *pPreviousCommand) + : _cmdLineOptions(cmdLineOptions), _pPreviousCommand(pPreviousCommand) {} + + static CoincenterCommand CreateMarketCommand(StringOptionParser &optionParser); + + CoincenterCommand createOrderCommand(CoincenterCommandType type, StringOptionParser &optionParser); + + CoincenterCommand createTradeCommand(CoincenterCommandType type, StringOptionParser &optionParser); + + CoincenterCommand createWithdrawApplyCommand(StringOptionParser &optionParser); + + CoincenterCommand createWithdrawApplyAllCommand(StringOptionParser &optionParser); + + private: + const CoincenterCmdLineOptions &_cmdLineOptions; + const CoincenterCommand *_pPreviousCommand; +}; +} // namespace cct \ No newline at end of file diff --git a/src/engine/include/coincentercommands.hpp b/src/engine/include/coincentercommands.hpp index d46ba447..d4b9bf8e 100644 --- a/src/engine/include/coincentercommands.hpp +++ b/src/engine/include/coincentercommands.hpp @@ -1,12 +1,10 @@ #pragma once #include -#include #include "cct_vector.hpp" #include "coincentercommand.hpp" #include "coincenteroptions.hpp" -#include "monitoringinfo.hpp" #include "timedef.hpp" namespace cct { @@ -16,18 +14,13 @@ class CoincenterCommands { // Builds a CoincenterCommands without any commands. CoincenterCommands() noexcept = default; - // Builds a CoincenterCommands and add commands from given command line options. - explicit CoincenterCommands(const CoincenterCmdLineOptions &cmdLineOptions) - : CoincenterCommands(std::span{&cmdLineOptions, 1U}) {} - // Builds a CoincenterCommands and add commands from given command line options span. explicit CoincenterCommands(std::span cmdLineOptionsSpan); static vector ParseOptions(int argc, const char *argv[]); /// @brief Set this CoincenterCommands from given command line options. - /// @return false if only help or version is asked, true otherwise - bool addOption(const CoincenterCmdLineOptions &cmdLineOptions); + void addOption(const CoincenterCmdLineOptions &cmdLineOptions, const CoincenterCommand *pPreviousCommand); std::span commands() const { return _commands; } diff --git a/src/engine/include/staticcommandlineoptioncheck.hpp b/src/engine/include/staticcommandlineoptioncheck.hpp index 370f268e..892e9617 100644 --- a/src/engine/include/staticcommandlineoptioncheck.hpp +++ b/src/engine/include/staticcommandlineoptioncheck.hpp @@ -2,8 +2,9 @@ #include #include +#include +#include #include -#include #include "commandlineoption.hpp" @@ -12,7 +13,7 @@ namespace cct { /// Compile time checker of arguments. Currently, the following checks are made: /// - Uniqueness of short hand flags /// - Uniqueness of long names -template +template consteval bool StaticCommandLineOptionsDuplicatesCheck(std::array... ar) { auto all = ComputeAllCommandLineOptions(ar...); @@ -42,7 +43,7 @@ consteval bool StaticCommandLineOptionsDuplicatesCheck(std::array... ar) { /// Compile time checker of descriptions. Following checks are made: /// - Should not start nor end with a '\n' /// - Should not start no end with a space -template +template consteval bool StaticCommandLineOptionsDescriptionCheck(std::array... ar) { const auto all = ComputeAllCommandLineOptions(ar...); const auto isSpaceOrNewLine = [](char ch) { return ch == '\n' || ch == ' '; }; @@ -60,20 +61,20 @@ consteval bool StaticCommandLineOptionsDescriptionCheck(std::array... ar) return true; } -template +template consteval auto ComputeAllCommandLineOptions(std::array... ar) { - constexpr size_t kNbArrays = sizeof...(ar); + constexpr std::size_t kNbArrays = sizeof...(ar); const T* arr[kNbArrays] = {&ar[0]...}; - constexpr size_t lengths[kNbArrays] = {ar.size()...}; + constexpr std::size_t lengths[kNbArrays] = {ar.size()...}; - constexpr size_t kSumLen = std::accumulate(lengths, lengths + kNbArrays, 0); + constexpr std::size_t kSumLen = std::accumulate(lengths, lengths + kNbArrays, 0); std::array all; - size_t allIdx = 0; - for (size_t dataIdx = 0; dataIdx < kNbArrays; ++dataIdx) { - for (size_t lenIdx = 0; lenIdx < lengths[dataIdx]; ++lenIdx) { + std::size_t allIdx = 0; + for (std::size_t dataIdx = 0; dataIdx < kNbArrays; ++dataIdx) { + for (std::size_t lenIdx = 0; lenIdx < lengths[dataIdx]; ++lenIdx) { all[allIdx] = std::get<0>(arr[dataIdx][lenIdx]); ++allIdx; } diff --git a/src/engine/include/stringoptionparser.hpp b/src/engine/include/stringoptionparser.hpp index 1fa58f15..869ef8bd 100644 --- a/src/engine/include/stringoptionparser.hpp +++ b/src/engine/include/stringoptionparser.hpp @@ -3,7 +3,6 @@ #include #include #include -#include #include #include "cct_string.hpp" @@ -16,52 +15,36 @@ namespace cct { class StringOptionParser { public: - using MarketExchanges = std::pair; - using CurrenciesPrivateExchanges = std::tuple; - using CurrencyPrivateExchanges = std::pair; - using MonetaryAmountCurrencyPrivateExchanges = std::tuple; - using CurrencyFromToPrivateExchange = std::pair; - using MonetaryAmountFromToPrivateExchange = std::tuple; - using MonetaryAmountFromToPublicExchangeToCurrency = std::tuple; - using CurrencyPublicExchanges = std::pair; - using CurrenciesPublicExchanges = std::tuple; + enum class AmountType : int8_t { kAbsolute, kPercentage, kNotPresent }; + enum class FieldIs : int8_t { kMandatory, kOptional }; - enum class CurrencyIs : int8_t { kMandatory, kOptional }; + StringOptionParser() noexcept = default; explicit StringOptionParser(std::string_view optFullStr) : _opt(optFullStr) {} - ExchangeNames getExchanges() const; + /// If FieldIs is kOptional and there is no currency, default currency code will be returned. + /// otherwise exception invalid_argument will be raised + CurrencyCode parseCurrency(FieldIs fieldIs = FieldIs::kMandatory); - MarketExchanges getMarketExchanges() const; + /// If FieldIs is kOptional and there is no market, default market will be returned. + /// otherwise exception invalid_argument will be raised + Market parseMarket(FieldIs fieldIs = FieldIs::kMandatory); - CurrencyPrivateExchanges getCurrencyPrivateExchanges(CurrencyIs currencyIs) const; + /// If FieldIs is kOptional and there is no amount, AmountType kNotPresent will be returned + /// otherwise exception invalid_argument will be raised + std::pair parseNonZeroAmount(FieldIs fieldIs = FieldIs::kMandatory); - auto getMonetaryAmountPrivateExchanges() const { - auto ret = getMonetaryAmountCurrencyPrivateExchanges(false); - return std::make_tuple(std::move(std::get<0>(ret)), std::move(std::get<1>(ret)), std::move(std::get<3>(ret))); - } + /// Parse the remaining option string with CSV string values. + vector getCSVValues(); - CurrenciesPrivateExchanges getCurrenciesPrivateExchanges(bool currenciesShouldBeSet = true) const; + /// Parse exchanges. + /// Exception will be raised for any invalid exchange name - but an empty list of exchanges is accepted. + ExchangeNames parseExchanges(char sep = ','); - MonetaryAmountCurrencyPrivateExchanges getMonetaryAmountCurrencyPrivateExchanges() const { - return getMonetaryAmountCurrencyPrivateExchanges(true); - } - - CurrencyFromToPrivateExchange getCurrencyFromToPrivateExchange() const; - - MonetaryAmountFromToPrivateExchange getMonetaryAmountFromToPrivateExchange() const; - - CurrencyPublicExchanges getCurrencyPublicExchanges() const; - - CurrenciesPublicExchanges getCurrenciesPublicExchanges() const; - - vector getCSVValues() const; - - protected: - std::size_t getNextCommaPos(std::size_t startPos = 0, bool throwIfNone = true) const; - - MonetaryAmountCurrencyPrivateExchanges getMonetaryAmountCurrencyPrivateExchanges(bool withCurrency) const; + void checkEndParsing() const; + private: std::string_view _opt; + std::size_t _pos{}; }; } // namespace cct \ No newline at end of file diff --git a/src/engine/include/transferablecommandresult.hpp b/src/engine/include/transferablecommandresult.hpp new file mode 100644 index 00000000..cd114d44 --- /dev/null +++ b/src/engine/include/transferablecommandresult.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "cct_smallvector.hpp" +#include "cct_type_traits.hpp" +#include "exchangename.hpp" +#include "monetaryamount.hpp" + +namespace cct { +class TransferableCommandResult { + public: + TransferableCommandResult(ExchangeName targetedExchange, MonetaryAmount resultedAmount) + : _targetedExchange(std::move(targetedExchange)), _resultedAmount(resultedAmount) {} + + const ExchangeName &targetedExchange() const { return _targetedExchange; } + MonetaryAmount resultedAmount() const { return _resultedAmount; } + + using trivially_relocatable = is_trivially_relocatable::type; + + bool operator==(const TransferableCommandResult &) const noexcept = default; + + private: + ExchangeName _targetedExchange; + MonetaryAmount _resultedAmount; +}; + +using TransferableCommandResultVector = SmallVector; + +class CoincenterCommand; + +std::pair ComputeTradeAmountAndExchanges( + const CoincenterCommand &cmd, std::span previousTransferableResults); + +std::pair ComputeWithdrawAmount( + const CoincenterCommand &cmd, std::span previousTransferableResults); +} // namespace cct \ No newline at end of file diff --git a/src/engine/src/coincenter.cpp b/src/engine/src/coincenter.cpp index a382b1ee..fd0d7e6e 100644 --- a/src/engine/src/coincenter.cpp +++ b/src/engine/src/coincenter.cpp @@ -4,9 +4,11 @@ #include #include #include +#include #include "balanceoptions.hpp" #include "cct_exception.hpp" +#include "cct_log.hpp" #include "coincentercommands.hpp" #include "coincentercommandtype.hpp" #include "coincenterinfo.hpp" @@ -20,9 +22,21 @@ #include "ordersconstraints.hpp" #include "queryresultprinter.hpp" #include "queryresulttypes.hpp" +#include "transferablecommandresult.hpp" #include "withdrawsconstraints.hpp" namespace cct { +namespace { +void FillTransferableCommandResults(const TradeResultPerExchange &tradeResultPerExchange, + TransferableCommandResultVector &transferableResults) { + for (const auto &[exchangePtr, tradeResult] : tradeResultPerExchange) { + if (tradeResult.isComplete()) { + transferableResults.emplace_back(exchangePtr->createExchangeName(), tradeResult.tradedAmounts().to); + } + } +} + +} // namespace using UniquePublicSelectedExchanges = ExchangeRetriever::UniquePublicSelectedExchanges; Coincenter::Coincenter(const CoincenterInfo &coincenterInfo, const ExchangeSecretsInfo &exchangeSecretsInfo) @@ -50,15 +64,18 @@ int Coincenter::process(const CoincenterCommands &coincenterCommands) { log::info("Processing request {}/{}", repeatPos + 1, nbRepeats); } } + TransferableCommandResultVector transferableResults; for (const auto &cmd : commands) { - processCommand(cmd); + transferableResults = processCommand(cmd, transferableResults); ++nbCommandsProcessed; } } return nbCommandsProcessed; } -void Coincenter::processCommand(const CoincenterCommand &cmd) { +TransferableCommandResultVector Coincenter::processCommand( + const CoincenterCommand &cmd, std::span previousTransferableResults) { + TransferableCommandResultVector transferableResults; switch (cmd.type()) { case CoincenterCommandType::kHealthCheck: { const auto healthCheckStatus = healthCheck(cmd.exchangeNames()); @@ -110,10 +127,9 @@ void Coincenter::processCommand(const CoincenterCommand &cmd) { } case CoincenterCommandType::kBalance: { - const BalanceOptions balanceOptions(cmd.withBalanceInUse() - ? BalanceOptions::AmountIncludePolicy::kWithBalanceInUse - : BalanceOptions::AmountIncludePolicy::kOnlyAvailable, - cmd.cur1()); + const auto amountIncludePolicy = cmd.withBalanceInUse() ? BalanceOptions::AmountIncludePolicy::kWithBalanceInUse + : BalanceOptions::AmountIncludePolicy::kOnlyAvailable; + const BalanceOptions balanceOptions(amountIncludePolicy, cmd.cur1()); const auto balancePerExchange = getBalance(cmd.exchangeNames(), balanceOptions); _queryResultPrinter.printBalance(balancePerExchange, cmd.cur1()); break; @@ -144,31 +160,50 @@ void Coincenter::processCommand(const CoincenterCommand &cmd) { break; } case CoincenterCommandType::kTrade: { + // 2 input styles are possible: + // - standard full information with an amount to trade, a destination currency and an optional list of exchanges + // where to trade + // - a currency - the destination one, and start amount and exchange(s) should come from previous command result + auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(cmd, previousTransferableResults); + if (startAmount.isDefault()) { + break; + } const auto tradeResultPerExchange = - trade(cmd.amount(), cmd.isPercentageAmount(), cmd.cur1(), cmd.exchangeNames(), cmd.tradeOptions()); - _queryResultPrinter.printTrades(tradeResultPerExchange, cmd.amount(), cmd.isPercentageAmount(), cmd.cur1(), + trade(startAmount, cmd.isPercentageAmount(), cmd.cur1(), exchangeNames, cmd.tradeOptions()); + _queryResultPrinter.printTrades(tradeResultPerExchange, startAmount, cmd.isPercentageAmount(), cmd.cur1(), cmd.tradeOptions()); + FillTransferableCommandResults(tradeResultPerExchange, transferableResults); break; } case CoincenterCommandType::kBuy: { const auto tradeResultPerExchange = smartBuy(cmd.amount(), cmd.exchangeNames(), cmd.tradeOptions()); _queryResultPrinter.printBuyTrades(tradeResultPerExchange, cmd.amount(), cmd.tradeOptions()); + FillTransferableCommandResults(tradeResultPerExchange, transferableResults); break; } case CoincenterCommandType::kSell: { + auto [startAmount, exchangeNames] = ComputeTradeAmountAndExchanges(cmd, previousTransferableResults); + if (startAmount.isDefault()) { + break; + } const auto tradeResultPerExchange = - smartSell(cmd.amount(), cmd.isPercentageAmount(), cmd.exchangeNames(), cmd.tradeOptions()); + smartSell(startAmount, cmd.isPercentageAmount(), exchangeNames, cmd.tradeOptions()); _queryResultPrinter.printSellTrades(tradeResultPerExchange, cmd.amount(), cmd.isPercentageAmount(), cmd.tradeOptions()); + FillTransferableCommandResults(tradeResultPerExchange, transferableResults); break; } case CoincenterCommandType::kWithdrawApply: { - const auto &fromExchangeName = cmd.exchangeNames().front(); - const auto &toExchangeName = cmd.exchangeNames().back(); - const auto deliveredWithdrawInfoWithExchanges = - withdraw(cmd.amount(), cmd.isPercentageAmount(), fromExchangeName, toExchangeName, cmd.withdrawOptions()); + const auto [grossAmount, exchangeName] = ComputeWithdrawAmount(cmd, previousTransferableResults); + if (grossAmount.isDefault()) { + break; + } + const auto deliveredWithdrawInfoWithExchanges = withdraw(grossAmount, cmd.isPercentageAmount(), exchangeName, + cmd.exchangeNames().back(), cmd.withdrawOptions()); _queryResultPrinter.printWithdraw(deliveredWithdrawInfoWithExchanges, cmd.isPercentageAmount(), cmd.withdrawOptions()); + transferableResults.emplace_back(deliveredWithdrawInfoWithExchanges.first[1]->createExchangeName(), + deliveredWithdrawInfoWithExchanges.second.receivedAmount()); break; } case CoincenterCommandType::kDustSweeper: { @@ -178,10 +213,11 @@ void Coincenter::processCommand(const CoincenterCommand &cmd) { default: throw exception("Unknown command type"); } + return transferableResults; } ExchangeHealthCheckStatus Coincenter::healthCheck(ExchangeNameSpan exchangeNames) { - ExchangeHealthCheckStatus ret = _exchangesOrchestrator.healthCheck(exchangeNames); + const auto ret = _exchangesOrchestrator.healthCheck(exchangeNames); _metricsExporter.exportHealthCheckMetrics(ret); @@ -189,7 +225,7 @@ ExchangeHealthCheckStatus Coincenter::healthCheck(ExchangeNameSpan exchangeNames } ExchangeTickerMaps Coincenter::getTickerInformation(ExchangeNameSpan exchangeNames) { - ExchangeTickerMaps ret = _exchangesOrchestrator.getTickerInformation(exchangeNames); + const auto ret = _exchangesOrchestrator.getTickerInformation(exchangeNames); _metricsExporter.exportTickerMetrics(ret); @@ -199,8 +235,7 @@ ExchangeTickerMaps Coincenter::getTickerInformation(ExchangeNameSpan exchangeNam MarketOrderBookConversionRates Coincenter::getMarketOrderBooks(Market mk, ExchangeNameSpan exchangeNames, CurrencyCode equiCurrencyCode, std::optional depth) { - MarketOrderBookConversionRates ret = - _exchangesOrchestrator.getMarketOrderBooks(mk, exchangeNames, equiCurrencyCode, depth); + const auto ret = _exchangesOrchestrator.getMarketOrderBooks(mk, exchangeNames, equiCurrencyCode, depth); _metricsExporter.exportOrderbookMetrics(mk, ret); @@ -308,7 +343,7 @@ MonetaryAmountPerExchange Coincenter::getLast24hTradedVolumePerExchange(Market m LastTradesPerExchange Coincenter::getLastTradesPerExchange(Market mk, ExchangeNameSpan exchangeNames, int nbLastTrades) { - LastTradesPerExchange ret = _exchangesOrchestrator.getLastTradesPerExchange(mk, exchangeNames, nbLastTrades); + const auto ret = _exchangesOrchestrator.getLastTradesPerExchange(mk, exchangeNames, nbLastTrades); _metricsExporter.exportLastTradesMetrics(mk, ret); diff --git a/src/engine/src/coincentercommandfactory.cpp b/src/engine/src/coincentercommandfactory.cpp new file mode 100644 index 00000000..80529f9f --- /dev/null +++ b/src/engine/src/coincentercommandfactory.cpp @@ -0,0 +1,132 @@ +#include "coincentercommandfactory.hpp" + +#include +#include +#include + +#include "cct_invalid_argument_exception.hpp" +#include "coincentercommand.hpp" +#include "coincentercommandtype.hpp" +#include "coincenteroptions.hpp" +#include "currencycode.hpp" +#include "market.hpp" +#include "monetaryamount.hpp" +#include "ordersconstraints.hpp" +#include "stringoptionparser.hpp" +#include "timedef.hpp" + +namespace cct { +CoincenterCommand CoincenterCommandFactory::CreateMarketCommand(StringOptionParser &optionParser) { + auto market = optionParser.parseMarket(StringOptionParser::FieldIs::kOptional); + if (market.isNeutral()) { + market = Market(optionParser.parseCurrency(), CurrencyCode()); + } + CoincenterCommand ret(CoincenterCommandType::kMarkets); + ret.setCur1(market.base()).setCur2(market.quote()).setExchangeNames(optionParser.parseExchanges()); + return ret; +} + +CoincenterCommand CoincenterCommandFactory::createOrderCommand(CoincenterCommandType type, + StringOptionParser &optionParser) { + auto market = optionParser.parseMarket(StringOptionParser::FieldIs::kOptional); + if (market.isNeutral()) { + market = Market(optionParser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + } + CoincenterCommand ret(type); + ret.setOrdersConstraints( + OrdersConstraints(market.base(), market.quote(), std::chrono::duration_cast(_cmdLineOptions.minAge), + std::chrono::duration_cast(_cmdLineOptions.maxAge), + OrdersConstraints::OrderIdSet(StringOptionParser(_cmdLineOptions.ids).getCSVValues()))) + .setExchangeNames(optionParser.parseExchanges()); + return ret; +} + +CoincenterCommand CoincenterCommandFactory::createTradeCommand(CoincenterCommandType type, + StringOptionParser &optionParser) { + if (!_cmdLineOptions.tradeStrategy.empty() && !_cmdLineOptions.tradePrice.empty()) { + throw invalid_argument("Trade price and trade strategy cannot be set together"); + } + + CoincenterCommand command(type); + command.setTradeOptions(_cmdLineOptions.computeTradeOptions()); + + if (!_cmdLineOptions.sellAll.empty()) { + // sell all - not possible with previous command information (probably unwanted, and dangerous) + command.setAmount(MonetaryAmount(100, optionParser.parseCurrency())) + .setPercentageAmount(true) + .setExchangeNames(optionParser.parseExchanges()); + } else if (!_cmdLineOptions.tradeAll.empty()) { + // trade all - not possible with previous command information (probably unwanted, and dangerous) + auto market = optionParser.parseMarket(); + command.setAmount(MonetaryAmount(100, market.base())) + .setPercentageAmount(true) + .setCur1(market.quote()) + .setExchangeNames(optionParser.parseExchanges()); + } else { + auto [amount, amountType] = optionParser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional); + if (!_cmdLineOptions.isSmartTrade()) { + command.setCur1(optionParser.parseCurrency()); + } + if (amountType == StringOptionParser::AmountType::kNotPresent) { + // trade command with only the destination currency + // we should use previous command information + if (_pPreviousCommand == nullptr) { + throw invalid_argument("No previous command to deduce information for trade"); + } + if (_cmdLineOptions.isSmartTrade() && _cmdLineOptions.sell.empty()) { + throw invalid_argument("No amount / exchanges is only possible for smart sell"); + } + } else { + if (amount <= 0) { + throw invalid_argument("Start trade amount should be positive"); + } + command.setAmount(amount) + .setPercentageAmount(amountType == StringOptionParser::AmountType::kPercentage) + .setExchangeNames(optionParser.parseExchanges()); + } + } + + return command; +} + +CoincenterCommand CoincenterCommandFactory::createWithdrawApplyCommand(StringOptionParser &optionParser) { + auto [amount, amountType] = optionParser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional); + auto exchanges = optionParser.parseExchanges('-'); + if (amountType == StringOptionParser::AmountType::kNotPresent) { + if (_pPreviousCommand == nullptr) { + throw invalid_argument("No previous command to deduce origin exchange for withdrawal"); + } + if (exchanges.size() != 1U) { + throw invalid_argument("One destination exchange should be provided for withdraw with previous command"); + } + const auto &previousCommand = *_pPreviousCommand; + if (!IsAnyTrade(previousCommand.type())) { + throw invalid_argument("Previous command for withdrawal should be an any trade type"); + } + } else if (exchanges.size() != 2U) { + throw invalid_argument("Exactly 2 exchanges 'from-to' should be provided for withdraw"); + } + CoincenterCommand command(CoincenterCommandType::kWithdrawApply); + command.setPercentageAmount(amountType == StringOptionParser::AmountType::kPercentage) + .setWithdrawOptions(_cmdLineOptions.computeWithdrawOptions()) + .setExchangeNames(std::move(exchanges)); + if (amountType != StringOptionParser::AmountType::kNotPresent) { + command.setAmount(amount); + } + return command; +} + +CoincenterCommand CoincenterCommandFactory::createWithdrawApplyAllCommand(StringOptionParser &optionParser) { + auto cur = optionParser.parseCurrency(); + auto exchanges = optionParser.parseExchanges('-'); + if (exchanges.size() != 2U || cur.isNeutral()) { + throw invalid_argument("Withdraw all expects a currency with a from-to pair of exchanges"); + } + CoincenterCommand command(CoincenterCommandType::kWithdrawApply); + command.setPercentageAmount(true) + .setExchangeNames(std::move(exchanges)) + .setWithdrawOptions(_cmdLineOptions.computeWithdrawOptions()) + .setAmount(MonetaryAmount(100, cur)); + return command; +} +} // namespace cct \ No newline at end of file diff --git a/src/engine/src/coincentercommands.cpp b/src/engine/src/coincentercommands.cpp index 2b78e06d..e2ac50a2 100644 --- a/src/engine/src/coincentercommands.cpp +++ b/src/engine/src/coincentercommands.cpp @@ -7,18 +7,16 @@ #include #include -#include "cct_invalid_argument_exception.hpp" #include "cct_vector.hpp" #include "coincentercommand.hpp" +#include "coincentercommandfactory.hpp" #include "coincentercommandtype.hpp" #include "coincenteroptions.hpp" #include "coincenteroptionsdef.hpp" #include "commandlineoptionsparser.hpp" #include "commandlineoptionsparseriterator.hpp" +#include "currencycode.hpp" #include "depositsconstraints.hpp" -#include "exchangename.hpp" -#include "monetaryamount.hpp" -#include "ordersconstraints.hpp" #include "stringoptionparser.hpp" #include "timedef.hpp" #include "withdrawsconstraints.hpp" @@ -69,28 +67,21 @@ vector CoincenterCommands::ParseOptions(int argc, cons return parsedOptions; } -namespace { -std::pair ParseOrderRequest(const CoincenterCmdLineOptions &cmdLineOptions, - std::string_view orderRequestStr) { - auto currenciesPrivateExchangesTuple = StringOptionParser(orderRequestStr).getCurrenciesPrivateExchanges(false); - auto orderIds = StringOptionParser(cmdLineOptions.ids).getCSVValues(); - return std::make_pair( - OrdersConstraints(std::get<0>(currenciesPrivateExchangesTuple), std::get<1>(currenciesPrivateExchangesTuple), - std::chrono::duration_cast(cmdLineOptions.minAge), - std::chrono::duration_cast(cmdLineOptions.maxAge), - OrdersConstraints::OrderIdSet(std::move(orderIds))), - std::get<2>(currenciesPrivateExchangesTuple)); -} - -} // namespace - CoincenterCommands::CoincenterCommands(std::span cmdLineOptionsSpan) { + _commands.reserve(cmdLineOptionsSpan.size()); + const CoincenterCommand *pPreviousCommand = nullptr; for (const CoincenterCmdLineOptions &cmdLineOptions : cmdLineOptionsSpan) { - addOption(cmdLineOptions); + addOption(cmdLineOptions, pPreviousCommand); + if (!_commands.empty()) { + pPreviousCommand = &_commands.back(); + } } } -bool CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOptions) { +void CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOptions, + const CoincenterCommand *pPreviousCommand) { + // Warning: pPreviousCommand is a pointer into an object in _commands. Do not use after insertion of a new command + // (pointer may be invalidated) if (cmdLineOptions.repeats.isPresent()) { if (cmdLineOptions.repeats.isSet()) { _repeats = *cmdLineOptions.repeats; @@ -102,210 +93,140 @@ bool CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOption _repeatTime = cmdLineOptions.repeatTime; + StringOptionParser optionParser; + CoincenterCommandFactory commandFactory(cmdLineOptions, pPreviousCommand); + if (cmdLineOptions.healthCheck) { - StringOptionParser anyParser(*cmdLineOptions.healthCheck); - _commands.emplace_back(CoincenterCommandType::kHealthCheck).setExchangeNames(anyParser.getExchanges()); + optionParser = StringOptionParser(*cmdLineOptions.healthCheck); + _commands.emplace_back(CoincenterCommandType::kHealthCheck).setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.markets.empty()) { - StringOptionParser anyParser(cmdLineOptions.markets); - auto [cur1, cur2, exchanges] = anyParser.getCurrenciesPublicExchanges(); - _commands.emplace_back(CoincenterCommandType::kMarkets) - .setCur1(cur1) - .setCur2(cur2) - .setExchangeNames(std::move(exchanges)); + optionParser = StringOptionParser(cmdLineOptions.markets); + _commands.push_back(CoincenterCommandFactory::CreateMarketCommand(optionParser)); } if (!cmdLineOptions.orderbook.empty()) { - StringOptionParser anyParser(cmdLineOptions.orderbook); - auto [market, exchanges] = anyParser.getMarketExchanges(); + optionParser = StringOptionParser(cmdLineOptions.orderbook); _commands.emplace_back(CoincenterCommandType::kOrderbook) - .setMarket(market) - .setExchangeNames(std::move(exchanges)) + .setMarket(optionParser.parseMarket()) + .setExchangeNames(optionParser.parseExchanges()) .setDepth(cmdLineOptions.orderbookDepth) .setCur1(cmdLineOptions.orderbookCur); } if (cmdLineOptions.ticker) { - StringOptionParser anyParser(*cmdLineOptions.ticker); - _commands.emplace_back(CoincenterCommandType::kTicker).setExchangeNames(anyParser.getExchanges()); + optionParser = StringOptionParser(*cmdLineOptions.ticker); + _commands.emplace_back(CoincenterCommandType::kTicker).setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.conversionPath.empty()) { - StringOptionParser anyParser(cmdLineOptions.conversionPath); - auto [market, exchanges] = anyParser.getMarketExchanges(); + optionParser = StringOptionParser(cmdLineOptions.conversionPath); _commands.emplace_back(CoincenterCommandType::kConversionPath) - .setMarket(market) - .setExchangeNames(std::move(exchanges)); + .setMarket(optionParser.parseMarket()) + .setExchangeNames(optionParser.parseExchanges()); } if (cmdLineOptions.balance) { - StringOptionParser anyParser(*cmdLineOptions.balance); - auto [balanceCurrencyCode, exchanges] = - anyParser.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kOptional); + optionParser = StringOptionParser(*cmdLineOptions.balance); _commands.emplace_back(CoincenterCommandType::kBalance) - .setCur1(balanceCurrencyCode) + .setCur1(optionParser.parseCurrency(StringOptionParser::FieldIs::kOptional)) .withBalanceInUse(cmdLineOptions.withBalanceInUse) - .setExchangeNames(std::move(exchanges)); + .setExchangeNames(optionParser.parseExchanges()); } - // Parse trade / buy / sell options - // First, check that at most one master trade option is set - // (options would be set for all trades otherwise which is not very intuitive) - if (static_cast(!cmdLineOptions.buy.empty()) + static_cast(!cmdLineOptions.sell.empty()) + - static_cast(!cmdLineOptions.sellAll.empty()) + static_cast(!cmdLineOptions.tradeAll.empty()) > - 1) { - throw invalid_argument("Only one trade can be done at a time"); - } auto [tradeArgs, cmdType] = cmdLineOptions.getTradeArgStr(); if (!tradeArgs.empty()) { - CoincenterCommand &coincenterCommand = _commands.emplace_back(cmdType); - - coincenterCommand.setTradeOptions(cmdLineOptions.computeTradeOptions()); - - StringOptionParser optParser(tradeArgs); - if (cmdLineOptions.isSmartTrade()) { - if (!cmdLineOptions.sellAll.empty()) { - auto [fromTradeCurrency, exchanges] = - optParser.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory); - coincenterCommand.setAmount(MonetaryAmount(100, fromTradeCurrency)) - .setPercentageAmount(true) - .setExchangeNames(std::move(exchanges)); - } else { - auto [amount, isPercentage, exchanges] = optParser.getMonetaryAmountPrivateExchanges(); - if (amount <= 0) { - throw invalid_argument("Start trade amount should be positive"); - } - coincenterCommand.setAmount(amount).setPercentageAmount(isPercentage).setExchangeNames(std::move(exchanges)); - } - } else if (!cmdLineOptions.tradeAll.empty()) { - auto [fromTradeCurrency, toTradeCurrency, exchanges] = optParser.getCurrenciesPrivateExchanges(); - coincenterCommand.setAmount(MonetaryAmount(100, fromTradeCurrency)) - .setPercentageAmount(true) - .setCur1(toTradeCurrency) - .setExchangeNames(std::move(exchanges)); - } else { - auto [startTradeAmount, isPercentage, toTradeCurrency, exchanges] = - optParser.getMonetaryAmountCurrencyPrivateExchanges(); - if (startTradeAmount <= 0) { - throw invalid_argument("Start trade amount should be positive"); - } - coincenterCommand.setAmount(startTradeAmount) - .setPercentageAmount(isPercentage) - .setCur1(toTradeCurrency) - .setExchangeNames(std::move(exchanges)); - } + optionParser = StringOptionParser(tradeArgs); + _commands.push_back(commandFactory.createTradeCommand(cmdType, optionParser)); } if (!cmdLineOptions.depositInfo.empty()) { - StringOptionParser anyParser(cmdLineOptions.depositInfo); - auto [depositCurrency, exchanges] = - anyParser.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory); + optionParser = StringOptionParser(cmdLineOptions.depositInfo); _commands.emplace_back(CoincenterCommandType::kDepositInfo) - .setCur1(depositCurrency) - .setExchangeNames(std::move(exchanges)); + .setCur1(optionParser.parseCurrency()) + .setExchangeNames(optionParser.parseExchanges()); } if (cmdLineOptions.openedOrdersInfo) { - auto [ordersConstraints, exchanges] = ParseOrderRequest(cmdLineOptions, *cmdLineOptions.openedOrdersInfo); - _commands.emplace_back(CoincenterCommandType::kOrdersOpened) - .setOrdersConstraints(std::move(ordersConstraints)) - .setExchangeNames(std::move(exchanges)); + optionParser = StringOptionParser(*cmdLineOptions.openedOrdersInfo); + _commands.push_back(commandFactory.createOrderCommand(CoincenterCommandType::kOrdersOpened, optionParser)); } if (cmdLineOptions.cancelOpenedOrders) { - auto [ordersConstraints, exchanges] = ParseOrderRequest(cmdLineOptions, *cmdLineOptions.cancelOpenedOrders); - _commands.emplace_back(CoincenterCommandType::kOrdersCancel) - .setOrdersConstraints(std::move(ordersConstraints)) - .setExchangeNames(std::move(exchanges)); + optionParser = StringOptionParser(*cmdLineOptions.cancelOpenedOrders); + _commands.push_back(commandFactory.createOrderCommand(CoincenterCommandType::kOrdersCancel, optionParser)); } if (cmdLineOptions.recentDepositsInfo) { - auto [currencyCode, exchanges] = StringOptionParser(*cmdLineOptions.recentDepositsInfo) - .getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kOptional); - auto depositIds = StringOptionParser(cmdLineOptions.ids).getCSVValues(); - DepositsConstraints depositConstraints(currencyCode, std::chrono::duration_cast(cmdLineOptions.minAge), - std::chrono::duration_cast(cmdLineOptions.maxAge), - DepositsConstraints::IdSet(std::move(depositIds))); + optionParser = StringOptionParser(*cmdLineOptions.recentDepositsInfo); _commands.emplace_back(CoincenterCommandType::kRecentDeposits) - .setDepositsConstraints(std::move(depositConstraints)) - .setExchangeNames(std::move(exchanges)); + .setDepositsConstraints( + DepositsConstraints(optionParser.parseCurrency(StringOptionParser::FieldIs::kOptional), + std::chrono::duration_cast(cmdLineOptions.minAge), + std::chrono::duration_cast(cmdLineOptions.maxAge), + DepositsConstraints::IdSet(StringOptionParser(cmdLineOptions.ids).getCSVValues()))) + .setExchangeNames(optionParser.parseExchanges()); } if (cmdLineOptions.recentWithdrawsInfo) { - auto [currencyCode, exchanges] = StringOptionParser(*cmdLineOptions.recentWithdrawsInfo) - .getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kOptional); - auto withdrawIds = StringOptionParser(cmdLineOptions.ids).getCSVValues(); - WithdrawsConstraints withdrawConstraints(currencyCode, std::chrono::duration_cast(cmdLineOptions.minAge), - std::chrono::duration_cast(cmdLineOptions.maxAge), - WithdrawsConstraints::IdSet(std::move(withdrawIds))); + optionParser = StringOptionParser(*cmdLineOptions.recentWithdrawsInfo); _commands.emplace_back(CoincenterCommandType::kRecentWithdraws) - .setWithdrawsConstraints(std::move(withdrawConstraints)) - .setExchangeNames(std::move(exchanges)); + .setWithdrawsConstraints( + WithdrawsConstraints(optionParser.parseCurrency(StringOptionParser::FieldIs::kOptional), + std::chrono::duration_cast(cmdLineOptions.minAge), + std::chrono::duration_cast(cmdLineOptions.maxAge), + WithdrawsConstraints::IdSet(StringOptionParser(cmdLineOptions.ids).getCSVValues()))) + .setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.withdrawApply.empty()) { - StringOptionParser anyParser(cmdLineOptions.withdrawApply); - auto [amountToWithdraw, isPercentageWithdraw, exchanges] = anyParser.getMonetaryAmountFromToPrivateExchange(); - _commands.emplace_back(CoincenterCommandType::kWithdrawApply) - .setAmount(amountToWithdraw) - .setPercentageAmount(isPercentageWithdraw) - .setExchangeNames(std::move(exchanges)) - .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()); + optionParser = StringOptionParser(cmdLineOptions.withdrawApply); + _commands.push_back(commandFactory.createWithdrawApplyCommand(optionParser)); } if (!cmdLineOptions.withdrawApplyAll.empty()) { - StringOptionParser anyParser(cmdLineOptions.withdrawApplyAll); - auto [curToWithdraw, exchanges] = anyParser.getCurrencyFromToPrivateExchange(); - _commands.emplace_back(CoincenterCommandType::kWithdrawApply) - .setAmount(MonetaryAmount(100, curToWithdraw)) - .setPercentageAmount(true) - .setExchangeNames(std::move(exchanges)) - .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()); + optionParser = StringOptionParser(cmdLineOptions.withdrawApplyAll); + _commands.push_back(commandFactory.createWithdrawApplyAllCommand(optionParser)); } if (!cmdLineOptions.dustSweeper.empty()) { - StringOptionParser anyParser(cmdLineOptions.dustSweeper); - auto [currencyCode, exchanges] = anyParser.getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory); + optionParser = StringOptionParser(cmdLineOptions.dustSweeper); _commands.emplace_back(CoincenterCommandType::kDustSweeper) - .setCur1(currencyCode) - .setExchangeNames(std::move(exchanges)); + .setCur1(optionParser.parseCurrency()) + .setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.withdrawFee.empty()) { - StringOptionParser anyParser(cmdLineOptions.withdrawFee); - auto [withdrawFeeCur, exchanges] = anyParser.getCurrencyPublicExchanges(); + optionParser = StringOptionParser(cmdLineOptions.withdrawFee); _commands.emplace_back(CoincenterCommandType::kWithdrawFee) - .setCur1(withdrawFeeCur) - .setExchangeNames(std::move(exchanges)); + .setCur1(optionParser.parseCurrency()) + .setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.last24hTradedVolume.empty()) { - StringOptionParser anyParser(cmdLineOptions.last24hTradedVolume); - auto [tradedVolumeMarket, exchanges] = anyParser.getMarketExchanges(); + optionParser = StringOptionParser(cmdLineOptions.last24hTradedVolume); _commands.emplace_back(CoincenterCommandType::kLast24hTradedVolume) - .setMarket(tradedVolumeMarket) - .setExchangeNames(std::move(exchanges)); + .setMarket(optionParser.parseMarket()) + .setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.lastTrades.empty()) { - StringOptionParser anyParser(cmdLineOptions.lastTrades); - auto [lastTradesMarket, exchanges] = anyParser.getMarketExchanges(); + optionParser = StringOptionParser(cmdLineOptions.lastTrades); _commands.emplace_back(CoincenterCommandType::kLastTrades) - .setMarket(lastTradesMarket) + .setMarket(optionParser.parseMarket()) .setNbLastTrades(cmdLineOptions.nbLastTrades) - .setExchangeNames(std::move(exchanges)); + .setExchangeNames(optionParser.parseExchanges()); } if (!cmdLineOptions.lastPrice.empty()) { - StringOptionParser anyParser(cmdLineOptions.lastPrice); - auto [lastPriceMarket, exchanges] = anyParser.getMarketExchanges(); + optionParser = StringOptionParser(cmdLineOptions.lastPrice); _commands.emplace_back(CoincenterCommandType::kLastPrice) - .setMarket(lastPriceMarket) - .setExchangeNames(std::move(exchanges)); + .setMarket(optionParser.parseMarket()) + .setExchangeNames(optionParser.parseExchanges()); } - return true; + optionParser.checkEndParsing(); // No more option part should be remaining } } // namespace cct diff --git a/src/engine/src/coincenterinfo_create.cpp b/src/engine/src/coincenterinfo_create.cpp index 1abb7639..061b6ca1 100644 --- a/src/engine/src/coincenterinfo_create.cpp +++ b/src/engine/src/coincenterinfo_create.cpp @@ -88,9 +88,9 @@ CoincenterInfo CoincenterInfo_Create(std::string_view programName, const Coincen ExchangeSecretsInfo ExchangeSecretsInfo_Create(const CoincenterCmdLineOptions &cmdLineOptions) { if (cmdLineOptions.noSecrets) { StringOptionParser anyParser(*cmdLineOptions.noSecrets); - return ExchangeSecretsInfo(anyParser.getExchanges()); + return ExchangeSecretsInfo(anyParser.parseExchanges()); } return {}; } -} // namespace cct \ No newline at end of file +} // namespace cct diff --git a/src/engine/src/stringoptionparser.cpp b/src/engine/src/stringoptionparser.cpp index 7b74b5f4..c6742abc 100644 --- a/src/engine/src/stringoptionparser.cpp +++ b/src/engine/src/stringoptionparser.cpp @@ -1,14 +1,10 @@ #include "stringoptionparser.hpp" -#include -#include #include #include -#include #include #include "cct_cctype.hpp" -#include "cct_const.hpp" #include "cct_invalid_argument_exception.hpp" #include "cct_string.hpp" #include "cct_vector.hpp" @@ -16,32 +12,9 @@ #include "exchangename.hpp" #include "market.hpp" #include "monetaryamount.hpp" -#include "toupperlower.hpp" namespace cct { namespace { -ExchangeNames GetExchanges(std::string_view str) { - ExchangeNames exchanges; - if (!str.empty()) { - std::size_t first; - std::size_t last; - for (first = 0, last = str.find(','); last != std::string_view::npos; last = str.find(',', last + 1)) { - exchanges.emplace_back(std::string_view(str.begin() + first, str.begin() + last)); - first = last + 1; - } - exchanges.emplace_back(std::string_view(str.begin() + first, str.end())); - } - return exchanges; -} - -std::string_view StrEnd(std::string_view opt, std::size_t startPos) { return {opt.begin() + startPos, opt.end()}; } - -bool IsExchangeName(std::string_view str) { - string lowerStr = ToLower(str); - return std::ranges::any_of(kSupportedExchanges, [&lowerStr](std::string_view ex) { - return lowerStr.starts_with(ex) && (lowerStr.size() == ex.size() || lowerStr[ex.size()] == '_'); - }); -} template std::string_view GetNextStr(std::string_view opt, CharOrStringType sep, std::size_t &pos) { @@ -63,199 +36,159 @@ std::string_view GetNextStr(std::string_view opt, CharOrStringType sep, std::siz return {opt.begin() + begPos, opt.begin() + endPos}; } -auto GetNextPercentageAmount(std::string_view opt, std::string_view sepWithPercentageAtLast, std::size_t &pos) { - auto amountStr = GetNextStr(opt, sepWithPercentageAtLast, pos); - - if (amountStr.empty()) { - if (pos == opt.size()) { - throw invalid_argument("Expected a start amount"); - } - // We matched one '-' representing a negative number - amountStr = GetNextStr(opt, sepWithPercentageAtLast, pos); - amountStr = std::string_view(amountStr.data() - 1U, amountStr.size() + 1U); - } - MonetaryAmount startAmount(amountStr); - bool isPercentage = pos > 0U && pos < opt.size() && opt[pos - 1] == '%'; - if (isPercentage) { - assert(sepWithPercentageAtLast.back() == '%'); - std::string_view sepWithoutPercentage(sepWithPercentageAtLast.begin(), sepWithPercentageAtLast.end() - 1); - startAmount = MonetaryAmount(startAmount, CurrencyCode(GetNextStr(opt, sepWithoutPercentage, pos))); - if (startAmount.abs().toNeutral() > MonetaryAmount(100)) { - throw invalid_argument("A percentage cannot be larger than 100"); - } - } - return std::make_pair(std::move(startAmount), isPercentage); -} - -template -auto GetNextExchangeName(std::string_view opt, CharOrStringType sep, std::size_t &pos) { - auto nextStr = GetNextStr(opt, sep, pos); - if (nextStr.empty()) { - throw invalid_argument("Expected an exchange identifier in '{}'", opt); - } - return ExchangeName(nextStr); -} - } // namespace -ExchangeNames StringOptionParser::getExchanges() const { return GetExchanges(_opt); } - -StringOptionParser::MarketExchanges StringOptionParser::getMarketExchanges() const { - std::size_t commaPos = getNextCommaPos(0, false); - std::string_view marketStr(_opt.begin(), commaPos == std::string_view::npos ? _opt.end() : _opt.begin() + commaPos); - std::size_t dashPos = marketStr.find('-'); - if (dashPos == std::string_view::npos) { - throw invalid_argument("Expected a dash"); - } - std::size_t startExchangesPos = - commaPos == std::string_view::npos ? _opt.size() : _opt.find_first_not_of(' ', commaPos + 1); - - return MarketExchanges{Market(CurrencyCode(std::string_view(marketStr.begin(), marketStr.begin() + dashPos)), - CurrencyCode(std::string_view(marketStr.begin() + dashPos + 1, marketStr.end()))), - GetExchanges(StrEnd(_opt, startExchangesPos))}; -} - -StringOptionParser::CurrencyPrivateExchanges StringOptionParser::getCurrencyPrivateExchanges( - CurrencyIs currencyIs) const { - std::string_view exchangesStr = _opt; +// At the end of the currency, either the end of the string or a comma is expected. +CurrencyCode StringOptionParser::parseCurrency(FieldIs fieldIs) { + const std::size_t commaPos = _opt.find(',', _pos); + const auto begIt = _opt.begin() + _pos; + const bool isCommaPresent = commaPos != std::string_view::npos; + const std::string_view firstStr(begIt, isCommaPresent ? _opt.begin() + commaPos : _opt.end()); std::string_view curStr; - std::size_t commaPos = getNextCommaPos(0, false); - std::string_view firstStr(_opt.data(), commaPos == std::string_view::npos ? _opt.size() : commaPos); - if (!firstStr.empty() && !IsExchangeName(firstStr)) { + if (!firstStr.empty() && !ExchangeName::IsValid(firstStr) && CurrencyCode::IsValid(firstStr)) { + // disambiguate currency code from exchange name curStr = firstStr; - if (firstStr.size() == _opt.size()) { - exchangesStr = std::string_view(); - } else { - exchangesStr = std::string_view(_opt.begin() + commaPos + 1, _opt.end()); + _pos += curStr.size(); + if (isCommaPresent) { + ++_pos; } + } else if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a valid currency code in '{}'", std::string_view(_opt.begin() + _pos, _opt.end())); } - if (curStr.empty() && currencyIs == CurrencyIs::kMandatory) { - throw invalid_argument("Expected a currency code first"); - } - - return {CurrencyCode(curStr), GetExchanges(exchangesStr)}; + return curStr; } -StringOptionParser::CurrenciesPrivateExchanges StringOptionParser::getCurrenciesPrivateExchanges( - bool currenciesShouldBeSet) const { - std::size_t dashPos = _opt.find('-', 1); - CurrencyCode fromTradeCurrency; - CurrencyCode toTradeCurrency; - std::size_t startExchangesPos = 0; - if (_opt.empty()) { - // Do nothing - } else if (dashPos == std::string_view::npos) { - // There is no dash, ambiguity to be resolved, assuming there is no crypto acronym with the same name as an exchange - std::size_t pos = 0; - std::string_view token1 = GetNextStr(_opt, ',', pos); - if (!IsExchangeName(token1)) { - startExchangesPos = pos; - fromTradeCurrency = CurrencyCode(token1); - std::string_view token2 = GetNextStr(_opt, ',', pos); - if (!IsExchangeName(token2)) { - startExchangesPos = pos; - toTradeCurrency = CurrencyCode(token2); - } +// At the end of the market, either the end of the string or a comma is expected. +Market StringOptionParser::parseMarket(FieldIs fieldIs) { + const std::size_t commaPos = _opt.find(',', _pos); + const auto begIt = _opt.begin() + _pos; + const bool isCommaPresent = commaPos != std::string_view::npos; + const std::string_view marketStr(begIt, isCommaPresent ? begIt + commaPos : _opt.end()); + const std::size_t dashPos = marketStr.find('-'); + if (dashPos == std::string_view::npos) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a dash in '{}'", std::string_view(_opt.begin() + _pos, _opt.end())); } - } else { - // No ambiguity possible, both currencies are set from first position - fromTradeCurrency = CurrencyCode(GetNextStr(_opt, '-', startExchangesPos)); - toTradeCurrency = CurrencyCode(GetNextStr(_opt, ',', startExchangesPos)); + return {}; } - if (currenciesShouldBeSet && (fromTradeCurrency.isNeutral() || toTradeCurrency.isNeutral())) { - throw invalid_argument("Expected a dash"); + std::string_view firstCur(marketStr.begin(), marketStr.begin() + dashPos); + if (!CurrencyCode::IsValid(firstCur)) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a valid first currency in '{}'", + std::string_view(_opt.begin() + _pos, _opt.end())); + } + return {}; } - return std::make_tuple(fromTradeCurrency, toTradeCurrency, GetExchanges(StrEnd(_opt, startExchangesPos))); -} - -StringOptionParser::MonetaryAmountCurrencyPrivateExchanges -StringOptionParser::getMonetaryAmountCurrencyPrivateExchanges(bool withCurrency) const { - std::size_t pos = 0; - auto [startAmount, isPercentage] = GetNextPercentageAmount(_opt, "-,%", pos); - CurrencyCode toTradeCurrency; - if (withCurrency) { - toTradeCurrency = CurrencyCode(GetNextStr(_opt, ',', pos)); + std::string_view secondCur(marketStr.begin() + dashPos + 1, marketStr.end()); + if (!CurrencyCode::IsValid(secondCur)) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a valid second currency in '{}'", + std::string_view(_opt.begin() + _pos, _opt.end())); + } + return {}; } - - return std::make_tuple(startAmount, isPercentage, toTradeCurrency, GetExchanges(GetNextStr(_opt, '\0', pos))); -} - -StringOptionParser::CurrencyFromToPrivateExchange StringOptionParser::getCurrencyFromToPrivateExchange() const { - std::size_t pos = 0; - CurrencyCode cur(GetNextStr(_opt, ',', pos)); - // Warning: in C++, order of evaluation of parameters is unspecified, so parsing of exchanges (with - // GetNextExchangeName) should be done at different lines - ExchangeNames exchangePair(1U, GetNextExchangeName(_opt, '-', pos)); - exchangePair.push_back(GetNextExchangeName(_opt, '-', pos)); - return std::make_pair(std::move(cur), std::move(exchangePair)); + _pos += marketStr.size(); + if (isCommaPresent) { + ++_pos; + } + return {CurrencyCode(firstCur), CurrencyCode(secondCur)}; } -StringOptionParser::MonetaryAmountFromToPrivateExchange StringOptionParser::getMonetaryAmountFromToPrivateExchange() - const { - std::size_t pos = 0; - auto [startAmount, isPercentage] = GetNextPercentageAmount(_opt, ",%", pos); - ExchangeNames exchangePair(1U, GetNextExchangeName(_opt, '-', pos)); - exchangePair.push_back(GetNextExchangeName(_opt, '-', pos)); - return std::make_tuple(std::move(startAmount), isPercentage, std::move(exchangePair)); -} +// At the end of the currency, either the end of the string, or a dash or comma is expected. +std::pair StringOptionParser::parseNonZeroAmount(FieldIs fieldIs) { + constexpr std::string_view sepWithPercentageAtLast = "-,%"; + std::size_t originalPos = _pos; + auto amountStr = GetNextStr(_opt, sepWithPercentageAtLast, _pos); + std::pair ret{MonetaryAmount(), StringOptionParser::AmountType::kNotPresent}; + if (amountStr.empty()) { + if (_pos == _opt.size()) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a non-zero amount in '{}'", + std::string_view(_opt.begin() + originalPos, _opt.end())); + } + _pos = originalPos; + return ret; + } -std::size_t StringOptionParser::getNextCommaPos(std::size_t startPos, bool throwIfNone) const { - std::size_t commaPos = _opt.find(',', startPos); - if (throwIfNone && commaPos == std::string_view::npos) { - throw invalid_argument("Expected a comma"); + // We matched one '-' representing a negative number + amountStr = GetNextStr(_opt, sepWithPercentageAtLast, _pos); + amountStr = std::string_view(amountStr.data() - 1U, amountStr.size() + 1U); } - return commaPos; -} - -StringOptionParser::CurrencyPublicExchanges StringOptionParser::getCurrencyPublicExchanges() const { - std::size_t firstCommaPos = getNextCommaPos(0, false); - CurrencyPublicExchanges ret; - if (firstCommaPos == std::string_view::npos) { - ret.first = CurrencyCode(_opt); - } else { - ret.first = CurrencyCode(std::string_view(_opt.begin(), _opt.begin() + firstCommaPos)); - ret.second = GetExchanges(StrEnd(_opt, firstCommaPos + 1)); + if (ExchangeName::IsValid(amountStr)) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a non-zero amount in '{}'", + std::string_view(_opt.begin() + originalPos, _opt.end())); + } + _pos = originalPos; + return ret; } - return ret; -} - -StringOptionParser::CurrenciesPublicExchanges StringOptionParser::getCurrenciesPublicExchanges() const { - std::size_t firstCommaPos = getNextCommaPos(0, false); - std::size_t dashPos = _opt.find('-', 1); - CurrenciesPublicExchanges ret; - if (firstCommaPos == std::string_view::npos) { - firstCommaPos = _opt.size(); - } else { - std::get<2>(ret) = GetExchanges(StrEnd(_opt, firstCommaPos + 1)); + MonetaryAmount amount(amountStr, MonetaryAmount::IfNoAmount::kNoThrow); + if (amount == 0) { + if (fieldIs == FieldIs::kMandatory) { + throw invalid_argument("Expected a non-zero amount"); + } + _pos = originalPos; + return ret; } - if (dashPos == std::string_view::npos) { - std::get<0>(ret) = CurrencyCode(std::string_view(_opt.data(), firstCommaPos)); + const bool isPercentage = _pos > originalPos && _pos < _opt.size() && _opt[_pos - 1] == '%'; + if (isPercentage) { + const std::string_view sepWithoutPercentage(sepWithPercentageAtLast.begin(), sepWithPercentageAtLast.end() - 1); + amount = MonetaryAmount(amount, CurrencyCode(GetNextStr(_opt, sepWithoutPercentage, _pos))); + if (amount.abs().toNeutral() > MonetaryAmount(100)) { + throw invalid_argument("Invalid percentage in '{}'", std::string_view(_opt.begin() + originalPos, _opt.end())); + } + ret.second = StringOptionParser::AmountType::kPercentage; } else { - std::get<0>(ret) = CurrencyCode(std::string_view(_opt.data(), dashPos)); - std::get<1>(ret) = CurrencyCode(std::string_view(_opt.begin() + dashPos + 1, _opt.begin() + firstCommaPos)); + ret.second = StringOptionParser::AmountType::kAbsolute; } + ret.first = amount; return ret; } -vector StringOptionParser::getCSVValues() const { - std::size_t pos = 0; +vector StringOptionParser::getCSVValues() { vector ret; if (!_opt.empty()) { do { - std::size_t nextCommaPos = getNextCommaPos(pos, false); + auto nextCommaPos = _opt.find(',', _pos); if (nextCommaPos == std::string_view::npos) { nextCommaPos = _opt.size(); } - if (pos != nextCommaPos) { - ret.emplace_back(_opt.begin() + pos, _opt.begin() + nextCommaPos); + if (_pos != nextCommaPos) { + ret.emplace_back(_opt.begin() + _pos, _opt.begin() + nextCommaPos); } if (nextCommaPos == _opt.size()) { break; } - pos = nextCommaPos + 1; + _pos = nextCommaPos + 1; } while (true); } return ret; } +ExchangeNames StringOptionParser::parseExchanges(char sep) { + std::string_view str(_opt.begin() + _pos, _opt.end()); + ExchangeNames exchanges; + if (!str.empty()) { + std::size_t first; + std::size_t last; + for (first = 0, last = str.find(sep); last != std::string_view::npos; last = str.find(sep, last + 1)) { + std::string_view exchangeNameStr(str.begin() + first, str.begin() + last); + exchanges.emplace_back(exchangeNameStr); + first = last + 1; + _pos += exchangeNameStr.size() + 1U; + } + // Add the last one as well + std::string_view exchangeNameStr(str.begin() + first, str.end()); + exchanges.emplace_back(exchangeNameStr); + _pos += exchangeNameStr.size(); + } + return exchanges; +} + +void StringOptionParser::checkEndParsing() const { + if (_pos != _opt.size()) { + throw invalid_argument("{} remaining characters not read", _opt.size() - _pos); + } +} + } // namespace cct diff --git a/src/engine/src/transferablecommandresult.cpp b/src/engine/src/transferablecommandresult.cpp new file mode 100644 index 00000000..618afb36 --- /dev/null +++ b/src/engine/src/transferablecommandresult.cpp @@ -0,0 +1,89 @@ +#include "transferablecommandresult.hpp" + +#include +#include +#include +#include + +#include "cct_exception.hpp" +#include "cct_log.hpp" +#include "coincentercommand.hpp" +#include "exchangename.hpp" +#include "monetaryamount.hpp" + +namespace cct { +namespace { +struct AmountExchangeNames { + MonetaryAmount amount; + ExchangeNames exchangeNames; + + bool operator==(const AmountExchangeNames &) const noexcept = default; +}; + +std::optional AccumulateAmount( + std::span previousTransferableResults) { + std::optional ret; + for (const TransferableCommandResult &previousResult : previousTransferableResults) { + const auto previousAmount = previousResult.resultedAmount(); + if (!ret) { + ret = {previousAmount, ExchangeNames{}}; + } else if (ret->amount.currencyCode() == previousAmount.currencyCode()) { + ret->amount += previousAmount; + } else { + ret.reset(); + break; + } + + auto exchangeName = previousResult.targetedExchange(); + const auto insertIt = std::ranges::lower_bound(ret->exchangeNames, exchangeName); + if (insertIt == ret->exchangeNames.end() || *insertIt != exchangeName) { + ret->exchangeNames.insert(insertIt, std::move(exchangeName)); + } + } + return ret; +} +} // namespace + +std::pair ComputeTradeAmountAndExchanges( + const CoincenterCommand &cmd, std::span previousTransferableResults) { + // 2 input styles are possible: + // - standard full information with an amount to trade, a destination currency and an optional list of exchanges + // where to trade + // - a currency - the destination one, and start amount and exchange(s) should come from previous command result + if (cmd.amount().isDefault() && !cmd.isPercentageAmount() && cmd.exchangeNames().empty()) { + // take information from previous results + auto optAmountExchangeNames = AccumulateAmount(previousTransferableResults); + if (!optAmountExchangeNames) { + log::error("Skipping trade as there are multiple currencies in previous resulted amounts"); + return {}; + } + return {optAmountExchangeNames->amount, std::move(optAmountExchangeNames->exchangeNames)}; + } + if (!cmd.amount().isDefault()) { + return {cmd.amount(), cmd.exchangeNames()}; + } + throw exception("Invalid flow for trade, should not happen (error should have been caught previously)"); +} + +std::pair ComputeWithdrawAmount( + const CoincenterCommand &cmd, std::span previousTransferableResults) { + // 2 input styles are possible: + // - standard full information with an amount to withdraw, and a couple of source - destination exchanges + // - a single exchange (which is the target one) with the source and amount information coming from previous + // command result + if (cmd.exchangeNames().size() == 1U && cmd.amount().isDefault()) { + if (previousTransferableResults.size() != 1U) { + log::error("Skipping withdraw apply all to {} as invalid previous transferable results size {}, expected 1", + cmd.exchangeNames().back(), cmd.exchangeNames().size()); + return {}; + } + const TransferableCommandResult &previousResult = previousTransferableResults.front(); + + return {previousResult.resultedAmount(), previousResult.targetedExchange()}; + } + if (cmd.exchangeNames().size() == 2U && !cmd.amount().isDefault()) { + return {cmd.amount(), cmd.exchangeNames().front()}; + } + throw exception("Invalid flow for withdraw apply, should not happen (error should have been caught previously)"); +} +} // namespace cct \ No newline at end of file diff --git a/src/engine/test/coincentercommandfactory_test.cpp b/src/engine/test/coincentercommandfactory_test.cpp new file mode 100644 index 00000000..0cebd086 --- /dev/null +++ b/src/engine/test/coincentercommandfactory_test.cpp @@ -0,0 +1,217 @@ +#include "coincentercommandfactory.hpp" + +#include + +#include "cct_invalid_argument_exception.hpp" +#include "coincentercommand.hpp" +#include "coincentercommandtype.hpp" +#include "currencycode.hpp" +#include "exchangename.hpp" +#include "monetaryamount.hpp" +#include "ordersconstraints.hpp" +#include "stringoptionparser.hpp" + +namespace cct { + +class CoincenterCommandFactoryTest : public ::testing::Test { + protected: + StringOptionParser &inputStr(std::string_view str) { + optionParser = StringOptionParser(str); + return optionParser; + } + + StringOptionParser optionParser; + CoincenterCmdLineOptions cmdLineOptions; + const CoincenterCommand *pPreviousCommand{}; + CoincenterCommandFactory commandFactory{cmdLineOptions, pPreviousCommand}; +}; + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandInvalidInputTest) { + EXPECT_THROW(CoincenterCommandFactory::CreateMarketCommand(inputStr("kucoin")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandMarketTest) { + EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("eth-usdt")), + CoincenterCommand(CoincenterCommandType::kMarkets).setCur1("ETH").setCur2("USDT")); +} + +TEST_F(CoincenterCommandFactoryTest, CreateMarketCommandSingleCurTest) { + EXPECT_EQ(CoincenterCommandFactory::CreateMarketCommand(inputStr("XLM,kraken,binance_user1")), + CoincenterCommand(CoincenterCommandType::kMarkets) + .setCur1("XLM") + .setExchangeNames(ExchangeNames({ExchangeName("kraken"), ExchangeName("binance", "user1")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandAll) { + CoincenterCommandType type = CoincenterCommandType::kOrdersOpened; + EXPECT_EQ(commandFactory.createOrderCommand(type, inputStr("")), CoincenterCommand(type)); +} + +TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandSingleCur) { + CoincenterCommandType type = CoincenterCommandType::kOrdersOpened; + EXPECT_EQ(commandFactory.createOrderCommand(type, inputStr("AVAX")), + CoincenterCommand(type).setOrdersConstraints(OrdersConstraints("AVAX"))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateOrderCommandMarketWithExchange) { + CoincenterCommandType type = CoincenterCommandType::kOrdersOpened; + EXPECT_EQ(commandFactory.createOrderCommand(type, inputStr("AVAX-BTC,huobi")), + CoincenterCommand(type) + .setOrdersConstraints(OrdersConstraints("AVAX", "BTC")) + .setExchangeNames(ExchangeNames({ExchangeName("huobi")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateTradeInvalidNegativeAmount) { + CoincenterCommandType type = CoincenterCommandType::kTrade; + EXPECT_THROW(commandFactory.createTradeCommand(type, inputStr("-13XRP-BTC,binance_user2")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateTradeInvalidSeveralTrades) { + CoincenterCommandType type = CoincenterCommandType::kTrade; + cmdLineOptions.buy = "100%USDT"; + EXPECT_THROW(commandFactory.createTradeCommand(type, inputStr("13XRP-BTC,binance_user2")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateTradeAbsolute) { + CoincenterCommandType type = CoincenterCommandType::kTrade; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("13XRP-BTC,binance_user2")), + CoincenterCommand(type) + .setTradeOptions(cmdLineOptions.computeTradeOptions()) + .setAmount(MonetaryAmount("13XRP")) + .setPercentageAmount(false) + .setCur1("BTC") + .setExchangeNames(ExchangeNames({ExchangeName("binance", "user2")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateTradePercentage) { + CoincenterCommandType type = CoincenterCommandType::kTrade; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("67.906%eth-USDT,huobi,upbit_user1")), + CoincenterCommand(type) + .setTradeOptions(cmdLineOptions.computeTradeOptions()) + .setAmount(MonetaryAmount("67.906ETH")) + .setPercentageAmount(true) + .setCur1("USDT") + .setExchangeNames(ExchangeNames({ExchangeName("huobi"), ExchangeName("upbit", "user1")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateBuyCommand) { + CoincenterCommandType type = CoincenterCommandType::kBuy; + cmdLineOptions.buy = "whatever"; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("804XLM")), + CoincenterCommand(type) + .setTradeOptions(cmdLineOptions.computeTradeOptions()) + .setAmount(MonetaryAmount("804XLM"))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateSellCommand) { + CoincenterCommandType type = CoincenterCommandType::kSell; + cmdLineOptions.sell = "whatever"; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("0.76BTC,bithumb")), + CoincenterCommand(type) + .setTradeOptions(cmdLineOptions.computeTradeOptions()) + .setAmount(MonetaryAmount("0.76BTC")) + .setExchangeNames(ExchangeNames({ExchangeName("bithumb")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateSellWithPreviousInvalidCommand) { + CoincenterCommandType type = CoincenterCommandType::kSell; + cmdLineOptions.sell = "whatever"; + EXPECT_THROW(commandFactory.createTradeCommand(type, inputStr("")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateSellAllCommand) { + CoincenterCommandType type = CoincenterCommandType::kSell; + cmdLineOptions.sellAll = "whatever"; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("DOGE")), + CoincenterCommand(type) + .setTradeOptions(cmdLineOptions.computeTradeOptions()) + .setPercentageAmount(true) + .setAmount(MonetaryAmount(100, "DOGE"))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawInvalidNoPrevious) { + EXPECT_THROW(commandFactory.createWithdrawApplyCommand(inputStr("")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawWithLessThan2Exchanges) { + EXPECT_THROW(commandFactory.createWithdrawApplyCommand(inputStr("kraken")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawWithMoreThan2Exchanges) { + EXPECT_THROW(commandFactory.createWithdrawApplyCommand(inputStr("bithumb-upbit_user3-kucoin")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAbsoluteValid) { + EXPECT_EQ(commandFactory.createWithdrawApplyCommand(inputStr("5000XRP,binance_user1-kucoin_user2")), + CoincenterCommand(CoincenterCommandType::kWithdrawApply) + .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) + .setAmount(MonetaryAmount("5000XRP")) + .setExchangeNames(ExchangeNames({ExchangeName("binance", "user1"), ExchangeName("kucoin", "user2")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawPercentageValid) { + EXPECT_EQ(commandFactory.createWithdrawApplyCommand(inputStr("43.25%ltc,bithumb-kraken")), + CoincenterCommand(CoincenterCommandType::kWithdrawApply) + .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) + .setAmount(MonetaryAmount("43.25LTC")) + .setPercentageAmount(true) + .setExchangeNames(ExchangeNames({ExchangeName("bithumb"), ExchangeName("kraken")}))); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllNoCurrencyInvalid) { + EXPECT_THROW(commandFactory.createWithdrawApplyAllCommand(inputStr("binance_user2-kraken")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllLessThan2ExchangesInvalid) { + EXPECT_THROW(commandFactory.createWithdrawApplyAllCommand(inputStr("bithumb_user4")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllMoreThan2ExchangesInvalid) { + EXPECT_THROW(commandFactory.createWithdrawApplyAllCommand(inputStr("binance-kucoin-kraken-upbit")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryTest, CreateWithdrawAllValid) { + EXPECT_EQ(commandFactory.createWithdrawApplyAllCommand(inputStr("sol,upbit-kraken")), + CoincenterCommand(CoincenterCommandType::kWithdrawApply) + .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) + .setAmount(MonetaryAmount(100, "SOL")) + .setPercentageAmount(true) + .setExchangeNames(ExchangeNames({ExchangeName("upbit"), ExchangeName("kraken")}))); +} + +class CoincenterCommandFactoryWithPreviousTest : public ::testing::Test { + protected: + StringOptionParser &inputStr(std::string_view str) { + optionParser = StringOptionParser(str); + return optionParser; + } + + StringOptionParser optionParser; + CoincenterCmdLineOptions cmdLineOptions; + CoincenterCommand previousCommand{CoincenterCommandType::kTrade}; + CoincenterCommandFactory commandFactory{cmdLineOptions, &previousCommand}; +}; + +TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateSellWithPreviousCommand) { + CoincenterCommandType type = CoincenterCommandType::kSell; + cmdLineOptions.sell = "whatever"; + EXPECT_EQ(commandFactory.createTradeCommand(type, inputStr("")), + CoincenterCommand(type).setTradeOptions(cmdLineOptions.computeTradeOptions())); +} + +TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateWithdrawInvalidNoExchange) { + EXPECT_THROW(commandFactory.createWithdrawApplyCommand(inputStr("")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateWithdrawInvalidMoreThan1Exchange) { + EXPECT_THROW(commandFactory.createWithdrawApplyCommand(inputStr("kucoin-huobi")), invalid_argument); +} + +TEST_F(CoincenterCommandFactoryWithPreviousTest, CreateWithdrawWithPreviousValid) { + EXPECT_EQ(commandFactory.createWithdrawApplyCommand(inputStr("kraken_user1")), + CoincenterCommand(CoincenterCommandType::kWithdrawApply) + .setWithdrawOptions(cmdLineOptions.computeWithdrawOptions()) + .setExchangeNames(ExchangeNames({ExchangeName("kraken", "user1")}))); +} + +} // namespace cct \ No newline at end of file diff --git a/src/engine/test/coincenteroptions_test.cpp b/src/engine/test/coincenteroptions_test.cpp index 9aa5ad3c..f8660034 100644 --- a/src/engine/test/coincenteroptions_test.cpp +++ b/src/engine/test/coincenteroptions_test.cpp @@ -145,4 +145,4 @@ TEST_F(CoincenterCmdLineOptionsTest, ComputeTradeArgStrBuy) { opts.buy = "some value"; EXPECT_EQ(opts.getTradeArgStr(), std::make_pair(opts.buy, CoincenterCommandType::kBuy)); } -} // namespace cct \ No newline at end of file +} // namespace cct diff --git a/src/engine/test/exchangedata_test.hpp b/src/engine/test/exchangedata_test.hpp index 7b782115..36d9f2ce 100644 --- a/src/engine/test/exchangedata_test.hpp +++ b/src/engine/test/exchangedata_test.hpp @@ -7,7 +7,6 @@ #include "exchange.hpp" #include "exchangeprivateapi_mock.hpp" #include "exchangepublicapi_mock.hpp" -#include "exchangepublicapitypes.hpp" namespace cct { diff --git a/src/engine/test/stringoptionparser_test.cpp b/src/engine/test/stringoptionparser_test.cpp index 7d946e21..3ed0023f 100644 --- a/src/engine/test/stringoptionparser_test.cpp +++ b/src/engine/test/stringoptionparser_test.cpp @@ -2,7 +2,6 @@ #include -#include #include #include "cct_invalid_argument_exception.hpp" @@ -14,193 +13,184 @@ #include "monetaryamount.hpp" namespace cct { - -TEST(StringOptionParserTest, GetExchanges) { - EXPECT_TRUE(StringOptionParser("").getExchanges().empty()); - EXPECT_EQ(StringOptionParser("kraken,upbit").getExchanges(), +namespace { +constexpr auto optional = StringOptionParser::FieldIs::kOptional; +constexpr auto mandatory = StringOptionParser::FieldIs::kMandatory; +} // namespace + +TEST(StringOptionParserTest, ParseExchangesDefaultSeparator) { + EXPECT_TRUE(StringOptionParser("").parseExchanges().empty()); + EXPECT_EQ(StringOptionParser("kraken,upbit").parseExchanges(), ExchangeNames({ExchangeName("kraken"), ExchangeName("upbit")})); - EXPECT_EQ(StringOptionParser("huobi_user1").getExchanges(), ExchangeNames({ExchangeName("huobi_user1")})); + EXPECT_EQ(StringOptionParser("huobi_user1").parseExchanges(), ExchangeNames({ExchangeName("huobi_user1")})); } -TEST(StringOptionParserTest, GetCurrencyPrivateExchanges) { - auto optionalCur = StringOptionParser::CurrencyIs::kOptional; - EXPECT_EQ(StringOptionParser("").getCurrencyPrivateExchanges(optionalCur), - std::make_pair(CurrencyCode(), ExchangeNames())); - EXPECT_EQ(StringOptionParser("eur").getCurrencyPrivateExchanges(optionalCur), - std::make_pair(CurrencyCode("EUR"), ExchangeNames())); - EXPECT_EQ(StringOptionParser("kraken1").getCurrencyPrivateExchanges(optionalCur), - std::make_pair(CurrencyCode("kraken1"), ExchangeNames())); - EXPECT_EQ(StringOptionParser("bithumb,binance_user1").getCurrencyPrivateExchanges(optionalCur), - std::make_pair(CurrencyCode(), ExchangeNames({ExchangeName("bithumb"), ExchangeName("binance", "user1")}))); - EXPECT_EQ(StringOptionParser("binance_user2,bithumb,binance_user1").getCurrencyPrivateExchanges(optionalCur), - std::make_pair(CurrencyCode(), ExchangeNames({ExchangeName("binance", "user2"), ExchangeName("bithumb"), - ExchangeName("binance", "user1")}))); - EXPECT_EQ( - StringOptionParser("krw,Bithumb,binance_user1") - .getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory), - std::make_pair(CurrencyCode("KRW"), ExchangeNames({ExchangeName("bithumb"), ExchangeName("binance", "user1")}))); - - EXPECT_THROW(StringOptionParser("toolongcurrency,Bithumb,binance_user1").getCurrencyPrivateExchanges(optionalCur), - invalid_argument); - EXPECT_THROW(StringOptionParser("binance_user1,bithumb") - .getCurrencyPrivateExchanges(StringOptionParser::CurrencyIs::kMandatory), - invalid_argument); +TEST(StringOptionParserTest, ParseExchangesCustomSeparator) { + EXPECT_TRUE(StringOptionParser("").parseExchanges('-').empty()); + EXPECT_EQ(StringOptionParser("kucoin-huobi_user1").parseExchanges('-'), + ExchangeNames({ExchangeName("kucoin"), ExchangeName("huobi", "user1")})); + EXPECT_EQ(StringOptionParser("kraken_user2").parseExchanges('-'), ExchangeNames({ExchangeName("kraken", "user2")})); } -TEST(StringOptionParserTest, GetMarketExchanges) { - EXPECT_EQ(StringOptionParser("eth-eur").getMarketExchanges(), - StringOptionParser::MarketExchanges(Market("ETH", "EUR"), ExchangeNames())); - EXPECT_EQ(StringOptionParser("dash-krw,bithumb,upbit").getMarketExchanges(), - StringOptionParser::MarketExchanges(Market("DASH", "KRW"), - ExchangeNames({ExchangeName("bithumb"), ExchangeName("upbit")}))); +TEST(StringOptionParserTest, ParseMarketMandatory) { + EXPECT_EQ(StringOptionParser("eth-eur").parseMarket(mandatory), Market("ETH", "EUR")); + EXPECT_EQ(StringOptionParser("dash-krw,bithumb,upbit").parseMarket(mandatory), Market("DASH", "KRW")); + + EXPECT_THROW(StringOptionParser("dash").parseMarket(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("dash-toolongcurrency,bithumb,upbit").parseMarket(mandatory), invalid_argument); +} - EXPECT_THROW(StringOptionParser("dash-toolongcurrency,bithumb,upbit").getMarketExchanges(), invalid_argument); +TEST(StringOptionParserTest, ParseMarketOptional) { + EXPECT_EQ(StringOptionParser("").parseMarket(optional), Market()); + EXPECT_EQ(StringOptionParser("eth").parseMarket(optional), Market()); + EXPECT_EQ(StringOptionParser("eth,kucoin").parseMarket(optional), Market()); + EXPECT_EQ(StringOptionParser("eth-eur").parseMarket(optional), Market("ETH", "EUR")); + EXPECT_EQ(StringOptionParser("BTC-USDT,bithumb,upbit").parseMarket(optional), Market("BTC", "USDT")); + EXPECT_EQ(StringOptionParser("kraken,upbit").parseMarket(optional), Market()); + EXPECT_EQ(StringOptionParser("dash-toolongcurrency,bithumb,upbit").parseMarket(optional), Market()); } -TEST(StringOptionParserTest, GetMonetaryAmountPrivateExchanges) { - EXPECT_EQ(StringOptionParser("45.09ADA").getMonetaryAmountPrivateExchanges(), - std::make_tuple(MonetaryAmount("45.09ADA"), false, ExchangeNames{})); - EXPECT_EQ(StringOptionParser("15%ADA").getMonetaryAmountPrivateExchanges(), - std::make_tuple(MonetaryAmount("15ADA"), true, ExchangeNames{})); - EXPECT_EQ(StringOptionParser("-0.6509btc,kraken").getMonetaryAmountPrivateExchanges(), - std::make_tuple(MonetaryAmount("-0.6509BTC"), false, ExchangeNames({ExchangeName("kraken")}))); - EXPECT_EQ(StringOptionParser("49%luna,bithumb_my_user").getMonetaryAmountPrivateExchanges(), - std::make_tuple(MonetaryAmount(49, "LUNA"), true, ExchangeNames({ExchangeName("bithumb", "my_user")}))); - EXPECT_EQ(StringOptionParser("10985.4006xlm,huobi,binance_user1").getMonetaryAmountPrivateExchanges(), - std::make_tuple(MonetaryAmount("10985.4006xlm"), false, - ExchangeNames({ExchangeName("huobi"), ExchangeName("binance", "user1")}))); - EXPECT_EQ(StringOptionParser("-7.009%fil,upbit,kucoin_MyUsername,binance").getMonetaryAmountPrivateExchanges(), - std::make_tuple( - MonetaryAmount("-7.009fil"), true, - ExchangeNames({ExchangeName("upbit"), ExchangeName("kucoin", "MyUsername"), ExchangeName("binance")}))); +TEST(StringOptionParserTest, ParseCurrencyMandatory) { + EXPECT_EQ(StringOptionParser("krw,kucoin,binance_user1").parseCurrency(mandatory), CurrencyCode("KRW")); + + EXPECT_THROW(StringOptionParser("").parseCurrency(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("binance_user1,bithumb").parseCurrency(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("toolongcurrency").parseCurrency(mandatory), invalid_argument); } -TEST(StringOptionParserTest, GetMonetaryAmountCurrencyCodePrivateExchanges) { - EXPECT_EQ(StringOptionParser("45.09ADA-eur,bithumb").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("45.09ADA"), false, CurrencyCode("EUR"), - ExchangeNames(1, ExchangeName("bithumb")))); - EXPECT_EQ(StringOptionParser("0.02 btc-xlm,upbit_user1,binance").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("0.02BTC"), false, CurrencyCode("XLM"), - ExchangeNames({ExchangeName("upbit", "user1"), ExchangeName("binance")}))); - EXPECT_EQ(StringOptionParser("2500.5 eur-sol").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("2500.5 EUR"), false, CurrencyCode("SOL"), ExchangeNames())); - EXPECT_EQ( - StringOptionParser("17%eur-sol,kraken").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("17EUR"), true, CurrencyCode("sol"), ExchangeNames(1, ExchangeName("kraken")))); - EXPECT_EQ(StringOptionParser("50.035%btc-KRW,upbit,bithumb_user2").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("50.035 BTC"), true, CurrencyCode("KRW"), - ExchangeNames({ExchangeName("upbit"), ExchangeName("bithumb", "user2")}))); - EXPECT_EQ(StringOptionParser("-056.04%sol-jpy").getMonetaryAmountCurrencyPrivateExchanges(), - std::make_tuple(MonetaryAmount("-56.04sol"), true, CurrencyCode("JPY"), ExchangeNames{})); +TEST(StringOptionParserTest, ParseCurrencyOptional) { + EXPECT_EQ(StringOptionParser("").parseCurrency(optional), CurrencyCode()); + EXPECT_EQ(StringOptionParser("eur").parseCurrency(optional), CurrencyCode("EUR")); + EXPECT_EQ(StringOptionParser("kraken1").parseCurrency(optional), CurrencyCode("kraken1")); + EXPECT_EQ(StringOptionParser("bithumb,binance_user1").parseCurrency(optional), CurrencyCode()); + EXPECT_EQ(StringOptionParser("binance_user2,bithumb,binance_user1").parseCurrency(optional), CurrencyCode()); + EXPECT_EQ(StringOptionParser("toolongcurrency,Bithumb,binance_user1").parseCurrency(optional), CurrencyCode()); } -TEST(StringOptionParserTest, GetMonetaryAmountCurrencyCodePrivateExchangesValidity) { - EXPECT_NO_THROW(StringOptionParser("100 % eur-sol").getMonetaryAmountCurrencyPrivateExchanges()); - EXPECT_NO_THROW(StringOptionParser("-15.709%eur-sol").getMonetaryAmountCurrencyPrivateExchanges()); - EXPECT_THROW(StringOptionParser("").getMonetaryAmountCurrencyPrivateExchanges(), invalid_argument); - EXPECT_THROW(StringOptionParser("100.2% eur-sol").getMonetaryAmountCurrencyPrivateExchanges(), invalid_argument); - EXPECT_THROW(StringOptionParser("-150 %eur-sol").getMonetaryAmountCurrencyPrivateExchanges(), invalid_argument); +TEST(StringOptionParserTest, ParseAmountMandatoryAbsolute) { + EXPECT_EQ(StringOptionParser("45.09ADA").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("45.09ADA"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(StringOptionParser("0.6509btc,kraken").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("0.6509BTC"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(StringOptionParser("10985.4006xlm,huobi,binance_user1").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("10985.4006xlm"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(StringOptionParser("-0.6509btc,kraken").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("-0.6509btc"), StringOptionParser::AmountType::kAbsolute)); + + EXPECT_THROW(StringOptionParser("").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("0BTC").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("eur").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("kraken").parseNonZeroAmount(mandatory), invalid_argument); } -TEST(StringOptionParserTest, GetCurrencyFromToPrivateExchange) { - EXPECT_EQ(StringOptionParser("btc,huobi-kraken").getCurrencyFromToPrivateExchange(), - std::make_pair(CurrencyCode("BTC"), ExchangeNames{ExchangeName("huobi"), ExchangeName("kraken")})); - EXPECT_EQ( - StringOptionParser("XLM,bithumb_user1-binance").getCurrencyFromToPrivateExchange(), - std::make_pair(CurrencyCode("XLM"), ExchangeNames{ExchangeName("bithumb", "user1"), ExchangeName("binance")})); - EXPECT_EQ(StringOptionParser("eth,kraken_user2-huobi_user3").getCurrencyFromToPrivateExchange(), - std::make_pair(CurrencyCode("ETH"), - ExchangeNames{ExchangeName("kraken", "user2"), ExchangeName("huobi", "user3")})); +TEST(StringOptionParserTest, ParseAmountMandatoryPercentage) { + EXPECT_EQ(StringOptionParser("15%ADA").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("15ADA"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(StringOptionParser("49%luna,bithumb_my_user").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount(49, "LUNA"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(StringOptionParser("7.009%fil,upbit,kucoin_MyUsername,binance").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("7.009fil"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(StringOptionParser("-0.009%fil,upbit,kucoin_MyUsername,binance").parseNonZeroAmount(mandatory), + std::make_pair(MonetaryAmount("-0.009fil"), StringOptionParser::AmountType::kPercentage)); + + EXPECT_THROW(StringOptionParser("").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("0%USDT").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("btc").parseNonZeroAmount(mandatory), invalid_argument); + EXPECT_THROW(StringOptionParser("230.009%fil,upbit,kucoin_MyUsername,binance").parseNonZeroAmount(mandatory), + invalid_argument); // > 100 % } -TEST(StringOptionParserTest, GetMonetaryAmountFromToPrivateExchange) { - EXPECT_EQ( - StringOptionParser("0.102btc,huobi-kraken").getMonetaryAmountFromToPrivateExchange(), - std::make_tuple(MonetaryAmount("0.102BTC"), false, ExchangeNames{ExchangeName("huobi"), ExchangeName("kraken")})); - EXPECT_EQ(StringOptionParser("3795541.90XLM,bithumb_user1-binance").getMonetaryAmountFromToPrivateExchange(), - std::make_tuple(MonetaryAmount("3795541.90XLM"), false, - ExchangeNames{ExchangeName("bithumb", "user1"), ExchangeName("binance")})); - EXPECT_EQ(StringOptionParser("4.106eth,kraken_user2-huobi_user3").getMonetaryAmountFromToPrivateExchange(), - std::make_tuple(MonetaryAmount("4.106ETH"), false, - ExchangeNames{ExchangeName("kraken", "user2"), ExchangeName("huobi", "user3")})); - - EXPECT_THROW(StringOptionParser("test").getMonetaryAmountFromToPrivateExchange(), invalid_argument); +TEST(StringOptionParserTest, ParseAmountOptionalAbsolute) { + EXPECT_EQ(StringOptionParser("").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(StringOptionParser("XRP").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(StringOptionParser("15ADA").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount("15ADA"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(StringOptionParser("bithumb_my_user").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(StringOptionParser("7.009fil,upbit,kucoin_MyUsername,binance").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount("7.009fil"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(StringOptionParser("-7.009shib,upbit,kucoin_MyUsername,binance").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount("-7.009shib"), StringOptionParser::AmountType::kAbsolute)); } -TEST(StringOptionParserTest, GetMonetaryAmountPercentageFromToPrivateExchange) { - EXPECT_EQ(StringOptionParser("1%btc,huobi-kraken").getMonetaryAmountFromToPrivateExchange(), - StringOptionParser::MonetaryAmountFromToPrivateExchange( - MonetaryAmount("1BTC"), true, ExchangeNames{ExchangeName("huobi"), ExchangeName("kraken")})); - EXPECT_EQ( - StringOptionParser("90.05%XLM,bithumb_user1-binance").getMonetaryAmountFromToPrivateExchange(), - StringOptionParser::MonetaryAmountFromToPrivateExchange( - MonetaryAmount("90.05XLM"), true, ExchangeNames{ExchangeName("bithumb", "user1"), ExchangeName("binance")})); - EXPECT_EQ(StringOptionParser("-50.758%eth,kraken_user2-huobi_user3").getMonetaryAmountFromToPrivateExchange(), - StringOptionParser::MonetaryAmountFromToPrivateExchange( - MonetaryAmount("-50.758ETH"), true, - ExchangeNames{ExchangeName("kraken", "user2"), ExchangeName("huobi", "user3")})); +TEST(StringOptionParserTest, ParseAmountOptionalPercentage) { + EXPECT_EQ(StringOptionParser("0%ADA").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(StringOptionParser("45.09%ADA").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount("45.09ADA"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(StringOptionParser("0.6509%btc,kraken").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount("0.6509BTC"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(StringOptionParser("huobi,binance_user1").parseNonZeroAmount(optional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(StringOptionParser("-78%btc,kraken").parseNonZeroAmount(), + std::make_pair(MonetaryAmount(-78, "BTC"), StringOptionParser::AmountType::kPercentage)); } -TEST(StringOptionParserTest, GetCurrencyPublicExchanges) { - using CurrencyPublicExchanges = StringOptionParser::CurrencyPublicExchanges; - EXPECT_EQ(StringOptionParser("btc").getCurrencyPublicExchanges(), CurrencyPublicExchanges("BTC", ExchangeNames())); - EXPECT_EQ(StringOptionParser("eur,kraken_user1").getCurrencyPublicExchanges(), - CurrencyPublicExchanges("EUR", ExchangeNames({ExchangeName("kraken_user1")}))); - EXPECT_EQ(StringOptionParser("eur,binance,huobi").getCurrencyPublicExchanges(), - CurrencyPublicExchanges("EUR", ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); +TEST(StringOptionParserTest, CSVValues) { + EXPECT_EQ(StringOptionParser("").getCSVValues(), vector()); + EXPECT_EQ(StringOptionParser("val1,").getCSVValues(), vector{{"val1"}}); + EXPECT_EQ(StringOptionParser("val1,value").getCSVValues(), vector({{"val1"}, {"value"}})); } -TEST(StringOptionParserTest, GetCurrencyCodesPublicExchanges) { - using CurrencyCodesPublicExchanges = StringOptionParser::CurrenciesPublicExchanges; - EXPECT_EQ(StringOptionParser("btc").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("BTC", CurrencyCode(), ExchangeNames())); - EXPECT_EQ(StringOptionParser("eur,kraken_user1").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("EUR", CurrencyCode(), ExchangeNames({ExchangeName("kraken_user1")}))); - EXPECT_EQ(StringOptionParser("eur,binance,huobi").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("EUR", CurrencyCode(), - ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); - - EXPECT_EQ(StringOptionParser("avax-btc").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("AVAX", "BTC", ExchangeNames())); - EXPECT_EQ(StringOptionParser("btc-eur,kraken_user1").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("BTC", "EUR", ExchangeNames({ExchangeName("kraken_user1")}))); - EXPECT_EQ( - StringOptionParser("xlm-eur,binance,huobi").getCurrenciesPublicExchanges(), - CurrencyCodesPublicExchanges("XLM", "EUR", ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); +TEST(StringOptionParserTest, AmountExchangesFlow) { + StringOptionParser parser("34.8XRP,kraken,huobi_long_user1"); + + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount("34.8XRP"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + + EXPECT_EQ(parser.parseExchanges(','), ExchangeNames({ExchangeName("kraken"), ExchangeName("huobi", "long_user1")})); + + EXPECT_NO_THROW(parser.checkEndParsing()); } -TEST(StringOptionParserTest, GetCurrenciesPrivateExchanges) { - using CurrenciesPrivateExchanges = StringOptionParser::CurrenciesPrivateExchanges; - EXPECT_EQ(StringOptionParser("").getCurrenciesPrivateExchanges(false), - CurrenciesPrivateExchanges("", "", ExchangeNames())); - EXPECT_EQ(StringOptionParser("eur,kraken_user1").getCurrenciesPrivateExchanges(false), - CurrenciesPrivateExchanges("EUR", CurrencyCode(), ExchangeNames({ExchangeName("kraken_user1")}))); - EXPECT_EQ(StringOptionParser("eur,binance,huobi").getCurrenciesPrivateExchanges(false), - CurrenciesPrivateExchanges("EUR", CurrencyCode(), - ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); - EXPECT_EQ( - StringOptionParser("kucoin-toto,binance,huobi").getCurrenciesPrivateExchanges(false), - CurrenciesPrivateExchanges("KUCOIN", "TOTO", ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); - EXPECT_EQ(StringOptionParser("kucoin,kraken,huobi").getCurrenciesPrivateExchanges(false), - CurrenciesPrivateExchanges( - CurrencyCode(), CurrencyCode(), - ExchangeNames({ExchangeName("kucoin"), ExchangeName("kraken"), ExchangeName("huobi")}))); +TEST(StringOptionParserTest, AmountCurrencyNoExchangesFlow) { + StringOptionParser parser("0.56%BTC-krw"); + + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount("0.56BTC"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kMandatory), CurrencyCode("KRW")); + + EXPECT_EQ(parser.parseExchanges('-'), ExchangeNames()); + + EXPECT_NO_THROW(parser.checkEndParsing()); } -TEST(StringOptionParserTest, GetCurrenciesPrivateExchangesWithCurrencies) { - using CurrenciesPrivateExchanges = StringOptionParser::CurrenciesPrivateExchanges; - EXPECT_EQ(StringOptionParser("avax-btc").getCurrenciesPrivateExchanges(), - CurrenciesPrivateExchanges("AVAX", "BTC", ExchangeNames())); - EXPECT_EQ(StringOptionParser("btc-eur,kraken_user1").getCurrenciesPrivateExchanges(), - CurrenciesPrivateExchanges("BTC", "EUR", ExchangeNames({ExchangeName("kraken_user1")}))); - EXPECT_EQ(StringOptionParser("xlm-eur,binance,huobi").getCurrenciesPrivateExchanges(), - CurrenciesPrivateExchanges("XLM", "EUR", ExchangeNames({ExchangeName("binance"), ExchangeName("huobi")}))); +TEST(StringOptionParserTest, AmountCurrencyWithExchangesFlow) { + StringOptionParser parser("15.9DOGE-USDT,binance_long_user2,kucoin"); + + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount("15.9DOGE"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount(), StringOptionParser::AmountType::kNotPresent)); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kMandatory), CurrencyCode("USDT")); + + EXPECT_EQ(parser.parseExchanges(','), ExchangeNames({ExchangeName("binance", "long_user2"), ExchangeName("kucoin")})); + + EXPECT_NO_THROW(parser.checkEndParsing()); } -TEST(StringOptionParserTest, CSVValues) { - EXPECT_EQ(StringOptionParser("").getCSVValues(), vector()); - EXPECT_EQ(StringOptionParser("val1,").getCSVValues(), vector{{"val1"}}); - EXPECT_EQ(StringOptionParser("val1,value").getCSVValues(), vector({{"val1"}, {"value"}})); +TEST(StringOptionParserTest, SeveralAmountCurrencyExchangesFlow) { + StringOptionParser parser("98.05%JST--67.4BTC-hydrA,binance-kraken"); + + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kMandatory), + std::make_pair(MonetaryAmount("98.05JST"), StringOptionParser::AmountType::kPercentage)); + EXPECT_EQ(parser.parseNonZeroAmount(StringOptionParser::FieldIs::kOptional), + std::make_pair(MonetaryAmount("-67.4BTC"), StringOptionParser::AmountType::kAbsolute)); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode("HYDRA")); + EXPECT_EQ(parser.parseCurrency(StringOptionParser::FieldIs::kOptional), CurrencyCode()); + + EXPECT_EQ(parser.parseExchanges('-'), ExchangeNames({ExchangeName("binance"), ExchangeName("kraken")})); + + EXPECT_NO_THROW(parser.checkEndParsing()); } } // namespace cct \ No newline at end of file diff --git a/src/engine/test/transferablecommandresult_test.cpp b/src/engine/test/transferablecommandresult_test.cpp new file mode 100644 index 00000000..5792b7db --- /dev/null +++ b/src/engine/test/transferablecommandresult_test.cpp @@ -0,0 +1,146 @@ +#include "transferablecommandresult.hpp" + +#include + +#include + +#include "cct_exception.hpp" +#include "coincentercommand.hpp" +#include "exchangename.hpp" +#include "monetaryamount.hpp" + +namespace cct { + +class TransferableCommandResultTest : public ::testing::Test { + protected: + static CoincenterCommand createCommand(CoincenterCommandType type, MonetaryAmount amt = MonetaryAmount(), + bool isPercentage = false, ExchangeNames exchangeNames = ExchangeNames()) { + CoincenterCommand cmd{type}; + cmd.setAmount(amt); + cmd.setPercentageAmount(isPercentage); + cmd.setExchangeNames(std::move(exchangeNames)); + return cmd; + } + + ExchangeName exchangeName11{"binance", "user1"}; + ExchangeName exchangeName12{"binance", "user2"}; + + ExchangeName exchangeName21{"kraken", "user1"}; + ExchangeName exchangeName22{"kraken", "user2"}; + + MonetaryAmount amount11{50, "DOGE"}; + MonetaryAmount amount12{10, "DOGE"}; + MonetaryAmount amount13{5, "DOGE"}; + + MonetaryAmount amount21{"0.56BTC"}; + MonetaryAmount amount22{"0.14BTC"}; +}; + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesUniqueAmount) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade), previousResults), + std::make_pair(MonetaryAmount{50, "DOGE"}, ExchangeNames({exchangeName11}))); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesDoubleAmountsSameExchange) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName11, amount12}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade), previousResults), + std::make_pair(MonetaryAmount{60, "DOGE"}, ExchangeNames({exchangeName11}))); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesDoubleAmountsDifferentExchanges) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName22, amount12}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade), previousResults), + std::make_pair(MonetaryAmount{60, "DOGE"}, ExchangeNames({exchangeName11, exchangeName22}))); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesTripleAmounts) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName11, amount12}, + TransferableCommandResult{exchangeName21, amount13}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade), previousResults), + std::make_pair(MonetaryAmount{65, "DOGE"}, ExchangeNames({exchangeName11, exchangeName21}))); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesDoubleAmountsInvalid) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName22, amount21}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade), previousResults), + std::make_pair(MonetaryAmount(), ExchangeNames())); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesWithFullInformation) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName22, amount21}}; + EXPECT_EQ(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade, MonetaryAmount(100, "DOGE")), + previousResults), + std::make_pair(MonetaryAmount(100, "DOGE"), ExchangeNames())); +} + +TEST_F(TransferableCommandResultTest, ComputeTradeAmountAndExchangesUnexpectedSituation) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName22, amount21}}; + + EXPECT_THROW(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade, MonetaryAmount(), true), + previousResults), + exception); + EXPECT_THROW(ComputeTradeAmountAndExchanges(createCommand(CoincenterCommandType::kTrade, MonetaryAmount(), false, + ExchangeNames({exchangeName11})), + previousResults), + exception); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountInvalidSingleExchangeAmount) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + + EXPECT_THROW(ComputeWithdrawAmount( + createCommand(CoincenterCommandType::kTrade, amount12, false, ExchangeNames({exchangeName11})), + previousResults), + exception); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountValidSingleExchange) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + + EXPECT_EQ(ComputeWithdrawAmount( + createCommand(CoincenterCommandType::kTrade, MonetaryAmount(), false, ExchangeNames({exchangeName12})), + previousResults), + std::make_pair(amount11, exchangeName11)); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountInvalidSingleExchangeTooManyTransferableResults) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}, + TransferableCommandResult{exchangeName21, amount12}}; + + EXPECT_EQ(ComputeWithdrawAmount( + createCommand(CoincenterCommandType::kTrade, MonetaryAmount(), false, ExchangeNames({exchangeName12})), + previousResults), + std::make_pair(MonetaryAmount(), ExchangeName())); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountInvalidTooManyExchanges) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + + EXPECT_THROW(ComputeWithdrawAmount(createCommand(CoincenterCommandType::kTrade, amount12, false, + ExchangeNames({exchangeName11, exchangeName12, exchangeName22})), + previousResults), + exception); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountInvalidNoExchange) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + + EXPECT_THROW(ComputeWithdrawAmount(createCommand(CoincenterCommandType::kTrade), previousResults), exception); +} + +TEST_F(TransferableCommandResultTest, ComputeWithdrawAmountValidDoubleExchange) { + const TransferableCommandResult previousResults[] = {TransferableCommandResult{exchangeName11, amount11}}; + + EXPECT_EQ(ComputeWithdrawAmount(createCommand(CoincenterCommandType::kTrade, amount22, false, + ExchangeNames({exchangeName12, exchangeName21})), + previousResults), + std::make_pair(amount22, exchangeName12)); +} +} // namespace cct \ No newline at end of file diff --git a/src/main/test/processcommandsfromcli_test.cpp b/src/main/test/processcommandsfromcli_test.cpp index aac21351..f6cc365f 100644 --- a/src/main/test/processcommandsfromcli_test.cpp +++ b/src/main/test/processcommandsfromcli_test.cpp @@ -2,6 +2,7 @@ #include +#include #include #include "cct_invalid_argument_exception.hpp" @@ -17,7 +18,7 @@ constexpr std::string_view kProgramName = "coincenter"; TEST(ProcessCommandsFromCLI, TestNoArguments) { CoincenterCmdLineOptions cmdLineOptions; - CoincenterCommands coincenterCommands{cmdLineOptions}; + CoincenterCommands coincenterCommands{std::span(&cmdLineOptions, 1U)}; EXPECT_NO_THROW(ProcessCommandsFromCLI(kProgramName, coincenterCommands, cmdLineOptions, kRunMode)); } @@ -25,7 +26,7 @@ TEST(ProcessCommandsFromCLI, TestNoArguments) { TEST(ProcessCommandsFromCLI, TestIncorrectArgument) { CoincenterCmdLineOptions cmdLineOptions; cmdLineOptions.apiOutputType = "invalid"; - CoincenterCommands coincenterCommands{cmdLineOptions}; + CoincenterCommands coincenterCommands{std::span(&cmdLineOptions, 1U)}; EXPECT_THROW(ProcessCommandsFromCLI(kProgramName, coincenterCommands, cmdLineOptions, kRunMode), invalid_argument); } diff --git a/src/objects/include/coincentercommandtype.hpp b/src/objects/include/coincentercommandtype.hpp index 78d347fe..3a1bc996 100644 --- a/src/objects/include/coincentercommandtype.hpp +++ b/src/objects/include/coincentercommandtype.hpp @@ -31,4 +31,6 @@ enum class CoincenterCommandType : int8_t { std::string_view CoincenterCommandTypeToString(CoincenterCommandType type); CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str); + +bool IsAnyTrade(CoincenterCommandType type); } // namespace cct \ No newline at end of file diff --git a/src/objects/include/currencycode.hpp b/src/objects/include/currencycode.hpp index 19c2e9a2..761f76df 100644 --- a/src/objects/include/currencycode.hpp +++ b/src/objects/include/currencycode.hpp @@ -123,6 +123,15 @@ class CurrencyCode { static constexpr auto kMaxLen = CurrencyCodeBase::kMaxLen; + /// Returns true if and only if a CurrencyCode can be constructed from 'curStr'. + /// Note that an empty string is a valid representation of a CurrencyCode. + static constexpr bool IsValid(std::string_view curStr) noexcept { + return curStr.size() <= kMaxLen && std::ranges::all_of(curStr, [](char ch) { + return ch > CurrencyCodeBase::kFirstAuthorizedLetter && + (ch <= CurrencyCodeBase::kLastAuthorizedLetter || (ch >= 'a' && ch <= 'z')); + }); + } + /// Constructs a neutral currency code. constexpr CurrencyCode() noexcept : _data() {} diff --git a/src/objects/include/exchangename.hpp b/src/objects/include/exchangename.hpp index 7b997173..40876539 100644 --- a/src/objects/include/exchangename.hpp +++ b/src/objects/include/exchangename.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -16,6 +17,8 @@ namespace cct { class ExchangeName { public: + static bool IsValid(std::string_view str); + ExchangeName() noexcept = default; /// Constructs a ExchangeName with a unique identifier name. @@ -30,7 +33,7 @@ class ExchangeName { std::string_view name() const { const std::size_t underscore = underscorePos(); - return std::string_view(_nameWithKey.data(), underscore == string::npos ? _nameWithKey.size() : underscore); + return {_nameWithKey.data(), underscore == string::npos ? _nameWithKey.size() : underscore}; } std::string_view keyName() const { @@ -44,6 +47,7 @@ class ExchangeName { std::string_view str() const { return _nameWithKey; } bool operator==(const ExchangeName &) const noexcept = default; + std::strong_ordering operator<=>(const ExchangeName &) const noexcept = default; friend std::ostream &operator<<(std::ostream &os, const ExchangeName &rhs) { return os << rhs.str(); } diff --git a/src/objects/include/monetaryamount.hpp b/src/objects/include/monetaryamount.hpp index 3566acbc..7a21f8f0 100644 --- a/src/objects/include/monetaryamount.hpp +++ b/src/objects/include/monetaryamount.hpp @@ -43,6 +43,7 @@ class MonetaryAmount { using AmountType = int64_t; enum class RoundType : int8_t { kDown, kUp, kNearest }; + enum class IfNoAmount : int8_t { kThrow, kNoThrow }; /// Constructs a MonetaryAmount with a value of 0 of neutral currency. constexpr MonetaryAmount() noexcept : _amount(0) {} @@ -74,7 +75,7 @@ class MonetaryAmount { /// Constructs a new MonetaryAmount from a string, containing an optional CurrencyCode. /// - If a currency is not present, assume default CurrencyCode /// - If the currency is too long to fit in a CurrencyCode, exception will be raised - /// - If only a currency is given, invalid_argument exception will be raised + /// - If only a currency is given, invalid_argument exception will be raised when ifNoAmount == IfNoAmount::kThrow /// - If given string is empty, it is equivalent to a default constructor /// /// A space can be present or not between the amount and the currency code. @@ -86,7 +87,7 @@ class MonetaryAmount { /// "-345.8909" -> -345.8909 units of no currency /// "36.61INCH" -> 36.63 units of currency INCH /// "36.6 1INCH" -> 36.6 units of currency 1INCH - explicit MonetaryAmount(std::string_view amountCurrencyStr); + explicit MonetaryAmount(std::string_view amountCurrencyStr, IfNoAmount ifNoAmount = IfNoAmount::kThrow); /// Constructs a new MonetaryAmount from a string representing the amount only and a currency code. /// Precision is calculated automatically. @@ -163,7 +164,7 @@ class MonetaryAmount { [[nodiscard]] std::strong_ordering operator<=>(const MonetaryAmount &other) const; - [[nodiscard]] constexpr bool operator==(const MonetaryAmount &other) const = default; + [[nodiscard]] constexpr bool operator==(const MonetaryAmount &) const = default; [[nodiscard]] constexpr bool operator==(AmountType amount) const { return _amount == amount && nbDecimals() == 0; } friend constexpr bool operator==(AmountType amount, MonetaryAmount rhs) { return rhs == amount; } diff --git a/src/objects/src/coincentercommandtype.cpp b/src/objects/src/coincentercommandtype.cpp index ac614844..f12c24d4 100644 --- a/src/objects/src/coincentercommandtype.cpp +++ b/src/objects/src/coincentercommandtype.cpp @@ -117,4 +117,17 @@ CoincenterCommandType CoincenterCommandTypeFromString(std::string_view str) { } throw exception("Unknown command type {}", str); } + +bool IsAnyTrade(CoincenterCommandType type) { + switch (type) { + case CoincenterCommandType::kTrade: + [[fallthrough]]; + case CoincenterCommandType::kBuy: + [[fallthrough]]; + case CoincenterCommandType::kSell: + return true; + default: + return false; + } +} } // namespace cct \ No newline at end of file diff --git a/src/objects/src/exchangename.cpp b/src/objects/src/exchangename.cpp index b9d95b1a..293e9b01 100644 --- a/src/objects/src/exchangename.cpp +++ b/src/objects/src/exchangename.cpp @@ -1,19 +1,28 @@ #include "exchangename.hpp" -#include +#include #include +#include +#include "cct_const.hpp" #include "cct_invalid_argument_exception.hpp" #include "cct_string.hpp" #include "toupperlower.hpp" namespace cct { + +bool ExchangeName::IsValid(std::string_view str) { + return std::ranges::any_of(kSupportedExchanges, [lowerStr = ToLower(str)](std::string_view ex) { + return lowerStr.starts_with(ex) && (lowerStr.size() == ex.size() || lowerStr[ex.size()] == '_'); + }); +} + ExchangeName::ExchangeName(std::string_view globalExchangeName) : _nameWithKey(globalExchangeName) { - if (globalExchangeName.empty()) { - throw invalid_argument("Exchange name cannot be empty"); + if (!IsValid(globalExchangeName)) { + throw invalid_argument("Invalid exchange name '{}'", globalExchangeName); } - for (std::size_t charPos = 0, sz = globalExchangeName.size(); charPos < sz && _nameWithKey[charPos] != '_'; - ++charPos) { + const auto sz = globalExchangeName.size(); + for (std::remove_const_t charPos = 0; charPos < sz && _nameWithKey[charPos] != '_'; ++charPos) { _nameWithKey[charPos] = tolower(_nameWithKey[charPos]); } } @@ -21,7 +30,7 @@ ExchangeName::ExchangeName(std::string_view globalExchangeName) : _nameWithKey(g ExchangeName::ExchangeName(std::string_view exchangeName, std::string_view keyName) : _nameWithKey(ToLower(exchangeName)) { if (_nameWithKey.find('_') != string::npos) { - throw invalid_argument("Invalid exchange name {}", _nameWithKey); + throw invalid_argument("Invalid exchange name '{}'", _nameWithKey); } if (!keyName.empty()) { _nameWithKey.push_back('_'); diff --git a/src/objects/src/monetaryamount.cpp b/src/objects/src/monetaryamount.cpp index a4a738d5..a98076c4 100644 --- a/src/objects/src/monetaryamount.cpp +++ b/src/objects/src/monetaryamount.cpp @@ -141,7 +141,7 @@ inline auto AmountIntegralFromStr(std::string_view amountStr, bool heuristicRoun } // namespace -MonetaryAmount::MonetaryAmount(std::string_view amountCurrencyStr) { +MonetaryAmount::MonetaryAmount(std::string_view amountCurrencyStr, IfNoAmount ifNoAmount) { const int negMult = ParseNegativeChar(amountCurrencyStr); auto last = amountCurrencyStr.begin(); @@ -157,7 +157,7 @@ MonetaryAmount::MonetaryAmount(std::string_view amountCurrencyStr) { std::string_view currencyStr(last, endIt); RemoveTrailingSpaces(currencyStr); RemovePrefixSpaces(currencyStr); - if (!currencyStr.empty() && amountStr.empty()) { + if (ifNoAmount == IfNoAmount::kThrow && !currencyStr.empty() && amountStr.empty()) { throw invalid_argument("Cannot construct MonetaryAmount with a currency without any amount"); } _curWithDecimals = CurrencyCode(currencyStr); diff --git a/src/objects/test/currencycode_test.cpp b/src/objects/test/currencycode_test.cpp index 0869d1b7..d95003e8 100644 --- a/src/objects/test/currencycode_test.cpp +++ b/src/objects/test/currencycode_test.cpp @@ -39,6 +39,16 @@ TEST(CurrencyCodeTest, String) { EXPECT_EQ("MAGIC4LIFE", CurrencyCode("Magic4Life").str()); } +TEST(CurrencyCodeTest, IsValid) { + EXPECT_TRUE(CurrencyCode::IsValid("")); + EXPECT_TRUE(CurrencyCode::IsValid("BTC")); + EXPECT_TRUE(CurrencyCode::IsValid("TESTCUR")); + EXPECT_TRUE(CurrencyCode::IsValid("lowCase")); + + EXPECT_FALSE(CurrencyCode::IsValid("averylongcurrency")); + EXPECT_FALSE(CurrencyCode::IsValid("inv ")); +} + TEST(CurrencyCodeTest, AppendString) { { string str(""); @@ -170,6 +180,9 @@ TEST(CurrencyCodeTest, Constexpr) { static_assert(!HasZ(CurrencyCode("LONGCUR"))); static_assert(HasZ(CurrencyCode("GTZFD"))); + + static_assert(CurrencyCode::IsValid("btC")); + static_assert(!CurrencyCode::IsValid("muchtoolongcur")); } TEST(CurrencyCodeTest, Iterator) { diff --git a/src/objects/test/monetaryamount_test.cpp b/src/objects/test/monetaryamount_test.cpp index bdb11ce8..2648e5f9 100644 --- a/src/objects/test/monetaryamount_test.cpp +++ b/src/objects/test/monetaryamount_test.cpp @@ -243,6 +243,7 @@ TEST(MonetaryAmountTest, StringConstructor) { EXPECT_EQ(MonetaryAmount("746REPV2"), MonetaryAmount("746", "REPV2")); EXPECT_THROW(MonetaryAmount("usdt"), invalid_argument); + EXPECT_NO_THROW(MonetaryAmount("usdt", MonetaryAmount::IfNoAmount::kNoThrow)); } TEST(MonetaryAmountTest, StringConstructorAmbiguity) {