├── .gitignore ├── BTC_USD_2017.png ├── BUILD ├── LICENSE.md ├── README.md ├── WORKSPACE ├── base ├── BUILD ├── account.cc ├── account.h ├── account_test.cc ├── base.h ├── base.proto ├── base_test.cc ├── history.cc ├── history.h ├── history_test.cc ├── side_input.cc ├── side_input.h ├── side_input_test.cc └── trader.h ├── convert.cc ├── eval ├── BUILD ├── eval.cc ├── eval.h ├── eval.proto └── eval_test.cc ├── fear_and_greed_index.ipynb ├── gmock.BUILD ├── indicators ├── BUILD ├── exponential_moving_average.cc ├── exponential_moving_average.h ├── exponential_moving_average_test.cc ├── last_n_ohlc_ticks.cc ├── last_n_ohlc_ticks.h ├── last_n_ohlc_ticks_test.cc ├── moving_average_convergence_divergence.cc ├── moving_average_convergence_divergence.h ├── moving_average_convergence_divergence_test.cc ├── relative_strength_index.cc ├── relative_strength_index.h ├── relative_strength_index_test.cc ├── simple_moving_average.cc ├── simple_moving_average.h ├── simple_moving_average_test.cc ├── stochastic_oscillator.cc ├── stochastic_oscillator.h ├── stochastic_oscillator_test.cc ├── test_util.cc ├── test_util.h ├── util.cc ├── util.h ├── util_test.cc ├── volatility.cc ├── volatility.h └── volatility_test.cc ├── logging ├── BUILD ├── csv_logger.cc ├── csv_logger.h ├── csv_logger_test.cc └── logger.h ├── market_data.ipynb ├── trader.cc ├── traders ├── BUILD ├── rebalancing_trader.cc ├── rebalancing_trader.h ├── rebalancing_trader.ipynb ├── stop_trader.cc ├── stop_trader.h ├── stop_trader.ipynb ├── trader_config.proto ├── trader_factory.cc └── trader_factory.h └── util ├── BUILD ├── example.proto ├── proto.h ├── proto_test.cc ├── time.cc ├── time.h └── time_test.cc /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | bazel-* 3 | *-checkpoint.ipynb 4 | *.json 5 | *.pb.h 6 | *.pb.cc 7 | -------------------------------------------------------------------------------- /BTC_USD_2017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petercerno/trader-backtest/4a757d8312b2812760eb15553f2ea24e22f55c50/BTC_USD_2017.png -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | cc_binary( 2 | name = "trader", 3 | srcs = ["trader.cc"], 4 | deps = [ 5 | "//base", 6 | "//base:side_input", 7 | "//eval", 8 | "//logging:csv_logger", 9 | "//traders:trader_factory", 10 | "//util:proto", 11 | "//util:time", 12 | "@com_google_absl//absl/flags:flag", 13 | "@com_google_absl//absl/flags:parse", 14 | "@com_google_absl//absl/memory", 15 | "@com_google_absl//absl/status", 16 | "@com_google_absl//absl/status:statusor", 17 | "@com_google_absl//absl/strings", 18 | "@com_google_absl//absl/strings:str_format", 19 | "@com_google_absl//absl/time", 20 | ], 21 | ) 22 | 23 | cc_binary( 24 | name = "convert", 25 | srcs = ["convert.cc"], 26 | deps = [ 27 | "//base", 28 | "//base:history", 29 | "//util:proto", 30 | "//util:time", 31 | "@com_google_absl//absl/flags:flag", 32 | "@com_google_absl//absl/flags:parse", 33 | "@com_google_absl//absl/status", 34 | "@com_google_absl//absl/status:statusor", 35 | "@com_google_absl//absl/strings", 36 | "@com_google_absl//absl/strings:str_format", 37 | "@com_google_absl//absl/time", 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2021 Peter Cerno 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | http_archive( 4 | name = "com_google_googletest", 5 | sha256 = "353571c2440176ded91c2de6d6cd88ddd41401d14692ec1f99e35d013feda55a", 6 | strip_prefix = "googletest-release-1.11.0", 7 | urls = ["https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip"], 8 | ) 9 | 10 | http_archive( 11 | name = "com_github_google_benchmark", 12 | sha256 = "03ddc88d9f381b056bc742592fb46cea11600329ecc7c9604618cdca438adee6", 13 | strip_prefix = "benchmark-1.5.6", 14 | urls = ["https://github.com/google/benchmark/archive/refs/tags/v1.5.6.zip"], 15 | ) 16 | 17 | http_archive( 18 | name = "com_google_absl", 19 | sha256 = "a4567ff02faca671b95e31d315bab18b42b6c6f1a60e91c6ea84e5a2142112c2", 20 | strip_prefix = "abseil-cpp-20211102.0", 21 | urls = ["https://github.com/abseil/abseil-cpp/archive/refs/tags/20211102.0.zip"], 22 | ) 23 | 24 | http_archive( 25 | name = "com_google_protobuf", 26 | sha256 = "528927e398f4e290001886894dac17c5c6a2e5548f3fb68004cfb01af901b53a", 27 | strip_prefix = "protobuf-3.17.3", 28 | urls = ["https://github.com/protocolbuffers/protobuf/archive/v3.17.3.zip"], 29 | ) 30 | 31 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") 32 | 33 | protobuf_deps() 34 | -------------------------------------------------------------------------------- /base/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = [ 2 | "//:__pkg__", 3 | "//eval:__pkg__", 4 | "//indicators:__pkg__", 5 | "//logging:__pkg__", 6 | "//traders:__pkg__", 7 | ]) 8 | 9 | load("@rules_proto//proto:defs.bzl", "proto_library") 10 | 11 | proto_library( 12 | name = "base_proto", 13 | srcs = ["base.proto"], 14 | ) 15 | 16 | cc_proto_library( 17 | name = "base_cc_proto", 18 | deps = [":base_proto"], 19 | ) 20 | 21 | cc_library( 22 | name = "base", 23 | hdrs = ["base.h"], 24 | deps = [":base_cc_proto"], 25 | ) 26 | 27 | cc_test( 28 | name = "base_test", 29 | srcs = ["base_test.cc"], 30 | deps = [ 31 | ":base", 32 | "@com_google_googletest//:gtest_main", 33 | ], 34 | ) 35 | 36 | cc_library( 37 | name = "account", 38 | srcs = ["account.cc"], 39 | hdrs = ["account.h"], 40 | deps = [":base"], 41 | ) 42 | 43 | cc_test( 44 | name = "account_test", 45 | srcs = ["account_test.cc"], 46 | deps = [ 47 | ":account", 48 | "@com_google_googletest//:gtest_main", 49 | ], 50 | ) 51 | 52 | cc_library( 53 | name = "trader", 54 | hdrs = ["trader.h"], 55 | deps = [":base"], 56 | ) 57 | 58 | cc_library( 59 | name = "history", 60 | srcs = ["history.cc"], 61 | hdrs = ["history.h"], 62 | deps = [":base"], 63 | ) 64 | 65 | cc_test( 66 | name = "history_test", 67 | srcs = ["history_test.cc"], 68 | deps = [ 69 | ":history", 70 | "@com_google_googletest//:gtest_main", 71 | ], 72 | ) 73 | 74 | cc_library( 75 | name = "side_input", 76 | srcs = ["side_input.cc"], 77 | hdrs = ["side_input.h"], 78 | deps = [":base"], 79 | ) 80 | 81 | cc_test( 82 | name = "side_input_test", 83 | srcs = ["side_input_test.cc"], 84 | deps = [ 85 | ":side_input", 86 | "@com_google_googletest//:gtest_main", 87 | ], 88 | ) 89 | -------------------------------------------------------------------------------- /base/account.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef BASE_ACCOUNT_H 4 | #define BASE_ACCOUNT_H 5 | 6 | #include "base/base.h" 7 | 8 | namespace trader { 9 | 10 | // Keeps track of balances and implements methods for all exchange orders. 11 | struct Account { 12 | // Base (crypto) currency balance (e.g. BTC balance when trading BTC/YYY). 13 | float base_balance = 0; 14 | // Quote currency balance (e.g. USD balance when trading XXX/USD). 15 | float quote_balance = 0; 16 | // Total accumulated transaction fee (in quote currency) over all executed 17 | // exchange orders. Transaction fee is based on the provided fee_config and 18 | // the total quote amount exchanged in the transaction, and is subtracted 19 | // from the quote currency balance (e.g. USD balance when trading XXX/USD). 20 | float total_fee = 0; 21 | 22 | // Smallest indivisible unit for base (crypto) currency balance. 23 | // Not used if zero. 24 | float base_unit = 0; 25 | // Smallest indivisible unit for quote currency balance. 26 | // Not used if zero. 27 | float quote_unit = 0; 28 | 29 | // Liquidity for executing market (stop) orders w.r.t. the given OHLC tick 30 | // from the interval [0; 1]. 31 | // If 1.0 then the market (stop) order will be executed at the opening price 32 | // (stop order price). This is the best price for the given order. 33 | // If 0.0 then the buy (sell) order will be executed at the highest (lowest) 34 | // price of the given OHLC tick. This is the worst price for the given order. 35 | // Anything in between 0.0 and 1.0 will be linearly interpolated. 36 | float market_liquidity = 1.0f; 37 | // Fraction of the OHLC tick volume that will be used to fill the limit order. 38 | // If the actual traded volume * max_volume_ratio is less than the limit 39 | // order amount, then the limit order will be filled only partially. 40 | // Not used if zero. 41 | float max_volume_ratio = 0.0f; 42 | 43 | // Initializes the account based on the account_config. 44 | void InitAccount(const AccountConfig& account_config); 45 | 46 | // Returns the fee (in quote currency) based on the provided fee_config and 47 | // the given quote currency amount involved in the transaction. 48 | float GetFee(const FeeConfig& fee_config, float quote_amount) const; 49 | 50 | // Returns the price of the market buy order based on the market_liquidity 51 | // (defined above) when executed over the given OHLC tick. 52 | float GetMarketBuyPrice(const OhlcTick& ohlc_tick) const; 53 | // Returns the price of the market sell order based on the market_liquidity 54 | // (defined above) when executed over the given OHLC tick. 55 | float GetMarketSellPrice(const OhlcTick& ohlc_tick) const; 56 | // Returns the price of the stop buy order based on the market_liquidity 57 | // (defined above) and the stop order price when executed over the OHLC tick. 58 | float GetStopBuyPrice(const OhlcTick& ohlc_tick, float price) const; 59 | // Returns the price of the stop sell order based on the market_liquidity 60 | // (defined above) and the stop order price when executed over the OHLC tick. 61 | float GetStopSellPrice(const OhlcTick& ohlc_tick, float price) const; 62 | // Returns the maximum tradeable base (crypto) currency amount based on 63 | // the max_volume_ratio (defined above) and the given OHLC tick. 64 | float GetMaxBaseAmount(const OhlcTick& ohlc_tick) const; 65 | 66 | // ORDERS AT SPECIFIC PRICE 67 | 68 | // Buys the specified amount of base (crypto) currency at the given price. 69 | // Returns true iff the order was executed successfully. 70 | bool BuyBase(const FeeConfig& fee_config, float base_amount, float price); 71 | // Buys as much base (crypto) currency as possible at the given price, 72 | // spending at most quote_amount in quote currency. 73 | // It is possible to buy at most max_base_amount base (crypto) currency. 74 | // Returns true iff the order was executed successfully. 75 | bool BuyAtQuote(const FeeConfig& fee_config, float quote_amount, float price, 76 | float max_base_amount = std::numeric_limits::max()); 77 | // Sells the specified amount of base (crypto) currency at the given price. 78 | // Returns true iff the order was executed successfully. 79 | bool SellBase(const FeeConfig& fee_config, float security_amount, 80 | float price); 81 | // Sells as much base (crypto) currency as possible at the given price, 82 | // receiving at most quote_amount in quote currency. 83 | // It is possible to sell at most max_base_amount base (crypto) currency. 84 | // Returns true iff the order was executed successfully. 85 | bool SellAtQuote(const FeeConfig& fee_config, float quote_amount, float price, 86 | float max_base_amount = std::numeric_limits::max()); 87 | 88 | // MARKET ORDERS 89 | 90 | // Executes market buy order for the specified amount of base (crypto) 91 | // currency. Returns true iff the order was executed successfully. 92 | bool MarketBuy(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 93 | float base_amount); 94 | // Executes market buy order spending at most quote_amount in quote currency. 95 | // Returns true iff the order was executed successfully. 96 | bool MarketBuyAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 97 | float quote_amount); 98 | // Executes market sell order for the specified amount of base (crypto) 99 | // currency. Returns true iff the order was executed successfully. 100 | bool MarketSell(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 101 | float base_amount); 102 | // Executes market sell order receiving at most quote_amount in quote 103 | // currency. Returns true iff the order was executed successfully. 104 | bool MarketSellAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 105 | float quote_amount); 106 | 107 | // STOP ORDERS 108 | 109 | // Executes stop buy order for the specified amount of base (crypto) currency 110 | // and at the specified stop order price. 111 | // Stop buy order can be executed only if the price jumps above the stop 112 | // order price, i.e. when the given OHLC tick's high price is greater than or 113 | // equal to the stop order price. 114 | // Returns true iff the order was executed successfully. 115 | bool StopBuy(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 116 | float base_amount, float stop_price); 117 | // Executes stop buy order for the specified amount of quote currency and at 118 | // the specified stop order price. 119 | // Stop buy order can be executed only if the price jumps above the stop 120 | // order price, i.e. when the given OHLC tick's high price is greater than or 121 | // equal to the stop order price. 122 | // Returns true iff the order was executed successfully. 123 | bool StopBuyAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 124 | float quote_amount, float stop_price); 125 | // Executes stop sell order for the specified amount of base (crypto) currency 126 | // and at the specified stop order price. 127 | // Stop sell order can be executed only if the price drops below the stop 128 | // order price, i.e. when the given OHLC tick's low price is lower than or 129 | // equal to the stop order price. 130 | // Returns true iff the order was executed successfully. 131 | bool StopSell(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 132 | float base_amount, float stop_price); 133 | // Executes stop sell order for the specified amount of quote currency 134 | // and at the specified stop order price. 135 | // Stop sell order can be executed only if the price drops below the stop 136 | // order price, i.e. when the given OHLC tick's low price is lower than or 137 | // equal to the stop order price. 138 | // Returns true iff the order was executed successfully. 139 | bool StopSellAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 140 | float quote_amount, float stop_price); 141 | 142 | // LIMIT ORDERS 143 | 144 | // Executes limit buy order for the specified amount of base (crypto) currency 145 | // and at the specified limit order price. 146 | // Limit buy order can be executed only if the price drops below the limit 147 | // order price, i.e. when the given OHLC tick's low price is lower than or 148 | // equal to the limit order price. 149 | // Returns true iff the order was executed successfully. 150 | bool LimitBuy(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 151 | float base_amount, float limit_price); 152 | // Executes limit buy order for the specified amount of quote currency and at 153 | // the specified limit order price. 154 | // Limit buy order can be executed only if the price drops below the limit 155 | // order price, i.e. when the given OHLC tick's low price is lower than or 156 | // equal to the limit order price. 157 | // Returns true iff the order was executed successfully. 158 | bool LimitBuyAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 159 | float quote_amount, float limit_price); 160 | // Executes limit sell order for the specified amount of base (crypto) 161 | // currency and at the specified limit order price. 162 | // Limit sell order can be executed only if the price jumps above the limit 163 | // order price, i.e. when the given OHLC tick's high price is greater than or 164 | // equal to the limit order price. 165 | // Returns true iff the order was executed successfully. 166 | bool LimitSell(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 167 | float base_amount, float limit_price); 168 | // Executes limit sell order for the specified amount of quote currency and 169 | // at the specified limit order price. 170 | // Limit sell order can be executed only if the price jumps above the limit 171 | // order price, i.e. when the given OHLC tick's high price is greater than or 172 | // equal to the limit order price. 173 | // Returns true iff the order was executed successfully. 174 | bool LimitSellAtQuote(const FeeConfig& fee_config, const OhlcTick& ohlc_tick, 175 | float quote_amount, float limit_price); 176 | 177 | // GENERAL ORDER EXECUTION 178 | 179 | // Executes the order over the given ohlc_tick. 180 | // Returns true iff the order was executed successfully. 181 | bool ExecuteOrder(const AccountConfig& account_config, const Order& order, 182 | const OhlcTick& ohlc_tick); 183 | }; 184 | 185 | } // namespace trader 186 | 187 | #endif // BASE_ACCOUNT_H 188 | -------------------------------------------------------------------------------- /base/base.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef BASE_BASE_H 4 | #define BASE_BASE_H 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | 30 | #include "base/base.pb.h" 31 | 32 | namespace trader { 33 | 34 | constexpr int kSecondsPerMinute = 60; 35 | constexpr int kSecondsPerHour = 60 * 60; 36 | constexpr int kSecondsPerDay = 24 * 60 * 60; 37 | constexpr int kSecondsPerWeek = 7 * 24 * 60 * 60; 38 | 39 | // Historical prices over time. 40 | using PriceHistory = std::vector; 41 | 42 | // Historical OHLC ticks over time. 43 | using OhlcHistory = std::vector; 44 | 45 | // Historical side inputs. 46 | using SideHistory = std::vector; 47 | 48 | // Returns a pair of iterators covering the time interval [start_timestamp_sec, 49 | // end_timestamp_sec) of the given history. 50 | template 51 | std::pair::const_iterator, 52 | typename std::vector::const_iterator> 53 | HistorySubset(const std::vector& history, int64_t start_timestamp_sec, 54 | int64_t end_timestamp_sec) { 55 | const auto record_compare = [](const T& record, int64_t timestamp_sec) { 56 | return record.timestamp_sec() < timestamp_sec; 57 | }; 58 | const auto record_begin = 59 | start_timestamp_sec > 0 60 | ? std::lower_bound(history.begin(), history.end(), 61 | start_timestamp_sec, record_compare) 62 | : history.begin(); 63 | const auto record_end = 64 | end_timestamp_sec > 0 65 | ? std::lower_bound(history.begin(), history.end(), end_timestamp_sec, 66 | record_compare) 67 | : history.end(); 68 | return {record_begin, record_end}; 69 | } 70 | 71 | // Returns the subset of the given history covering the time interval 72 | // [start_timestamp_sec, end_timestamp_sec). 73 | template 74 | std::vector HistorySubsetCopy(const std::vector& history, 75 | int64_t start_timestamp_sec, 76 | int64_t end_timestamp_sec) { 77 | std::vector history_subset_copy; 78 | const auto history_subset = 79 | HistorySubset(history, start_timestamp_sec, end_timestamp_sec); 80 | const auto size = std::distance(history_subset.first, history_subset.second); 81 | if (size == 0) { 82 | return {}; 83 | } 84 | history_subset_copy.reserve(size); 85 | std::copy(history_subset.first, history_subset.second, 86 | std::back_inserter(history_subset_copy)); 87 | return history_subset_copy; 88 | } 89 | 90 | } // namespace trader 91 | 92 | #endif // BASE_BASE_H 93 | -------------------------------------------------------------------------------- /base/base.proto: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | syntax = "proto2"; 4 | 5 | package trader; 6 | 7 | // Base (crypto) currency traded at a specific price and time. 8 | message PriceRecord { 9 | // UNIX timestamp (in seconds). 10 | optional int64 timestamp_sec = 1; 11 | // Base (crypto) currency price (e.g. BTC/USD price). 12 | optional float price = 2; 13 | // Traded base (crypto) currency volume. 14 | optional float volume = 3; 15 | } 16 | 17 | // Open, high, low, and close prices for specific time interval. 18 | // The duration of the time interval is assumed implicitly. 19 | message OhlcTick { 20 | // UNIX timestamp (in seconds) of the start of the time interval. 21 | optional int64 timestamp_sec = 1; 22 | // Opening base (crypto) currency price at the start of the time interval. 23 | optional float open = 2; 24 | // Highest base (crypto) currency price during the time interval. 25 | optional float high = 3; 26 | // Lowest base (crypto) currency price during the time interval. 27 | optional float low = 4; 28 | // Closing base (crypto) currency price at the end of the time interval. 29 | optional float close = 5; 30 | // Total traded volume during the time interval. 31 | optional float volume = 6; 32 | } 33 | 34 | // Used to provide additional information (signals) to the trader. 35 | // Note that the interpretation of the signals is up to the trader itself. 36 | message SideInputRecord { 37 | // UNIX timestamp (in seconds). 38 | optional int64 timestamp_sec = 1; 39 | // List of signals. We assume that all signals are floats and that every 40 | // SideInputRecord has the same number of signals. 41 | repeated float signal = 2; 42 | } 43 | 44 | // Transaction fee configuration. 45 | message FeeConfig { 46 | // Relative transaction fee. 47 | optional float relative_fee = 1; 48 | // Fixed transaction fee in quote currency. 49 | optional float fixed_fee = 2; 50 | // Minimum transaction fee in quote currency. 51 | optional float minimum_fee = 3; 52 | } 53 | 54 | // Trader account configuration. 55 | message AccountConfig { 56 | // Starting base (crypto) currency balance (e.g. BTC balance). 57 | optional float start_base_balance = 1; 58 | // Starting quote balance (e.g. USD balance). 59 | optional float start_quote_balance = 2; 60 | // Smallest indivisible unit for base (crypto) currency balance. 61 | // Not used if zero. 62 | optional float base_unit = 3; 63 | // Smallest indivisible unit for quote balance. 64 | // Not used if zero. 65 | optional float quote_unit = 4; 66 | // Transaction fee configuration for market orders. 67 | optional FeeConfig market_order_fee_config = 5; 68 | // Transaction fee configuration for stop orders. 69 | optional FeeConfig stop_order_fee_config = 6; 70 | // Transaction fee configuration for limit orders. 71 | optional FeeConfig limit_order_fee_config = 7; 72 | // Liquidity for executing market (stop) orders w.r.t. the given OHLC tick 73 | // from the interval [0; 1]. 74 | // If 1.0 then the market (stop) order will be executed at the opening price 75 | // (stop order price). This is the best price for the given order. 76 | // If 0.0 then the buy (sell) order will be executed at the highest (lowest) 77 | // price of the given OHLC tick. This is the worst price for the given order. 78 | // Anything in between 0.0 and 1.0 will be linearly interpolated. 79 | optional float market_liquidity = 8; 80 | // Fraction of the OHLC tick volume that will be used to fill the limit order. 81 | // If the actual traded volume * max_volume_ratio is less than the limit 82 | // order size, then the limit order will be filled only partially. 83 | // Not used if zero. 84 | optional float max_volume_ratio = 9; 85 | } 86 | 87 | // Exchange order. 88 | message Order { 89 | enum Type { 90 | MARKET = 0; // Market order. 91 | STOP = 1; // Stop order. 92 | LIMIT = 2; // Limit order. 93 | } 94 | enum Side { 95 | BUY = 0; // Buy order. 96 | SELL = 1; // Sell order. 97 | } 98 | // Type of the order. Order is invalid if the order type is missing. 99 | optional Type type = 1; 100 | // Side of the order. Order is invalid if the order side is missing. 101 | optional Side side = 2; 102 | oneof oneof_amount { 103 | // The amount of base (crypto) currency to by buy / sell. 104 | float base_amount = 3; 105 | // The (maximum) amount of quote to be spent on buying (or to be received 106 | // when selling) the base (crypto) currency. 107 | // The actual traded amount might be smaller due to exchange fees. 108 | float quote_amount = 4; 109 | } 110 | // Target price at which to execute the order. Ignored for market orders. 111 | // The actual traded price might differ for stop orders. 112 | optional float price = 5; 113 | } 114 | -------------------------------------------------------------------------------- /base/base_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "base/base.h" 4 | 5 | #include "gtest/gtest.h" 6 | 7 | namespace trader { 8 | namespace { 9 | void AddPriceRecord(int64_t timestamp_sec, float price, float volume, 10 | PriceHistory& price_history) { 11 | price_history.emplace_back(); 12 | price_history.back().set_timestamp_sec(timestamp_sec); 13 | price_history.back().set_price(price); 14 | price_history.back().set_volume(volume); 15 | } 16 | 17 | void ExpectNearPriceRecord(const PriceRecord& actual, 18 | const PriceRecord& expected) { 19 | EXPECT_EQ(actual.timestamp_sec(), expected.timestamp_sec()); 20 | EXPECT_FLOAT_EQ(actual.price(), expected.price()); 21 | EXPECT_FLOAT_EQ(actual.volume(), expected.volume()); 22 | } 23 | } // namespace 24 | 25 | TEST(HistorySubsetTest, Basic) { 26 | PriceHistory price_history; 27 | AddPriceRecord(1483228800, 700.0f, 1.0e3f, price_history); 28 | AddPriceRecord(1483229400, 800.0f, 1.5e3f, price_history); 29 | AddPriceRecord(1483230000, 750.0f, 1.0e3f, price_history); 30 | AddPriceRecord(1483230600, 850.0f, 2.0e3f, price_history); 31 | AddPriceRecord(1483231200, 800.0f, 1.5e3f, price_history); 32 | 33 | std::pair 34 | history_subset = HistorySubset(price_history, 35 | /*start_timestamp_sec=*/0, 36 | /*end_timestamp_sec=*/0); 37 | EXPECT_EQ(history_subset.first, price_history.begin()); 38 | EXPECT_EQ(history_subset.second, price_history.end()); 39 | 40 | history_subset = HistorySubset(price_history, 41 | /*start_timestamp_sec=*/0, 42 | /*end_timestamp_sec=*/1483230000); 43 | EXPECT_EQ(history_subset.first, price_history.begin()); 44 | EXPECT_EQ(history_subset.second, price_history.begin() + 2); 45 | 46 | history_subset = HistorySubset(price_history, 47 | /*start_timestamp_sec=*/1483229400, 48 | /*end_timestamp_sec=*/0); 49 | EXPECT_EQ(history_subset.first, price_history.begin() + 1); 50 | EXPECT_EQ(history_subset.second, price_history.end()); 51 | 52 | history_subset = HistorySubset(price_history, 53 | /*start_timestamp_sec=*/1483228800, 54 | /*end_timestamp_sec=*/1483231800); 55 | EXPECT_EQ(history_subset.first, price_history.begin()); 56 | EXPECT_EQ(history_subset.second, price_history.end()); 57 | 58 | history_subset = HistorySubset(price_history, 59 | /*start_timestamp_sec=*/1483228800, 60 | /*end_timestamp_sec=*/1483228800); 61 | EXPECT_EQ(history_subset.first, price_history.begin()); 62 | EXPECT_EQ(history_subset.second, price_history.begin()); 63 | 64 | history_subset = HistorySubset(price_history, 65 | /*start_timestamp_sec=*/1483228800, 66 | /*end_timestamp_sec=*/1483229400); 67 | EXPECT_EQ(history_subset.first, price_history.begin()); 68 | EXPECT_EQ(history_subset.second, price_history.begin() + 1); 69 | 70 | history_subset = HistorySubset(price_history, 71 | /*start_timestamp_sec=*/1483228860, 72 | /*end_timestamp_sec=*/1483229400); 73 | EXPECT_EQ(history_subset.first, price_history.begin() + 1); 74 | EXPECT_EQ(history_subset.second, price_history.begin() + 1); 75 | 76 | history_subset = HistorySubset(price_history, 77 | /*start_timestamp_sec=*/1483229100, 78 | /*end_timestamp_sec=*/1483230900); 79 | EXPECT_EQ(history_subset.first, price_history.begin() + 1); 80 | EXPECT_EQ(history_subset.second, price_history.begin() + 4); 81 | } 82 | 83 | TEST(HistorySubsetCopyTest, Basic) { 84 | PriceHistory price_history; 85 | AddPriceRecord(1483228800, 700.0f, 1.0e3f, price_history); 86 | AddPriceRecord(1483229400, 800.0f, 1.5e3f, price_history); 87 | AddPriceRecord(1483230000, 750.0f, 2.0e3f, price_history); 88 | AddPriceRecord(1483230600, 850.0f, 2.5e3f, price_history); 89 | AddPriceRecord(1483231200, 650.0f, 3.0e3f, price_history); 90 | 91 | PriceHistory history_subset_copy = 92 | HistorySubsetCopy(price_history, 93 | /*start_timestamp_sec=*/0, 94 | /*end_timestamp_sec=*/0); 95 | ASSERT_EQ(history_subset_copy.size(), 5); 96 | for (size_t i = 0; i < 5; ++i) { 97 | ExpectNearPriceRecord(history_subset_copy[i], price_history[i]); 98 | } 99 | 100 | history_subset_copy = HistorySubsetCopy(price_history, 101 | /*start_timestamp_sec=*/0, 102 | /*end_timestamp_sec=*/1483230600); 103 | ASSERT_EQ(history_subset_copy.size(), 3); 104 | for (size_t i = 0; i < 3; ++i) { 105 | ExpectNearPriceRecord(history_subset_copy[i], price_history[i]); 106 | } 107 | 108 | history_subset_copy = HistorySubsetCopy(price_history, 109 | /*start_timestamp_sec=*/1483230000, 110 | /*end_timestamp_sec=*/0); 111 | ASSERT_EQ(history_subset_copy.size(), 3); 112 | for (size_t i = 0; i < 3; ++i) { 113 | ExpectNearPriceRecord(history_subset_copy[i], price_history[i + 2]); 114 | } 115 | 116 | history_subset_copy = HistorySubsetCopy(price_history, 117 | /*start_timestamp_sec=*/1483229100, 118 | /*end_timestamp_sec=*/1483230900); 119 | ASSERT_EQ(history_subset_copy.size(), 3); 120 | for (size_t i = 0; i < 3; ++i) { 121 | ExpectNearPriceRecord(history_subset_copy[i], price_history[i + 1]); 122 | } 123 | 124 | history_subset_copy = HistorySubsetCopy(price_history, 125 | /*start_timestamp_sec=*/1483228800, 126 | /*end_timestamp_sec=*/1483228800); 127 | ASSERT_EQ(history_subset_copy.size(), 0); 128 | 129 | history_subset_copy = HistorySubsetCopy(price_history, 130 | /*start_timestamp_sec=*/1483230300, 131 | /*end_timestamp_sec=*/1483230600); 132 | ASSERT_EQ(history_subset_copy.size(), 0); 133 | } 134 | 135 | } // namespace trader 136 | -------------------------------------------------------------------------------- /base/history.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "base/history.h" 4 | 5 | namespace trader { 6 | 7 | bool CheckPriceHistoryTimestamps(const PriceHistory& price_history) { 8 | for (size_t i = 1; i < price_history.size(); ++i) { 9 | if (price_history[i].timestamp_sec() < 10 | price_history[i - 1].timestamp_sec()) { 11 | return false; 12 | } 13 | } 14 | return true; 15 | } 16 | 17 | std::vector GetPriceHistoryGaps(PriceHistory::const_iterator begin, 18 | PriceHistory::const_iterator end, 19 | int64_t start_timestamp_sec, 20 | int64_t end_timestamp_sec, 21 | size_t top_n) { 22 | if (begin == end) { 23 | return {}; 24 | } 25 | auto gap_cmp = [](HistoryGap lhs, HistoryGap rhs) { 26 | const int64_t length_delta = 27 | lhs.second - lhs.first - rhs.second + rhs.first; 28 | return length_delta > 0 || (length_delta == 0 && lhs.first < rhs.first); 29 | }; 30 | std::priority_queue, decltype(gap_cmp)> 31 | gap_queue(gap_cmp); 32 | std::vector history_gaps; 33 | if (start_timestamp_sec > 0) { 34 | gap_queue.push(HistoryGap{start_timestamp_sec, begin->timestamp_sec()}); 35 | } 36 | auto it_prev = end; 37 | for (auto it = begin; it != end; ++it) { 38 | if (it_prev != end) { 39 | gap_queue.push(HistoryGap{it_prev->timestamp_sec(), it->timestamp_sec()}); 40 | if (gap_queue.size() > top_n) { 41 | gap_queue.pop(); 42 | } 43 | } 44 | it_prev = it; 45 | } 46 | if (end_timestamp_sec > 0) { 47 | gap_queue.push(HistoryGap{it_prev->timestamp_sec(), end_timestamp_sec}); 48 | if (gap_queue.size() > top_n) { 49 | gap_queue.pop(); 50 | } 51 | } 52 | while (!gap_queue.empty()) { 53 | history_gaps.push_back(gap_queue.top()); 54 | gap_queue.pop(); 55 | } 56 | std::sort(history_gaps.begin(), history_gaps.end()); 57 | return history_gaps; 58 | } 59 | 60 | PriceHistory RemoveOutliers(PriceHistory::const_iterator begin, 61 | PriceHistory::const_iterator end, 62 | float max_price_deviation_per_min, 63 | std::vector* outlier_indices) { 64 | static constexpr int MAX_LOOKAHEAD = 10; 65 | static constexpr int MIN_LOOKAHEAD_PERSISTENT = 3; 66 | PriceHistory price_history_clean; 67 | for (auto it = begin; it != end; ++it) { 68 | const PriceRecord& price_record = *it; 69 | const size_t price_record_index = std::distance(begin, it); 70 | if (price_record.price() <= 0 || price_record.volume() < 0) { 71 | if (outlier_indices != nullptr) { 72 | outlier_indices->push_back(price_record_index); 73 | } 74 | continue; 75 | } 76 | if (price_history_clean.empty()) { 77 | price_history_clean.push_back(price_record); 78 | continue; 79 | } 80 | const PriceRecord& price_record_prev = price_history_clean.back(); 81 | const float reference_price = price_record_prev.price(); 82 | const float duration_min = 83 | std::max(1.0f, static_cast(price_record.timestamp_sec() - 84 | price_record_prev.timestamp_sec()) / 85 | 60.0f); 86 | const float jump_factor = 87 | (1.0f + max_price_deviation_per_min) * std::sqrt(duration_min); 88 | const float jump_up_price = reference_price * jump_factor; 89 | const float jump_down_price = reference_price / jump_factor; 90 | const bool jumped_up = price_record.price() > jump_up_price; 91 | const bool jumped_down = price_record.price() < jump_down_price; 92 | bool is_outlier = false; 93 | if (jumped_up || jumped_down) { 94 | // Let's look ahead if this jump persists. 95 | int lookahead = 0; 96 | int lookahead_persistent = 0; 97 | const float middle_up_price = 98 | 0.8f * jump_up_price + 0.2f * reference_price; 99 | const float middle_down_price = 100 | 0.8f * jump_down_price + 0.2f * reference_price; 101 | for (auto jt = it + 1; jt != end && lookahead < MAX_LOOKAHEAD; ++jt) { 102 | if (jt->price() <= 0 || jt->volume() < 0) { 103 | continue; 104 | } 105 | if ((jumped_up && jt->price() > middle_up_price) || 106 | (jumped_down && jt->price() < middle_down_price)) { 107 | ++lookahead_persistent; 108 | } 109 | ++lookahead; 110 | } 111 | is_outlier = lookahead_persistent < MIN_LOOKAHEAD_PERSISTENT; 112 | } 113 | if (!is_outlier) { 114 | price_history_clean.push_back(price_record); 115 | } else if (outlier_indices != nullptr) { 116 | outlier_indices->push_back(price_record_index); 117 | } 118 | } 119 | return price_history_clean; 120 | } 121 | 122 | std::map GetOutlierIndicesWithContext( 123 | const std::vector& outlier_indices, size_t price_history_size, 124 | size_t left_context_size, size_t right_context_size, size_t last_n) { 125 | std::map index_to_outlier; 126 | const size_t start_i = (last_n == 0 || outlier_indices.size() <= last_n) 127 | ? 0 128 | : outlier_indices.size() - last_n; 129 | for (size_t i = start_i; i < outlier_indices.size(); ++i) { 130 | const size_t j = outlier_indices[i]; 131 | const size_t a = (j <= left_context_size) ? 0 : j - left_context_size; 132 | const size_t b = std::min(j + right_context_size + 1, price_history_size); 133 | for (size_t k = a; k < b; ++k) { 134 | // Keep the existing outliers. 135 | index_to_outlier[k] |= false; 136 | } 137 | index_to_outlier[j] = true; 138 | } 139 | return index_to_outlier; 140 | } 141 | 142 | OhlcHistory Resample(PriceHistory::const_iterator begin, 143 | PriceHistory::const_iterator end, int sampling_rate_sec) { 144 | OhlcHistory resampled_ohlc_history; 145 | for (auto it = begin; it != end; ++it) { 146 | const int64_t downsampled_timestamp_sec = 147 | sampling_rate_sec * (it->timestamp_sec() / sampling_rate_sec); 148 | while (!resampled_ohlc_history.empty() && 149 | resampled_ohlc_history.back().timestamp_sec() + sampling_rate_sec < 150 | downsampled_timestamp_sec) { 151 | const int64_t prev_timestamp_sec = 152 | resampled_ohlc_history.back().timestamp_sec(); 153 | const float prev_close = resampled_ohlc_history.back().close(); 154 | resampled_ohlc_history.emplace_back(); 155 | OhlcTick* ohlc_tick = &resampled_ohlc_history.back(); 156 | ohlc_tick->set_timestamp_sec(prev_timestamp_sec + sampling_rate_sec); 157 | ohlc_tick->set_open(prev_close); 158 | ohlc_tick->set_high(prev_close); 159 | ohlc_tick->set_low(prev_close); 160 | ohlc_tick->set_close(prev_close); 161 | ohlc_tick->set_volume(0); 162 | } 163 | if (resampled_ohlc_history.empty() || 164 | resampled_ohlc_history.back().timestamp_sec() < 165 | downsampled_timestamp_sec) { 166 | resampled_ohlc_history.emplace_back(); 167 | OhlcTick* ohlc_tick = &resampled_ohlc_history.back(); 168 | ohlc_tick->set_timestamp_sec(downsampled_timestamp_sec); 169 | ohlc_tick->set_open(it->price()); 170 | ohlc_tick->set_high(it->price()); 171 | ohlc_tick->set_low(it->price()); 172 | ohlc_tick->set_close(it->price()); 173 | ohlc_tick->set_volume(it->volume()); 174 | } else { 175 | assert(resampled_ohlc_history.back().timestamp_sec() == 176 | downsampled_timestamp_sec); 177 | OhlcTick* ohlc_tick = &resampled_ohlc_history.back(); 178 | ohlc_tick->set_high(std::max(ohlc_tick->high(), it->price())); 179 | ohlc_tick->set_low(std::min(ohlc_tick->low(), it->price())); 180 | ohlc_tick->set_close(it->price()); 181 | ohlc_tick->set_volume(ohlc_tick->volume() + it->volume()); 182 | } 183 | } 184 | return resampled_ohlc_history; 185 | } 186 | 187 | } // namespace trader 188 | -------------------------------------------------------------------------------- /base/history.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef BASE_HISTORY_H 4 | #define BASE_HISTORY_H 5 | 6 | #include "base/base.h" 7 | 8 | namespace trader { 9 | 10 | // Gap in the price history, represented as a pair of timestamps (in seconds). 11 | using HistoryGap = std::pair; 12 | 13 | // Returns the top_n largest (chronologically sorted) price history gaps. 14 | std::vector GetPriceHistoryGaps(PriceHistory::const_iterator begin, 15 | PriceHistory::const_iterator end, 16 | int64_t start_timestamp_sec, 17 | int64_t end_timestamp_sec, 18 | size_t top_n); 19 | 20 | // Returns price history with removed outliers. 21 | // max_price_deviation_per_min is maximum allowed price deviation per minute. 22 | // outlier_indices is an optional output vector of removed outlier indices. 23 | PriceHistory RemoveOutliers(PriceHistory::const_iterator begin, 24 | PriceHistory::const_iterator end, 25 | float max_price_deviation_per_min, 26 | std::vector* outlier_indices); 27 | 28 | // Returns a map from price_history indices to booleans indicating whether the 29 | // indices correspond to outliers or not (taking the last_n outlier_indices). 30 | // Every outlier has left_context_size indices to the left (if possible) and 31 | // right_context_size indices to the right (if possible). 32 | // price_history_size is the size of the original price history. 33 | std::map GetOutlierIndicesWithContext( 34 | const std::vector& outlier_indices, size_t price_history_size, 35 | size_t left_context_size, size_t right_context_size, size_t last_n); 36 | 37 | // Returns the resampled ohlc_history with the given sampling rate (in seconds). 38 | OhlcHistory Resample(PriceHistory::const_iterator begin, 39 | PriceHistory::const_iterator end, int sampling_rate_sec); 40 | 41 | } // namespace trader 42 | 43 | #endif // BASE_HISTORY_H 44 | -------------------------------------------------------------------------------- /base/side_input.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "base/side_input.h" 4 | 5 | namespace trader { 6 | 7 | SideInput::SideInput(const SideHistory& side_history) 8 | : num_signals_(side_history.front().signal_size()) { 9 | timestamp_sec_history_.reserve(side_history.size()); 10 | data_.reserve(side_history.size() * num_signals_); 11 | int64_t prev_timestamp_sec = 0; 12 | for (const SideInputRecord& side_input : side_history) { 13 | assert(side_input.signal_size() == num_signals_); 14 | assert(side_input.timestamp_sec() > prev_timestamp_sec); 15 | timestamp_sec_history_.push_back(side_input.timestamp_sec()); 16 | prev_timestamp_sec = side_input.timestamp_sec(); 17 | for (const float signal : side_input.signal()) { 18 | data_.push_back(signal); 19 | } 20 | } 21 | } 22 | 23 | float SideInput::GetSideInputSignal(int side_input_index, 24 | int signal_index) const { 25 | assert(side_input_index >= 0 && side_input_index < GetNumberOfRecords()); 26 | assert(signal_index >= 0 && signal_index < GetNumberOfSignals()); 27 | return data_.at(side_input_index * num_signals_ + signal_index); 28 | } 29 | 30 | void SideInput::GetSideInputSignals( 31 | int side_input_index, std::vector& side_input_signals) const { 32 | assert(side_input_index >= 0 && side_input_index < GetNumberOfRecords()); 33 | const int offset = side_input_index * num_signals_; 34 | for (int signal_index = 0; signal_index < num_signals_; ++signal_index) { 35 | side_input_signals.push_back(data_.at(offset + signal_index)); 36 | } 37 | } 38 | 39 | int SideInput::GetSideInputIndex(int64_t timestamp_sec) const { 40 | return std::upper_bound(timestamp_sec_history_.begin(), 41 | timestamp_sec_history_.end(), timestamp_sec) - 42 | timestamp_sec_history_.begin() - 1; 43 | } 44 | 45 | int SideInput::GetSideInputIndex(int64_t timestamp_sec, 46 | int prev_side_input_index) const { 47 | if (prev_side_input_index < 0) { 48 | return GetSideInputIndex(timestamp_sec); 49 | } 50 | assert(timestamp_sec >= timestamp_sec_history_.at(prev_side_input_index)); 51 | if (prev_side_input_index == timestamp_sec_history_.size() - 1 || 52 | timestamp_sec < timestamp_sec_history_.at(prev_side_input_index + 1)) { 53 | return prev_side_input_index; 54 | } 55 | if (prev_side_input_index == timestamp_sec_history_.size() - 2 || 56 | timestamp_sec < timestamp_sec_history_.at(prev_side_input_index + 2)) { 57 | return prev_side_input_index + 1; 58 | } 59 | return std::upper_bound( 60 | timestamp_sec_history_.begin() + prev_side_input_index + 2, 61 | timestamp_sec_history_.end(), timestamp_sec) - 62 | timestamp_sec_history_.begin() - 1; 63 | } 64 | 65 | } // namespace trader 66 | -------------------------------------------------------------------------------- /base/side_input.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef BASE_SIDE_INPUT_H 4 | #define BASE_SIDE_INPUT_H 5 | 6 | #include "base/base.h" 7 | 8 | namespace trader { 9 | 10 | // Side history wrapper for fast thread-safe read-only access. 11 | class SideInput { 12 | public: 13 | // Constructor. Expects non-empty side_history with increasing timestamps. 14 | explicit SideInput(const SideHistory& side_history); 15 | virtual ~SideInput() {} 16 | 17 | // Returns the number of signals per side input record. 18 | int GetNumberOfSignals() const { return num_signals_; } 19 | // Returns the number of side input records. 20 | int GetNumberOfRecords() const { return timestamp_sec_history_.size(); } 21 | // Returns the timestamp (in seconds) for the given side_input_index. 22 | int64_t GetSideInputTimestamp(int side_input_index) const { 23 | return timestamp_sec_history_.at(side_input_index); 24 | } 25 | // Returns the signal for the given side_input_index and signal_index. 26 | float GetSideInputSignal(int side_input_index, int signal_index) const; 27 | // Adds signals (at the side_input_index) to the side_input_signals vector. 28 | void GetSideInputSignals(int side_input_index, 29 | std::vector& side_input_signals) const; 30 | // Returns the latest side input index before (or at) the given timestamp. 31 | // Returns -1 if the first side input record is after the given timestamp. 32 | // This method runs in O(log N) where N is the number of side input records. 33 | int GetSideInputIndex(int64_t timestamp_sec) const; 34 | // The same method as GetSideInputIndex, but with a hint about what was the 35 | // previous side_input_index. If the prev_side_input_index is -1, then the 36 | // hint is ignored. The time complexity is O(1) if the given timestamp is 37 | // close enough to the the previous timestamp. 38 | int GetSideInputIndex(int64_t timestamp_sec, int prev_side_input_index) const; 39 | 40 | private: 41 | // Number of signals per side input record. 42 | int num_signals_ = 0; 43 | // All historical (increasing) side input timestamps (in seconds). 44 | std::vector timestamp_sec_history_; 45 | // Flattened vector of all historical side input signals. 46 | std::vector data_; 47 | }; 48 | 49 | } // namespace trader 50 | 51 | #endif // BASE_SIDE_INPUT_H 52 | -------------------------------------------------------------------------------- /base/trader.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef BASE_TRADER_H 4 | #define BASE_TRADER_H 5 | 6 | #include "base/base.h" 7 | 8 | namespace trader { 9 | 10 | // The trader is executed as follows: 11 | // - At every step the trader receives the latest OHLC tick T[i], some 12 | // additional side input signals (possibly an empty vector), and current 13 | // account balances. Based on this information the trader updates its own 14 | // internal state and data-structures. The current time of the trader is at 15 | // the end of the OHLC tick T[i] time period. (The trader does not receive 16 | // zero volume OHLC ticks. These OHLC ticks indicate a gap in a price history, 17 | // which could have been caused by an unresponsive exchange or its API.) 18 | // - Then the trader needs to decide what orders to emit. There are no other 19 | // active orders on the exchange at this moment (see the explanation below). 20 | // - Once the trader decides what orders to emit, the exchange will execute 21 | // (or cancel) all these orders on the follow-up OHLC tick T[i+1]. The trader 22 | // does not see the follow-up OHLC tick T[i+1] (nor any follow-up side input), 23 | // so it cannot peek into the future by design. 24 | // - Once all orders are executed (or canceled) by the exchange, the trader 25 | // receives the follow-up OHLC tick T[i+1] and the whole process repeats. 26 | // Note that at every step every order gets either executed or canceled by the 27 | // exchange. This is a design simplification so that there are no active orders 28 | // that the trader needs to maintain over time. 29 | // In practice, however, we would not cancel orders if they would be re-emitted 30 | // again. We would simply modify the existing orders (from the previous 31 | // iteration) based on the updated state. 32 | // Also note that the OHLC history sampling rate defines the frequency at which 33 | // the trader is updated and emits orders. Traders should be designed in 34 | // a frequency-agnostic way. In other words, they should have similar behavior 35 | // and performance regardless of how frequently they are called. Traders should 36 | // not assume anything about how often and when exactly they are called. One 37 | // reason for that is that the exchanges (or their APIs) sometimes become 38 | // unresponsive for random periods of time (and we see that e.g. in the gaps 39 | // in the price histories). Therefore, we encourage to test the traders on OHLC 40 | // histories with various sampling rates and gaps. 41 | class Trader { 42 | public: 43 | Trader() {} 44 | virtual ~Trader() {} 45 | 46 | // Updates the (internal) trader state and emits zero or more orders. 47 | // We assume that "orders" is not null and points to an empty vector. 48 | // This method is called consecutively (by the exchange) on every OHLC tick. 49 | // Trader can assume that there are no active orders when this method is 50 | // called. The emitted orders will be either executed or cancelled by the 51 | // exchange at the next OHLC tick. 52 | virtual void Update(const OhlcTick& ohlc_tick, 53 | const std::vector& side_input_signals, 54 | float base_balance, float quote_balance, 55 | std::vector& orders) = 0; 56 | 57 | // Returns the internal trader state (as a string). 58 | // Note that it is recommended to represent the internal state as a string of 59 | // (fixed number of) comma-separated values for easier analysis. 60 | virtual std::string GetInternalState() const = 0; 61 | }; 62 | 63 | // Usually we want to evaluate the same trader over different time periods. 64 | // This is where the trader emitter comes in handy, as it can emit a new 65 | // instance of the same trader (with the same configuration) whenever needed. 66 | class TraderEmitter { 67 | public: 68 | TraderEmitter() {} 69 | virtual ~TraderEmitter() {} 70 | 71 | // Returns a name identifying all traders emitted by this emitter. 72 | // The name should be escaped for the CSV file format. 73 | virtual std::string GetName() const = 0; 74 | 75 | // Returns a new (freshly initialized) instance of a trader. 76 | virtual std::unique_ptr NewTrader() const = 0; 77 | }; 78 | 79 | } // namespace trader 80 | 81 | #endif // BASE_TRADER_H 82 | -------------------------------------------------------------------------------- /eval/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//:__pkg__"]) 2 | 3 | load("@rules_proto//proto:defs.bzl", "proto_library") 4 | 5 | proto_library( 6 | name = "eval_proto", 7 | srcs = ["eval.proto"], 8 | deps = ["//base:base_proto"], 9 | ) 10 | 11 | cc_proto_library( 12 | name = "eval_cc_proto", 13 | deps = [":eval_proto"], 14 | ) 15 | 16 | cc_library( 17 | name = "eval", 18 | srcs = ["eval.cc"], 19 | hdrs = ["eval.h"], 20 | deps = [ 21 | ":eval_cc_proto", 22 | "//base", 23 | "//base:account", 24 | "//base:side_input", 25 | "//base:trader", 26 | "//indicators:volatility", 27 | "//logging:logger", 28 | "//util:time", 29 | ], 30 | ) 31 | 32 | cc_test( 33 | name = "eval_test", 34 | srcs = ["eval_test.cc"], 35 | deps = [ 36 | ":eval", 37 | "//logging:csv_logger", 38 | "//util:time", 39 | "@com_google_googletest//:gtest_main", 40 | "@com_google_absl//absl/memory", 41 | "@com_google_absl//absl/strings", 42 | "@com_google_absl//absl/strings:str_format", 43 | ], 44 | ) 45 | -------------------------------------------------------------------------------- /eval/eval.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "eval/eval.h" 4 | 5 | #include "indicators/volatility.h" 6 | #include "util/time.h" 7 | 8 | namespace trader { 9 | namespace { 10 | // Returns the average of values returned by the selector. 11 | template 12 | float GetAverage(const C& container, F selector) { 13 | if (container.empty()) { 14 | return 0; 15 | } 16 | float sum = 0; 17 | for (const auto& element : container) { 18 | sum += selector(element); 19 | } 20 | return sum / container.size(); 21 | } 22 | 23 | // Returns the geometric average of values returned by the selector. 24 | template 25 | float GetGeometricAverage(const C& container, F selector) { 26 | if (container.empty()) { 27 | return 0; 28 | } 29 | double mul = 1; 30 | for (const auto& element : container) { 31 | mul *= selector(element); 32 | } 33 | assert(mul >= 0); 34 | return static_cast(std::pow(mul, 1.0 / container.size())); 35 | } 36 | } // namespace 37 | 38 | ExecutionResult ExecuteTrader(const AccountConfig& account_config, 39 | OhlcHistory::const_iterator ohlc_history_begin, 40 | OhlcHistory::const_iterator ohlc_history_end, 41 | const SideInput* side_input, bool fast_eval, 42 | Trader& trader, Logger* logger) { 43 | ExecutionResult result; 44 | if (ohlc_history_begin == ohlc_history_end) { 45 | return {}; 46 | } 47 | Account account; 48 | account.InitAccount(account_config); 49 | std::vector side_input_signals; 50 | if (side_input != nullptr) { 51 | // The last signal is the age (in seconds) of the side input signals. 52 | side_input_signals.reserve(side_input->GetNumberOfSignals() + 1); 53 | } 54 | int prev_side_input_index = -1; 55 | std::vector orders; 56 | constexpr size_t kEmittedOrdersReserve = 8; 57 | orders.reserve(kEmittedOrdersReserve); 58 | int total_executed_orders = 0; 59 | Volatility base_volatility(/*window_size=*/0, 60 | /*period_size_sec=*/kSecondsPerDay); 61 | Volatility trader_volatility(/*window_size=*/0, 62 | /*period_size_sec=*/kSecondsPerDay); 63 | for (auto ohlc_tick_it = ohlc_history_begin; ohlc_tick_it != ohlc_history_end; 64 | ++ohlc_tick_it) { 65 | const OhlcTick& ohlc_tick = *ohlc_tick_it; 66 | if (side_input != nullptr) { 67 | const int side_input_index = side_input->GetSideInputIndex( 68 | ohlc_tick.timestamp_sec(), prev_side_input_index); 69 | if (side_input_index >= 0) { 70 | const int64_t side_input_timestamp_sec = 71 | side_input->GetSideInputTimestamp(side_input_index); 72 | side_input_signals.clear(); 73 | side_input->GetSideInputSignals(side_input_index, side_input_signals); 74 | // Adding the age (in seconds) of the side input signals. 75 | side_input_signals.push_back(ohlc_tick.timestamp_sec() - 76 | side_input_timestamp_sec); 77 | prev_side_input_index = side_input_index; 78 | } 79 | } 80 | // Log the current OHLC tick T[i] and the trader account. 81 | // Note: We do not explicitly log the side_input_signals as those can be 82 | // easily logged through trader internal state. 83 | if (logger != nullptr) { 84 | logger->LogExchangeState(ohlc_tick, account); 85 | } 86 | // The trader was updated on the previous OHLC tick T[i-1] and emitted 87 | // "orders". There are no other active orders on the exchange. 88 | // Execute (or cancel) "orders" on the current OHLC tick T[i]. 89 | for (const Order& order : orders) { 90 | const bool success = 91 | account.ExecuteOrder(account_config, order, ohlc_tick); 92 | if (success) { 93 | ++total_executed_orders; 94 | // Log only the executed orders and their impact on the trader account. 95 | if (logger != nullptr) { 96 | logger->LogExchangeState(ohlc_tick, account, order); 97 | } 98 | } 99 | } 100 | if (ohlc_tick.volume() == 0) { 101 | // Zero volume OHLC tick indicates a gap in a price history. Such gap 102 | // could have been caused by an unresponsive exchange (or its API). 103 | // Therefore, we do not update the trader and simply keep the previously 104 | // emitted orders. 105 | continue; 106 | } 107 | // Update the trader internal state on the current OHLC tick T[i]. 108 | // Emit a new set of "orders" for the next OHLC tick T[i+1]. 109 | orders.clear(); 110 | trader.Update(ohlc_tick, side_input_signals, account.base_balance, 111 | account.quote_balance, orders); 112 | if (logger != nullptr) { 113 | logger->LogTraderState(trader.GetInternalState()); 114 | } 115 | if (!fast_eval) { 116 | base_volatility.Update(ohlc_tick, /*base_balance=*/1.0f, 117 | /*quote_balance=*/0.0f); 118 | trader_volatility.Update(ohlc_tick, account.base_balance, 119 | account.quote_balance); 120 | } 121 | } 122 | result.set_start_base_balance(account_config.start_base_balance()); 123 | result.set_start_quote_balance(account_config.start_quote_balance()); 124 | result.set_end_base_balance(account.base_balance); 125 | result.set_end_quote_balance(account.quote_balance); 126 | result.set_start_price(ohlc_history_begin->close()); 127 | result.set_end_price((--ohlc_history_end)->close()); 128 | result.set_start_value(result.start_quote_balance() + 129 | result.start_price() * result.start_base_balance()); 130 | result.set_end_value(result.end_quote_balance() + 131 | result.end_price() * result.end_base_balance()); 132 | result.set_total_executed_orders(total_executed_orders); 133 | result.set_total_fee(account.total_fee); 134 | if (!fast_eval) { 135 | result.set_base_volatility(base_volatility.GetVolatility() * 136 | std::sqrt(365)); 137 | result.set_trader_volatility(trader_volatility.GetVolatility() * 138 | std::sqrt(365)); 139 | } 140 | return result; 141 | } 142 | 143 | EvaluationResult EvaluateTrader(const AccountConfig& account_config, 144 | const EvaluationConfig& eval_config, 145 | const OhlcHistory& ohlc_history, 146 | const SideInput* side_input, 147 | const TraderEmitter& trader_emitter, 148 | Logger* logger) { 149 | EvaluationResult eval_result; 150 | *eval_result.mutable_account_config() = account_config; 151 | *eval_result.mutable_eval_config() = eval_config; 152 | eval_result.set_name(trader_emitter.GetName()); 153 | for (int month_offset = 0;; ++month_offset) { 154 | const int64_t start_eval_timestamp_sec = AddMonthsToTimestampSec( 155 | eval_config.start_timestamp_sec(), month_offset); 156 | const int64_t end_eval_timestamp_sec = 157 | eval_config.evaluation_period_months() > 0 158 | ? AddMonthsToTimestampSec(start_eval_timestamp_sec, 159 | eval_config.evaluation_period_months()) 160 | : eval_config.end_timestamp_sec(); 161 | if (end_eval_timestamp_sec > eval_config.end_timestamp_sec()) { 162 | break; 163 | } 164 | const auto ohlc_history_subset = HistorySubset( 165 | ohlc_history, start_eval_timestamp_sec, end_eval_timestamp_sec); 166 | if (ohlc_history_subset.first == ohlc_history_subset.second) { 167 | continue; 168 | } 169 | std::unique_ptr trader = trader_emitter.NewTrader(); 170 | ExecutionResult result = ExecuteTrader( 171 | account_config, ohlc_history_subset.first, ohlc_history_subset.second, 172 | side_input, eval_config.fast_eval(), *trader, logger); 173 | EvaluationResult::Period* period = eval_result.add_period(); 174 | period->set_start_timestamp_sec(start_eval_timestamp_sec); 175 | period->set_end_timestamp_sec(end_eval_timestamp_sec); 176 | *period->mutable_result() = result; 177 | assert(result.start_value() > 0); 178 | period->set_final_gain(result.end_value() / result.start_value()); 179 | assert(result.start_price() > 0 && result.end_price() > 0); 180 | period->set_base_final_gain(result.end_price() / result.start_price()); 181 | if (eval_config.evaluation_period_months() == 0) { 182 | break; 183 | } 184 | } 185 | eval_result.set_score(GetGeometricAverage( 186 | eval_result.period(), [](const EvaluationResult::Period& period) { 187 | return period.final_gain() / period.base_final_gain(); 188 | })); 189 | eval_result.set_avg_gain(GetAverage( 190 | eval_result.period(), [](const EvaluationResult::Period& period) { 191 | return period.final_gain(); 192 | })); 193 | eval_result.set_avg_base_gain(GetAverage( 194 | eval_result.period(), [](const EvaluationResult::Period& period) { 195 | return period.base_final_gain(); 196 | })); 197 | eval_result.set_avg_total_executed_orders(GetAverage( 198 | eval_result.period(), [](const EvaluationResult::Period& period) { 199 | return period.result().total_executed_orders(); 200 | })); 201 | eval_result.set_avg_total_fee(GetAverage( 202 | eval_result.period(), [](const EvaluationResult::Period& period) { 203 | return period.result().total_fee(); 204 | })); 205 | return eval_result; 206 | } 207 | 208 | std::vector EvaluateBatchOfTraders( 209 | const AccountConfig& account_config, const EvaluationConfig& eval_config, 210 | const OhlcHistory& ohlc_history, const SideInput* side_input, 211 | const std::vector>& trader_emitters) { 212 | std::vector eval_results; 213 | std::vector> eval_result_futures; 214 | for (const auto& trader_emitter_ptr : trader_emitters) { 215 | const TraderEmitter& trader_emitter = *trader_emitter_ptr; 216 | eval_result_futures.emplace_back( 217 | std::async([&account_config, &eval_config, &ohlc_history, side_input, 218 | &trader_emitter]() { 219 | return EvaluateTrader(account_config, eval_config, ohlc_history, 220 | side_input, trader_emitter, 221 | /*logger=*/nullptr); 222 | })); 223 | } 224 | for (auto& eval_result_future : eval_result_futures) { 225 | eval_results.emplace_back(eval_result_future.get()); 226 | } 227 | return eval_results; 228 | } 229 | 230 | } // namespace trader 231 | -------------------------------------------------------------------------------- /eval/eval.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef EVAL_EVAL_H 4 | #define EVAL_EVAL_H 5 | 6 | #include "base/account.h" 7 | #include "base/base.h" 8 | #include "base/side_input.h" 9 | #include "base/trader.h" 10 | #include "eval/eval.pb.h" 11 | #include "logging/logger.h" 12 | 13 | namespace trader { 14 | 15 | // Executes an instance of a trader over a region of the OHLC history. 16 | // Returns the final trader's ExecutionResult at the end of the execution. 17 | ExecutionResult ExecuteTrader(const AccountConfig& account_config, 18 | OhlcHistory::const_iterator ohlc_history_begin, 19 | OhlcHistory::const_iterator ohlc_history_end, 20 | const SideInput* side_input, bool fast_eval, 21 | Trader& trader, Logger* logger); 22 | 23 | // Evaluates a single (type of) trader (as emitted by the trader_emitter) 24 | // over one or more regions of the OHLC history (as defined by the 25 | // eval_config). Returns trader's EvaluationResult. 26 | EvaluationResult EvaluateTrader(const AccountConfig& account_config, 27 | const EvaluationConfig& eval_config, 28 | const OhlcHistory& ohlc_history, 29 | const SideInput* side_input, 30 | const TraderEmitter& trader_emitter, 31 | Logger* logger); 32 | 33 | // Evaluates (in parallel) a batch of traders (as emitted by the vector of 34 | // trader_emitters) over one or more regions of the OHLC history. 35 | std::vector EvaluateBatchOfTraders( 36 | const AccountConfig& account_config, const EvaluationConfig& eval_config, 37 | const OhlcHistory& ohlc_history, const SideInput* side_input, 38 | const std::vector>& trader_emitters); 39 | 40 | } // namespace trader 41 | 42 | #endif // EVAL_EVAL_H 43 | -------------------------------------------------------------------------------- /eval/eval.proto: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | syntax = "proto2"; 4 | 5 | import "base/base.proto"; 6 | 7 | package trader; 8 | 9 | // Result of trader execution over a region of the OHLC history. 10 | message ExecutionResult { 11 | // Base (crypto) currency balance at the beginning of trader execution. 12 | optional float start_base_balance = 1; 13 | // Quote balance in quote currency at the beginning of trader execution. 14 | optional float start_quote_balance = 2; 15 | // Base (crypto) currency balance at the end of trader execution. 16 | optional float end_base_balance = 3; 17 | // Quote balance in quote currency at the end of trader execution. 18 | optional float end_quote_balance = 4; 19 | // Base (crypto) currency price at the beginning of trader execution. 20 | optional float start_price = 5; 21 | // Base (crypto) currency price at the end of trader execution. 22 | optional float end_price = 6; 23 | // Total value (in quote currency) at the beginning of trader execution. 24 | optional float start_value = 7; 25 | // Total value (in quote currency) at the end of trader execution. 26 | optional float end_value = 8; 27 | // Total number of executed exchange orders. 28 | optional int32 total_executed_orders = 9; 29 | // Total accumulated transaction fee (in quote currency). 30 | optional float total_fee = 10; 31 | // Annual volatility of the baseline's (Buy and HODL) portfolio, defined as: 32 | // Standard deviation of the baseline's daily logarithmic returns 33 | // multiplied by std::sqrt(365). 34 | optional float base_volatility = 11; 35 | // Annual volatility of the trader's portfolio, defined as: 36 | // Standard deviation of the trader's daily logarithmic returns 37 | // multiplied by std::sqrt(365). 38 | optional float trader_volatility = 12; 39 | } 40 | 41 | // Trader evaluation configuration. 42 | message EvaluationConfig { 43 | // Starting UNIX timestamp (in seconds). 44 | optional int64 start_timestamp_sec = 1; 45 | // Ending UNIX timestamp (in seconds). 46 | optional int64 end_timestamp_sec = 2; 47 | // Length of evaluation period (in months). 48 | optional int32 evaluation_period_months = 3; 49 | // When true, avoids computing volatility (to speed up the computation). 50 | // This is useful when evaluating a batch of traders in parallel. 51 | optional bool fast_eval = 4; 52 | } 53 | 54 | // Trader evaluation result for a given evaluation configuration. 55 | message EvaluationResult { 56 | // Trader account configuration. 57 | optional AccountConfig account_config = 1; 58 | // Evaluation configuration. 59 | optional EvaluationConfig eval_config = 2; 60 | // String representation of the trader (trader name and configuration). 61 | optional string name = 3; 62 | // Trader evaluation over a specific period. 63 | message Period { 64 | // Starting UNIX timestamp (in seconds) of the evaluation period (included). 65 | optional int64 start_timestamp_sec = 1; 66 | // Ending UNIX timestamp (in seconds) of the evaluation period (excluded). 67 | optional int64 end_timestamp_sec = 2; 68 | // Result of trader execution over the period. 69 | optional ExecutionResult result = 3; 70 | // Final percentual gain of the trader (after fees). 71 | optional float final_gain = 4; 72 | // Final percentual gain of the baseline (Buy and HODL) method. 73 | optional float base_final_gain = 5; 74 | } 75 | // Trader evaluation over multiple periods. 76 | repeated Period period = 4; 77 | // Trader evaluation score. 78 | optional float score = 5; 79 | // Average percentual gain of the trader (after fees). 80 | optional float avg_gain = 6; 81 | // Average percentual gain of the baseline (Buy and HODL) method. 82 | optional float avg_base_gain = 7; 83 | // Average total number of executed orders. 84 | optional float avg_total_executed_orders = 8; 85 | // Average total trader fees in quote currency. 86 | optional float avg_total_fee = 9; 87 | } 88 | -------------------------------------------------------------------------------- /fear_and_greed_index.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Crypto Fear & Greed Index\n", 8 | "\n", 9 | "Source: https://alternative.me/crypto/fear-and-greed-index/\n", 10 | "\n", 11 | "This code allows you to download the Crypto Fear & Greed Index for Bitcoin into a CSV file, which can be then used as a side input for your trading strategy." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import pandas as pd\n", 22 | "import urllib.request, json\n", 23 | "\n", 24 | "fear_and_greed_index_data = {\n", 25 | " 'timestamp_sec': [],\n", 26 | " 'value': [],\n", 27 | "}\n", 28 | "\n", 29 | "with urllib.request.urlopen(\n", 30 | " 'https://api.alternative.me/fng/?limit=10000') as url:\n", 31 | " fear_and_greed_index_json = json.loads(url.read().decode())\n", 32 | " for data in fear_and_greed_index_json['data']:\n", 33 | " fear_and_greed_index_data['timestamp_sec'].append(int(data['timestamp']))\n", 34 | " fear_and_greed_index_data['value'].append(float(data['value']))\n", 35 | "\n", 36 | "fear_and_greed_frame = pd.DataFrame(fear_and_greed_index_data).sort_values(\n", 37 | " by='timestamp_sec').reset_index(drop=True)[['timestamp_sec', 'value']]\n", 38 | "\n", 39 | "fear_and_greed_frame.to_csv('data/fear_and_greed_index.csv',\n", 40 | " index=False,\n", 41 | " header=None)\n", 42 | "\n", 43 | "fear_and_greed_frame[:5]" 44 | ] 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "Python 3", 50 | "language": "python", 51 | "name": "python3" 52 | }, 53 | "language_info": { 54 | "codemirror_mode": { 55 | "name": "ipython", 56 | "version": 3 57 | }, 58 | "file_extension": ".py", 59 | "mimetype": "text/x-python", 60 | "name": "python", 61 | "nbconvert_exporter": "python", 62 | "pygments_lexer": "ipython3", 63 | "version": "3.9.1" 64 | } 65 | }, 66 | "nbformat": 4, 67 | "nbformat_minor": 4 68 | } 69 | -------------------------------------------------------------------------------- /gmock.BUILD: -------------------------------------------------------------------------------- 1 | cc_library( 2 | name = "gtest", 3 | srcs = [ 4 | "googlemock/src/gmock-all.cc", 5 | "googletest/src/gtest-all.cc", 6 | ], 7 | hdrs = glob([ 8 | "**/*.h", 9 | "googletest/src/*.cc", 10 | "googlemock/src/*.cc", 11 | ]), 12 | includes = [ 13 | "googlemock", 14 | "googlemock/include", 15 | "googletest", 16 | "googletest/include", 17 | ], 18 | linkopts = ["-pthread"], 19 | visibility = ["//visibility:public"], 20 | ) 21 | 22 | cc_library( 23 | name = "gtest_main", 24 | srcs = ["googlemock/src/gmock_main.cc"], 25 | linkopts = ["-pthread"], 26 | visibility = ["//visibility:public"], 27 | deps = [":gtest"], 28 | ) 29 | -------------------------------------------------------------------------------- /indicators/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//traders:__pkg__"]) 2 | 3 | load("@rules_proto//proto:defs.bzl", "proto_library") 4 | 5 | cc_library( 6 | name = "util", 7 | srcs = ["util.cc"], 8 | hdrs = ["util.h"], 9 | ) 10 | 11 | cc_test( 12 | name = "util_test", 13 | srcs = ["util_test.cc"], 14 | deps = [ 15 | ":util", 16 | "@com_google_googletest//:gtest_main", 17 | ], 18 | ) 19 | 20 | cc_library( 21 | name = "test_util", 22 | srcs = ["test_util.cc"], 23 | hdrs = ["test_util.h"], 24 | deps = [ 25 | "//base", 26 | "@com_google_googletest//:gtest_main", 27 | ], 28 | ) 29 | 30 | cc_library( 31 | name = "last_n_ohlc_ticks", 32 | srcs = ["last_n_ohlc_ticks.cc"], 33 | hdrs = ["last_n_ohlc_ticks.h"], 34 | deps = ["//base"], 35 | ) 36 | 37 | cc_test( 38 | name = "last_n_ohlc_ticks_test", 39 | srcs = ["last_n_ohlc_ticks_test.cc"], 40 | deps = [ 41 | ":last_n_ohlc_ticks", 42 | ":test_util", 43 | "@com_google_googletest//:gtest_main", 44 | ], 45 | ) 46 | 47 | cc_library( 48 | name = "simple_moving_average", 49 | srcs = ["simple_moving_average.cc"], 50 | hdrs = ["simple_moving_average.h"], 51 | deps = [ 52 | ":last_n_ohlc_ticks", 53 | "//base", 54 | ], 55 | ) 56 | 57 | cc_test( 58 | name = "simple_moving_average_test", 59 | srcs = ["simple_moving_average_test.cc"], 60 | deps = [ 61 | ":simple_moving_average", 62 | ":test_util", 63 | "@com_google_googletest//:gtest_main", 64 | ], 65 | ) 66 | 67 | cc_library( 68 | name = "exponential_moving_average", 69 | srcs = ["exponential_moving_average.cc"], 70 | hdrs = ["exponential_moving_average.h"], 71 | deps = [ 72 | ":last_n_ohlc_ticks", 73 | ":util", 74 | "//base", 75 | ], 76 | ) 77 | 78 | cc_test( 79 | name = "exponential_moving_average_test", 80 | srcs = ["exponential_moving_average_test.cc"], 81 | deps = [ 82 | ":exponential_moving_average", 83 | ":test_util", 84 | "@com_google_googletest//:gtest_main", 85 | ], 86 | ) 87 | 88 | cc_library( 89 | name = "moving_average_convergence_divergence", 90 | srcs = ["moving_average_convergence_divergence.cc"], 91 | hdrs = ["moving_average_convergence_divergence.h"], 92 | deps = [ 93 | ":last_n_ohlc_ticks", 94 | ":util", 95 | "//base", 96 | ], 97 | ) 98 | 99 | cc_test( 100 | name = "moving_average_convergence_divergence_test", 101 | srcs = ["moving_average_convergence_divergence_test.cc"], 102 | deps = [ 103 | ":moving_average_convergence_divergence", 104 | ":test_util", 105 | "@com_google_googletest//:gtest_main", 106 | ], 107 | ) 108 | 109 | cc_library( 110 | name = "relative_strength_index", 111 | srcs = ["relative_strength_index.cc"], 112 | hdrs = ["relative_strength_index.h"], 113 | deps = [ 114 | ":last_n_ohlc_ticks", 115 | ":util", 116 | "//base", 117 | ], 118 | ) 119 | 120 | cc_test( 121 | name = "relative_strength_index_test", 122 | srcs = ["relative_strength_index_test.cc"], 123 | deps = [ 124 | ":relative_strength_index", 125 | ":test_util", 126 | "@com_google_googletest//:gtest_main", 127 | ], 128 | ) 129 | 130 | cc_library( 131 | name = "stochastic_oscillator", 132 | srcs = ["stochastic_oscillator.cc"], 133 | hdrs = ["stochastic_oscillator.h"], 134 | deps = [ 135 | ":last_n_ohlc_ticks", 136 | ":util", 137 | "//base", 138 | ], 139 | ) 140 | 141 | cc_test( 142 | name = "stochastic_oscillator_test", 143 | srcs = ["stochastic_oscillator_test.cc"], 144 | deps = [ 145 | ":stochastic_oscillator", 146 | ":test_util", 147 | "@com_google_googletest//:gtest_main", 148 | ], 149 | ) 150 | 151 | cc_library( 152 | name = "volatility", 153 | srcs = ["volatility.cc"], 154 | hdrs = ["volatility.h"], 155 | visibility = ["//eval:__pkg__"], 156 | deps = [ 157 | ":last_n_ohlc_ticks", 158 | ":util", 159 | "//base", 160 | ], 161 | ) 162 | 163 | cc_test( 164 | name = "volatility_test", 165 | srcs = ["volatility_test.cc"], 166 | deps = [ 167 | ":test_util", 168 | ":volatility", 169 | "@com_google_googletest//:gtest_main", 170 | ], 171 | ) 172 | -------------------------------------------------------------------------------- /indicators/exponential_moving_average.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/exponential_moving_average.h" 4 | 5 | namespace trader { 6 | 7 | ExponentialMovingAverage::ExponentialMovingAverage(float smoothing, 8 | int ema_length, 9 | int period_size_sec) 10 | : last_n_ohlc_ticks_(/*num_ohlc_ticks=*/1, period_size_sec), 11 | weight_(smoothing / (1.0f + ema_length)) { 12 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 13 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 14 | // We have observed at least 1 OHLC tick. 15 | // The most recent OHLC tick was updated. 16 | assert(ema_helper_.GetNumValues() >= 1); 17 | ema_helper_.UpdateCurrentValue(new_ohlc_tick.close(), weight_); 18 | }); 19 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 20 | [this](const OhlcTick& new_ohlc_tick) { 21 | // This is the very first observed OHLC tick. 22 | assert(ema_helper_.GetNumValues() == 0); 23 | ema_helper_.AddNewValue(new_ohlc_tick.close(), weight_); 24 | }); 25 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 26 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 27 | // We have observed at least 1 OHLC tick. 28 | // New OHLC tick was added. 29 | assert(ema_helper_.GetNumValues() >= 1); 30 | ema_helper_.AddNewValue(new_ohlc_tick.close(), weight_); 31 | }); 32 | } 33 | 34 | float ExponentialMovingAverage::GetExponentialMovingAverage() const { 35 | return ema_helper_.GetExponentialMovingAverage(); 36 | } 37 | 38 | int ExponentialMovingAverage::GetNumOhlcTicks() const { 39 | return ema_helper_.GetNumValues(); 40 | } 41 | 42 | void ExponentialMovingAverage::Update(const OhlcTick& ohlc_tick) { 43 | last_n_ohlc_ticks_.Update(ohlc_tick); 44 | } 45 | 46 | } // namespace trader 47 | -------------------------------------------------------------------------------- /indicators/exponential_moving_average.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_EXPONENTIAL_MOVING_AVERAGE_H 4 | #define INDICATORS_EXPONENTIAL_MOVING_AVERAGE_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | #include "indicators/util.h" 9 | 10 | namespace trader { 11 | 12 | // Calculates the Exponential Moving Average (EMA) of the closing prices over 13 | // all (previous) OHLC ticks with a specified period size (in seconds). 14 | // We assume that the period is divisible by the period of update OHLC ticks. 15 | // Based on: https://www.investopedia.com/terms/m/movingaverage.asp 16 | // and: https://www.investopedia.com/terms/e/ema.asp 17 | class ExponentialMovingAverage { 18 | public: 19 | // Constructor. 20 | // smoothing: Smoothing factor, the most common choice is 2. 21 | // ema_length: EMA length, typically 10, 50, or 200. 22 | // period_size_sec: Period of the OHLC ticks (in seconds). 23 | ExponentialMovingAverage(float smoothing, int ema_length, 24 | int period_size_sec); 25 | virtual ~ExponentialMovingAverage() {} 26 | 27 | // Returns Exponential Moving Average (of closing prices) over all (previous) 28 | // OHLC ticks. This method runs in O(1) time. 29 | virtual float GetExponentialMovingAverage() const; 30 | 31 | // Returns the number of seen OHLC ticks. This method runs in O(1) time. 32 | virtual int GetNumOhlcTicks() const; 33 | 34 | // Updates the Exponential Moving Average. 35 | // This method has the same time complexity as the LastNOhlcTicks::Update 36 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 37 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 38 | virtual void Update(const OhlcTick& ohlc_tick); 39 | 40 | private: 41 | // Keeps track of the current OHLC tick. 42 | LastNOhlcTicks last_n_ohlc_ticks_; 43 | 44 | // Weight of the new closing prices in the Exponential Moving Average. 45 | float weight_ = 0; 46 | // Exponential Moving Average helper. 47 | ExponentialMovingAverageHelper ema_helper_; 48 | }; 49 | 50 | } // namespace trader 51 | 52 | #endif // INDICATORS_EXPONENTIAL_MOVING_AVERAGE_H 53 | -------------------------------------------------------------------------------- /indicators/exponential_moving_average_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/exponential_moving_average.h" 4 | 5 | #include "gtest/gtest.h" 6 | #include "indicators/test_util.h" 7 | 8 | namespace trader { 9 | using ::trader::testing::PrepareExampleOhlcHistory; 10 | 11 | TEST(ExponentialMovingAverageTest, GetEMAWhenAdding8HourOhlcTicks) { 12 | OhlcHistory ohlc_history; 13 | PrepareExampleOhlcHistory(ohlc_history); 14 | 15 | ExponentialMovingAverage exponential_moving_average( 16 | /*smoothing=*/2, 17 | /*ema_length=*/7, 18 | /*period_size_sec=*/kSecondsPerDay); 19 | 20 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 0); 21 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 0); 22 | 23 | // O: 100 H: 150 L: 80 C: 120 V: 1000 T: 2017-01-01 00:00 24 | // --- Daily History --- 25 | // O: 100 H: 150 L: 80 C: 120 V: 1000 T: 2017-01-01 (Day 1) 26 | exponential_moving_average.Update(ohlc_history[0]); 27 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 28 | 120); 29 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 1); 30 | 31 | // O: 120 H: 180 L: 100 C: 150 V: 1000 T: 2017-01-01 08:00 32 | // --- Daily History --- 33 | // O: 100 H: 180 L: 80 C: 150 V: 2000 T: 2017-01-01 (Day 1) 34 | exponential_moving_average.Update(ohlc_history[1]); 35 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 36 | 150); 37 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 1); 38 | 39 | // O: 150 H: 250 L: 100 C: 140 V: 1000 T: 2017-01-01 16:00 40 | // --- Daily History --- 41 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 42 | exponential_moving_average.Update(ohlc_history[2]); 43 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 44 | 140); 45 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 1); 46 | 47 | // O: 140 H: 150 L: 80 C: 100 V: 1000 T: 2017-01-02 00:00 (+1 Day) 48 | // --- Daily History --- 49 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 50 | // O: 140 H: 150 L: 80 C: 100 V: 1000 T: 2017-01-02 (Day 2) 51 | exponential_moving_average.Update(ohlc_history[3]); 52 | EXPECT_FLOAT_EQ( 53 | exponential_moving_average.GetExponentialMovingAverage(), 54 | 100.0f * (2.0f / (1.0f + 7.0f)) + 140.0f * (1.0f - 2.0f / (1.0f + 7.0f))); 55 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 2); 56 | 57 | // O: 100 H: 120 L: 20 C: 50 V: 1000 T: 2017-01-02 08:00 58 | // --- Daily History --- 59 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 60 | // O: 140 H: 150 L: 20 C: 50 V: 2000 T: 2017-01-02 (Day 2) 61 | exponential_moving_average.Update(ohlc_history[4]); 62 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 63 | 50.0f * 0.25f + 140.0f * 0.75f); 64 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 2); 65 | 66 | // O: 50 H: 100 L: 40 C: 80 V: 1000 T: 2017-01-02 16:00 67 | // --- Daily History --- 68 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 69 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 70 | exponential_moving_average.Update(ohlc_history[5]); 71 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 72 | 80.0f * 0.25f + 140.0f * 0.75f); 73 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 2); 74 | 75 | // O: 80 H: 180 L: 50 C: 150 V: 1000 T: 2017-01-03 00:00 (+1 Day) 76 | // --- Daily History --- 77 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 78 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 79 | // O: 80 H: 180 L: 50 C: 150 V: 1000 T: 2017-01-03 (Day 3) 80 | exponential_moving_average.Update(ohlc_history[6]); 81 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 82 | 150.0f * 0.25f + (80.0f * 0.25f + 140.0f * 0.75f) * 0.75f); 83 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 3); 84 | 85 | // O: 150 H: 250 L: 120 C: 240 V: 1000 T: 2017-01-03 08:00 86 | // --- Daily History --- 87 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 88 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 89 | // O: 80 H: 250 L: 50 C: 240 V: 2000 T: 2017-01-03 (Day 3) 90 | exponential_moving_average.Update(ohlc_history[7]); 91 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 92 | 240.0f * 0.25f + 125.0f * 0.75f); 93 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 3); 94 | 95 | // O: 240 H: 450 L: 220 C: 400 V: 1000 T: 2017-01-03 16:00 96 | // --- Daily History --- 97 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 98 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 99 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 100 | exponential_moving_average.Update(ohlc_history[8]); 101 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 102 | 400.0f * 0.25f + 125.0f * 0.75f); 103 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 3); 104 | 105 | // O: 400 H: 450 L: 250 C: 300 V: 1000 T: 2017-01-04 00:00 (+1 Day) 106 | // --- Daily History --- 107 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 108 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 109 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 110 | // O: 400 H: 450 L: 250 C: 300 V: 1000 T: 2017-01-04 (Day 4) 111 | exponential_moving_average.Update(ohlc_history[9]); 112 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 113 | 300.0f * 0.25f + (400.0f * 0.25f + 125.0f * 0.75f) * 0.75f); 114 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 4); 115 | 116 | // O: 300 H: 700 L: 220 C: 650 V: 1000 T: 2017-01-04 08:00 117 | // --- Daily History --- 118 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 119 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 120 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 121 | // O: 400 H: 700 L: 220 C: 650 V: 2000 T: 2017-01-04 (Day 4) 122 | exponential_moving_average.Update(ohlc_history[10]); 123 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 124 | 650.0f * 0.25f + 193.75 * 0.75f); 125 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 4); 126 | 127 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-04 16:00 128 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 00:00 (+1 Day) 129 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 08:00 130 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 16:00 131 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-06 00:00 (+1 Day) 132 | // O: 650 H: 800 L: 600 C: 750 V: 1000 T: 2017-01-06 08:00 133 | // --- Daily History --- 134 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 135 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 136 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 137 | // O: 400 H: 700 L: 220 C: 650 V: 2000 T: 2017-01-04 (Day 4) 138 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 (Day 5) 139 | // O: 650 H: 800 L: 600 C: 750 V: 1000 T: 2017-01-06 (Day 6) 140 | exponential_moving_average.Update(ohlc_history[11]); 141 | EXPECT_FLOAT_EQ(exponential_moving_average.GetExponentialMovingAverage(), 142 | 750.0f * 0.25f + // nowrap 143 | (650.0f * 0.25f + // nowrap 144 | (650.0f * 0.25f + 193.75 * 0.75f) * 0.75f) * 145 | 0.75f); 146 | EXPECT_EQ(exponential_moving_average.GetNumOhlcTicks(), 6); 147 | } 148 | 149 | } // namespace trader 150 | -------------------------------------------------------------------------------- /indicators/last_n_ohlc_ticks.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/last_n_ohlc_ticks.h" 4 | 5 | namespace trader { 6 | 7 | void LastNOhlcTicks::RegisterLastTickUpdatedCallback( 8 | LastTickUpdatedCallback last_tick_updated_callback) { 9 | last_tick_updated_callback_ = last_tick_updated_callback; 10 | } 11 | 12 | void LastNOhlcTicks::RegisterNewTickAddedCallback( 13 | NewTickAddedCallback new_tick_added_callback) { 14 | new_tick_added_callback_ = new_tick_added_callback; 15 | } 16 | 17 | void LastNOhlcTicks::RegisterNewTickAddedAndOldestTickRemovedCallback( 18 | NewTickAddedAndOldestTickRemovedCallback 19 | new_tick_added_and_oldest_tick_removed_callback) { 20 | new_tick_added_and_oldest_tick_removed_callback_ = 21 | new_tick_added_and_oldest_tick_removed_callback; 22 | } 23 | 24 | const std::deque& LastNOhlcTicks::GetLastNOhlcTicks() const { 25 | return last_n_ohlc_ticks_; 26 | } 27 | 28 | void LastNOhlcTicks::Update(const OhlcTick& ohlc_tick) { 29 | const int64_t adjusted_timestamp_sec = 30 | period_size_sec_ * (ohlc_tick.timestamp_sec() / period_size_sec_); 31 | while (!last_n_ohlc_ticks_.empty() && 32 | last_n_ohlc_ticks_.back().timestamp_sec() + period_size_sec_ < 33 | adjusted_timestamp_sec) { 34 | const int64_t prev_timestamp_sec = 35 | last_n_ohlc_ticks_.back().timestamp_sec(); 36 | const float prev_close = last_n_ohlc_ticks_.back().close(); 37 | last_n_ohlc_ticks_.emplace_back(); 38 | OhlcTick* top_ohlc_tick = &last_n_ohlc_ticks_.back(); 39 | top_ohlc_tick->set_timestamp_sec(prev_timestamp_sec + period_size_sec_); 40 | top_ohlc_tick->set_open(prev_close); 41 | top_ohlc_tick->set_high(prev_close); 42 | top_ohlc_tick->set_low(prev_close); 43 | top_ohlc_tick->set_close(prev_close); 44 | top_ohlc_tick->set_volume(0); 45 | NewOhlcTickAdded(); 46 | } 47 | if (last_n_ohlc_ticks_.empty() || 48 | last_n_ohlc_ticks_.back().timestamp_sec() < adjusted_timestamp_sec) { 49 | last_n_ohlc_ticks_.push_back(ohlc_tick); 50 | last_n_ohlc_ticks_.back().set_timestamp_sec(adjusted_timestamp_sec); 51 | NewOhlcTickAdded(); 52 | } else { 53 | assert(last_n_ohlc_ticks_.back().timestamp_sec() == adjusted_timestamp_sec); 54 | OhlcTick* top_ohlc_tick = &last_n_ohlc_ticks_.back(); 55 | const OhlcTick old_ohlc_tick_copy = *top_ohlc_tick; 56 | top_ohlc_tick->set_high(std::max(top_ohlc_tick->high(), ohlc_tick.high())); 57 | top_ohlc_tick->set_low(std::min(top_ohlc_tick->low(), ohlc_tick.low())); 58 | top_ohlc_tick->set_close(ohlc_tick.close()); 59 | top_ohlc_tick->set_volume(top_ohlc_tick->volume() + ohlc_tick.volume()); 60 | LastOhlcTickUpdated(old_ohlc_tick_copy); 61 | } 62 | } 63 | 64 | void LastNOhlcTicks::NewOhlcTickAdded() { 65 | if (last_n_ohlc_ticks_.size() <= num_ohlc_ticks_) { 66 | if (new_tick_added_callback_) { 67 | new_tick_added_callback_(last_n_ohlc_ticks_.back()); 68 | } 69 | } else { 70 | const OhlcTick removed_ohlc_tick_copy = last_n_ohlc_ticks_.front(); 71 | last_n_ohlc_ticks_.pop_front(); 72 | if (new_tick_added_and_oldest_tick_removed_callback_) { 73 | new_tick_added_and_oldest_tick_removed_callback_( 74 | removed_ohlc_tick_copy, last_n_ohlc_ticks_.back()); 75 | } 76 | } 77 | } 78 | 79 | void LastNOhlcTicks::LastOhlcTickUpdated(const OhlcTick& old_ohlc_tick) { 80 | if (last_tick_updated_callback_) { 81 | last_tick_updated_callback_(old_ohlc_tick, last_n_ohlc_ticks_.back()); 82 | } 83 | } 84 | 85 | } // namespace trader 86 | -------------------------------------------------------------------------------- /indicators/last_n_ohlc_ticks.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_LAST_N_OHLC_TICKS_H 4 | #define INDICATORS_LAST_N_OHLC_TICKS_H 5 | 6 | #include "base/base.h" 7 | 8 | namespace trader { 9 | 10 | // Keeps track of the last N OHLC ticks with a specified period (in seconds). 11 | // We assume that this period is divisible by the period of update OHLC ticks. 12 | class LastNOhlcTicks { 13 | public: 14 | // Called after the last OHLC tick was updated, but no OHLC tick was added. 15 | // This happens when the OHLC tick provided in the Update method (below) 16 | // is fully contained in the period of the most recent OHLC tick in the deque. 17 | // old_ohlc_tick: The previous OHLC tick in the deque that was updated. 18 | // new_ohlc_tick: The updated (most recent) OHLC tick in the deque. 19 | using LastTickUpdatedCallback = std::function; 21 | // Called after a new OHLC tick was added to the deque. 22 | // This happens when the OHLC tick provided in the Update method (below) 23 | // starts after the period of the most recent OHLC tick in the deque. 24 | // new_ohlc_tick: The newly added (most recent) OHLC tick in the deque. 25 | using NewTickAddedCallback = 26 | std::function; 27 | // Called after a new OHLC tick was added to the deque and the oldest OHLC 28 | // tick was removed from the deque. 29 | // This happens when the deque grows beyond N OHLC ticks, so we need to remove 30 | // the oldest one. 31 | // removed_ohlc_tick: The oldest OHLC tick that was removed from deque. 32 | // new_ohlc_tick: The newly added (most recent) OHLC tick in the deque. 33 | using NewTickAddedAndOldestTickRemovedCallback = std::function; 35 | 36 | // Constructor. 37 | // num_ohlc_ticks: Number N of OHLC ticks that we want to keep in the deque. 38 | // period_size_sec: Period of the kept OHLC ticks (in seconds). 39 | LastNOhlcTicks(int num_ohlc_ticks, int period_size_sec) 40 | : num_ohlc_ticks_(num_ohlc_ticks), period_size_sec_(period_size_sec) { 41 | assert(num_ohlc_ticks > 0); 42 | assert(period_size_sec > 0); 43 | } 44 | virtual ~LastNOhlcTicks() {} 45 | 46 | virtual void RegisterLastTickUpdatedCallback( 47 | LastTickUpdatedCallback last_tick_updated_callback); 48 | virtual void RegisterNewTickAddedCallback( 49 | NewTickAddedCallback new_tick_added_callback); 50 | virtual void RegisterNewTickAddedAndOldestTickRemovedCallback( 51 | NewTickAddedAndOldestTickRemovedCallback 52 | new_tick_added_and_oldest_tick_removed_callback); 53 | 54 | // Returns the deque of last N OHLC ticks. 55 | virtual const std::deque& GetLastNOhlcTicks() const; 56 | 57 | // Updates the deque of last N OHLC ticks. 58 | // Under normal circumstances this method runs in O(1) time. 59 | // The only exception is when the given OHLC tick is far in the future, in 60 | // which case we need to backfill all the intermediate zero volume OHLC ticks. 61 | // We assume that period_size_sec_ is divisible by the period of ohlc_tick. 62 | virtual void Update(const OhlcTick& ohlc_tick); 63 | 64 | private: 65 | int num_ohlc_ticks_ = 0; 66 | int period_size_sec_ = 0; 67 | 68 | std::deque last_n_ohlc_ticks_; 69 | 70 | LastTickUpdatedCallback last_tick_updated_callback_; 71 | NewTickAddedCallback new_tick_added_callback_; 72 | NewTickAddedAndOldestTickRemovedCallback 73 | new_tick_added_and_oldest_tick_removed_callback_; 74 | 75 | void NewOhlcTickAdded(); 76 | void LastOhlcTickUpdated(const OhlcTick& old_ohlc_tick); 77 | }; 78 | 79 | } // namespace trader 80 | 81 | #endif // INDICATORS_LAST_N_OHLC_TICKS_H 82 | -------------------------------------------------------------------------------- /indicators/moving_average_convergence_divergence.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/moving_average_convergence_divergence.h" 4 | 5 | namespace trader { 6 | 7 | MovingAverageConvergenceDivergence::MovingAverageConvergenceDivergence( 8 | int fast_length, int slow_length, int signal_smoothing, int period_size_sec) 9 | : last_n_ohlc_ticks_(/*num_ohlc_ticks=*/1, period_size_sec), 10 | fast_weight_(/*smoothing=*/2.0f / (1.0f + fast_length)), 11 | slow_weight_(/*smoothing=*/2.0f / (1.0f + slow_length)), 12 | signal_weight_(/*smoothing=*/2.0f / (1.0f + signal_smoothing)) { 13 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 14 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 15 | // We have observed at least 1 OHLC tick. 16 | // The most recent OHLC tick was updated. 17 | assert(num_ohlc_ticks_ >= 1); 18 | LastTickUpdated(new_ohlc_tick); 19 | }); 20 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 21 | [this](const OhlcTick& new_ohlc_tick) { 22 | // This is the very first observed OHLC tick. 23 | assert(num_ohlc_ticks_ == 0); 24 | NewTickAdded(new_ohlc_tick); 25 | }); 26 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 27 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 28 | // We have observed at least 1 OHLC tick. 29 | // New OHLC tick was added. 30 | assert(num_ohlc_ticks_ >= 1); 31 | NewTickAdded(new_ohlc_tick); 32 | }); 33 | } 34 | 35 | float MovingAverageConvergenceDivergence::GetFastExponentialMovingAverage() 36 | const { 37 | return fast_ema_.GetExponentialMovingAverage(); 38 | } 39 | 40 | float MovingAverageConvergenceDivergence::GetSlowExponentialMovingAverage() 41 | const { 42 | return slow_ema_.GetExponentialMovingAverage(); 43 | } 44 | 45 | float MovingAverageConvergenceDivergence::GetMACDSeries() const { 46 | return GetFastExponentialMovingAverage() - GetSlowExponentialMovingAverage(); 47 | } 48 | 49 | float MovingAverageConvergenceDivergence::GetMACDSignal() const { 50 | return signal_ema_.GetExponentialMovingAverage(); 51 | } 52 | 53 | float MovingAverageConvergenceDivergence::GetDivergence() const { 54 | return GetMACDSeries() - GetMACDSignal(); 55 | } 56 | 57 | int MovingAverageConvergenceDivergence::GetNumOhlcTicks() const { 58 | return num_ohlc_ticks_; 59 | } 60 | 61 | void MovingAverageConvergenceDivergence::Update(const OhlcTick& ohlc_tick) { 62 | last_n_ohlc_ticks_.Update(ohlc_tick); 63 | } 64 | 65 | void MovingAverageConvergenceDivergence::LastTickUpdated( 66 | const OhlcTick& ohlc_tick) { 67 | fast_ema_.UpdateCurrentValue(ohlc_tick.close(), fast_weight_); 68 | slow_ema_.UpdateCurrentValue(ohlc_tick.close(), slow_weight_); 69 | signal_ema_.UpdateCurrentValue(GetMACDSeries(), signal_weight_); 70 | } 71 | 72 | void MovingAverageConvergenceDivergence::NewTickAdded( 73 | const OhlcTick& ohlc_tick) { 74 | ++num_ohlc_ticks_; 75 | fast_ema_.AddNewValue(ohlc_tick.close(), fast_weight_); 76 | slow_ema_.AddNewValue(ohlc_tick.close(), slow_weight_); 77 | signal_ema_.AddNewValue(GetMACDSeries(), signal_weight_); 78 | } 79 | 80 | } // namespace trader 81 | -------------------------------------------------------------------------------- /indicators/moving_average_convergence_divergence.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_MOVING_AVERAGE_CONVERGENCE_DIVERGENCE_H 4 | #define INDICATORS_MOVING_AVERAGE_CONVERGENCE_DIVERGENCE_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | #include "indicators/util.h" 9 | 10 | namespace trader { 11 | 12 | // Calculates the Moving Average Convergence/Divergence (MACD) of the closing 13 | // prices over OHLC ticks with a specified period size (in seconds). 14 | // We assume that the period is divisible by the period of update OHLC ticks. 15 | // All Get methods run O(1) time. 16 | // Based on: https://en.wikipedia.org/wiki/MACD 17 | // and: https://www.investopedia.com/terms/m/macd.asp 18 | class MovingAverageConvergenceDivergence { 19 | public: 20 | // Constructor. 21 | // fast_length: Period for fast (short period) EMA (typically 12 days). 22 | // slow_length: Period for slow (long period) EMA (typically 26 days). 23 | // signal_smoothing: Period for signal (average) series (typically 9 days). 24 | // period_size_sec: Period of the OHLC ticks (in seconds). 25 | MovingAverageConvergenceDivergence(int fast_length, int slow_length, 26 | int signal_smoothing, int period_size_sec); 27 | virtual ~MovingAverageConvergenceDivergence() {} 28 | 29 | // Returns the fast Exponential Moving Average (over closing prices). 30 | virtual float GetFastExponentialMovingAverage() const; 31 | // Returns the slow Exponential Moving Average (over closing prices). 32 | virtual float GetSlowExponentialMovingAverage() const; 33 | // Returns the difference between "fast" (short period) and "slow" (longer 34 | // period) Exponential Moving Average (over closing prices). 35 | virtual float GetMACDSeries() const; 36 | // Returns the Exponential Moving Average of the MACD Series itself (with 37 | // signal smoothing period). 38 | virtual float GetMACDSignal() const; 39 | // Returns the difference between the MACD Series and the MACD Signal. 40 | virtual float GetDivergence() const; 41 | 42 | // Returns the number of seen OHLC ticks. 43 | virtual int GetNumOhlcTicks() const; 44 | 45 | // Updates the Moving Average Convergence/Divergence (MACD). 46 | // This method has the same time complexity as the LastNOhlcTicks::Update 47 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 48 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 49 | virtual void Update(const OhlcTick& ohlc_tick); 50 | 51 | private: 52 | // Auxiliary method called when the last OHLC tick was updated. 53 | void LastTickUpdated(const OhlcTick& ohlc_tick); 54 | // Auxiliary method called when a new OHLC tick was added. 55 | void NewTickAdded(const OhlcTick& ohlc_tick); 56 | 57 | // Keeps track of the current OHLC tick. 58 | LastNOhlcTicks last_n_ohlc_ticks_; 59 | 60 | // Number of observed OHLC ticks. 61 | int num_ohlc_ticks_ = 0; 62 | 63 | // Weight for the Fast Exponential Moving Average. 64 | float fast_weight_; 65 | // Weight for the Slow Exponential Moving Average. 66 | float slow_weight_; 67 | // Weight for the Exponential Moving Average of the MACD Series. 68 | float signal_weight_; 69 | 70 | // Fast Exponential Moving Average. 71 | ExponentialMovingAverageHelper fast_ema_; 72 | // Slow Exponential Moving Average. 73 | ExponentialMovingAverageHelper slow_ema_; 74 | // Exponential Moving Average of the MACD Series. 75 | ExponentialMovingAverageHelper signal_ema_; 76 | }; 77 | 78 | } // namespace trader 79 | 80 | #endif // INDICATORS_MOVING_AVERAGE_CONVERGENCE_DIVERGENCE_H 81 | -------------------------------------------------------------------------------- /indicators/relative_strength_index.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/relative_strength_index.h" 4 | 5 | namespace trader { 6 | 7 | RelativeStrengthIndex::RelativeStrengthIndex(int num_periods, 8 | int period_size_sec) 9 | : num_periods_(num_periods), 10 | last_n_ohlc_ticks_(/*num_ohlc_ticks=*/2, period_size_sec) { 11 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 12 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 13 | // We have observed at least 1 OHLC tick. 14 | // The most recent OHLC tick was updated. 15 | assert(num_ohlc_ticks_ >= 1); 16 | LastTickUpdated(); 17 | }); 18 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 19 | [this](const OhlcTick& new_ohlc_tick) { 20 | // This is the first or second observed OHLC tick. 21 | assert(num_ohlc_ticks_ <= 1); 22 | NewTickAdded(); 23 | }); 24 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 25 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 26 | // We have observed at least 2 OHLC ticks. 27 | // New OHLC tick was added. 28 | assert(num_ohlc_ticks_ >= 2); 29 | NewTickAdded(); 30 | }); 31 | } 32 | 33 | float RelativeStrengthIndex::GetUpwardChangeModifiedMovingAverage() const { 34 | return upward_change_mma_.GetExponentialMovingAverage(); 35 | } 36 | 37 | float RelativeStrengthIndex::GetDownwardChangeModifiedMovingAverage() const { 38 | return downward_change_mma_.GetExponentialMovingAverage(); 39 | } 40 | 41 | float RelativeStrengthIndex::GetRelativeStrengthIndex() const { 42 | const float upward_change_mma = 43 | upward_change_mma_.GetExponentialMovingAverage(); 44 | const float downward_change_mma = 45 | downward_change_mma_.GetExponentialMovingAverage(); 46 | if (upward_change_mma < 1.0e-6f && downward_change_mma < 1.0e-6f) { 47 | return 50.0f; 48 | } 49 | if (downward_change_mma < upward_change_mma * 1.0e-6f) { 50 | return 100.0f; 51 | } 52 | return 100.0f - 100.0f / (1.0f + upward_change_mma / downward_change_mma); 53 | } 54 | 55 | int RelativeStrengthIndex::GetNumOhlcTicks() const { return num_ohlc_ticks_; } 56 | 57 | void RelativeStrengthIndex::Update(const OhlcTick& ohlc_tick) { 58 | last_n_ohlc_ticks_.Update(ohlc_tick); 59 | } 60 | 61 | void RelativeStrengthIndex::LastTickUpdated() { 62 | const float weight = GetModifiedMovingAverageWeight(); 63 | const auto change = GetUpwardDownwardChange(); 64 | upward_change_mma_.UpdateCurrentValue(change.first, weight); 65 | downward_change_mma_.UpdateCurrentValue(change.second, weight); 66 | } 67 | 68 | void RelativeStrengthIndex::NewTickAdded() { 69 | ++num_ohlc_ticks_; 70 | const float weight = GetModifiedMovingAverageWeight(); 71 | const auto change = GetUpwardDownwardChange(); 72 | upward_change_mma_.AddNewValue(change.first, weight); 73 | downward_change_mma_.AddNewValue(change.second, weight); 74 | } 75 | 76 | float RelativeStrengthIndex::GetModifiedMovingAverageWeight() const { 77 | return 1.0f / std::min(num_ohlc_ticks_, num_periods_); 78 | } 79 | 80 | std::pair RelativeStrengthIndex::GetUpwardDownwardChange() const { 81 | float upward_change = 0; 82 | float downward_change = 0; 83 | const std::deque& last_n_ohlc_ticks = 84 | last_n_ohlc_ticks_.GetLastNOhlcTicks(); 85 | if (last_n_ohlc_ticks.size() == 1) { 86 | // We do not have a previous OHLC tick, so we use the opening and closing 87 | // price of the current OHLC tick. 88 | const OhlcTick& ohlc_tick = last_n_ohlc_ticks.at(0); 89 | if (ohlc_tick.close() >= ohlc_tick.open()) { 90 | upward_change = ohlc_tick.close() - ohlc_tick.open(); 91 | } else { 92 | downward_change = ohlc_tick.open() - ohlc_tick.close(); 93 | } 94 | } else { 95 | assert(last_n_ohlc_ticks.size() == 2); 96 | // We do have a previous OHLC tick, so we use the closing price of both 97 | // the previous and the current OHLC tick. 98 | const OhlcTick& prev_ohlc_tick = last_n_ohlc_ticks.at(0); 99 | const OhlcTick& ohlc_tick = last_n_ohlc_ticks.at(1); 100 | if (ohlc_tick.close() >= prev_ohlc_tick.close()) { 101 | upward_change = ohlc_tick.close() - prev_ohlc_tick.close(); 102 | } else { 103 | downward_change = prev_ohlc_tick.close() - ohlc_tick.close(); 104 | } 105 | } 106 | return {upward_change, downward_change}; 107 | } 108 | 109 | } // namespace trader 110 | -------------------------------------------------------------------------------- /indicators/relative_strength_index.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_RELATIVE_STRENGTH_INDEX_H 4 | #define INDICATORS_RELATIVE_STRENGTH_INDEX_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | #include "indicators/util.h" 9 | 10 | namespace trader { 11 | 12 | // Calculates the Relative Strength Index (RSI) of the closing prices over 13 | // all (previous) OHLC ticks with a specified period size (in seconds). 14 | // We assume that the period is divisible by the period of update OHLC ticks. 15 | // Upward change U and downward change D are calculated using an N-period 16 | // smoothed or modified moving average (SMMA or MMA). 17 | // Typically one uses N = 14 day periods. 18 | // Based on: https://en.wikipedia.org/wiki/Relative_strength_index 19 | // and: https://www.investopedia.com/terms/r/rsi.asp 20 | class RelativeStrengthIndex { 21 | public: 22 | // Constructor. 23 | // num_periods: Number N of periods over which we want to compute the RSI. 24 | // period_size_sec: Period of the OHLC ticks (in seconds). 25 | RelativeStrengthIndex(int num_periods, int period_size_sec); 26 | virtual ~RelativeStrengthIndex() {} 27 | 28 | // Returns the smoothed or modified moving average for the upward change U. 29 | virtual float GetUpwardChangeModifiedMovingAverage() const; 30 | // Returns the smoothed or modified moving average for the downward change D. 31 | virtual float GetDownwardChangeModifiedMovingAverage() const; 32 | 33 | // Returns Relative Strength Index (of closing prices) over all (previous) 34 | // OHLC ticks. This method runs in O(1) time. 35 | virtual float GetRelativeStrengthIndex() const; 36 | 37 | // Returns the number of seen OHLC ticks. This method runs in O(1) time. 38 | virtual int GetNumOhlcTicks() const; 39 | 40 | // Updates the Relative Strength Index. 41 | // This method has the same time complexity as the LastNOhlcTicks::Update 42 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 43 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 44 | virtual void Update(const OhlcTick& ohlc_tick); 45 | 46 | private: 47 | // Auxiliary method called when the last OHLC tick was updated. 48 | void LastTickUpdated(); 49 | // Auxiliary method called when a new OHLC tick was added. 50 | void NewTickAdded(); 51 | // Computes the weight for the smoothed or modified moving average. 52 | float GetModifiedMovingAverageWeight() const; 53 | // Computes the most recent upward change U and downward change D. 54 | std::pair GetUpwardDownwardChange() const; 55 | 56 | // Number N of periods over which we want to compute the RSI. 57 | int num_periods_ = 0; 58 | // Keeps track of the current and the previous OHLC tick. 59 | LastNOhlcTicks last_n_ohlc_ticks_; 60 | 61 | // Number of observed OHLC ticks. 62 | int num_ohlc_ticks_ = 0; 63 | // Smoothed or modified moving average (MMA) for the upward change U. 64 | ExponentialMovingAverageHelper upward_change_mma_; 65 | // Smoothed or modified moving average (MMA) for the downward change D. 66 | ExponentialMovingAverageHelper downward_change_mma_; 67 | }; 68 | 69 | } // namespace trader 70 | 71 | #endif // INDICATORS_RELATIVE_STRENGTH_INDEX_H 72 | -------------------------------------------------------------------------------- /indicators/simple_moving_average.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/simple_moving_average.h" 4 | 5 | namespace trader { 6 | 7 | SimpleMovingAverage::SimpleMovingAverage(int num_ohlc_ticks, 8 | int period_size_sec) 9 | : last_n_ohlc_ticks_(num_ohlc_ticks, period_size_sec) { 10 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 11 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 12 | sum_close_price_ += new_ohlc_tick.close() - old_ohlc_tick.close(); 13 | }); 14 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 15 | [this](const OhlcTick& new_ohlc_tick) { 16 | sum_close_price_ += new_ohlc_tick.close(); 17 | }); 18 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 19 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 20 | sum_close_price_ += new_ohlc_tick.close() - removed_ohlc_tick.close(); 21 | }); 22 | } 23 | 24 | float SimpleMovingAverage::GetSimpleMovingAverage() const { 25 | const int num_ohlc_ticks = GetNumOhlcTicks(); 26 | if (num_ohlc_ticks == 0) { 27 | return 0; 28 | } 29 | return sum_close_price_ / num_ohlc_ticks; 30 | } 31 | 32 | int SimpleMovingAverage::GetNumOhlcTicks() const { 33 | return last_n_ohlc_ticks_.GetLastNOhlcTicks().size(); 34 | } 35 | 36 | void SimpleMovingAverage::Update(const OhlcTick& ohlc_tick) { 37 | last_n_ohlc_ticks_.Update(ohlc_tick); 38 | } 39 | 40 | } // namespace trader 41 | -------------------------------------------------------------------------------- /indicators/simple_moving_average.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_SIMPLE_MOVING_AVERAGE_H 4 | #define INDICATORS_SIMPLE_MOVING_AVERAGE_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | 9 | namespace trader { 10 | 11 | // Calculates the Simple Moving Average (SMA) of the closing prices over 12 | // the last N OHLC ticks with a specified period size (in seconds). 13 | // We assume that the period is divisible by the period of update OHLC ticks. 14 | // Based on: https://www.investopedia.com/terms/m/movingaverage.asp 15 | // and: https://www.investopedia.com/terms/s/sma.asp 16 | class SimpleMovingAverage { 17 | public: 18 | // Constructor. 19 | // num_ohlc_ticks: Number N of OHLC ticks over which we want to compute SMA. 20 | // period_size_sec: Period of the OHLC ticks (in seconds). 21 | SimpleMovingAverage(int num_ohlc_ticks, int period_size_sec); 22 | virtual ~SimpleMovingAverage() {} 23 | 24 | // Returns Simple Moving Average (of closing prices) over the last 25 | // (at most) N OHLC ticks (in the deque). This method runs in O(1) time. 26 | virtual float GetSimpleMovingAverage() const; 27 | 28 | // Returns the number of OHLC ticks (in the deque) over which the Simple 29 | // Moving Average is computed. This method runs in O(1) time. 30 | virtual int GetNumOhlcTicks() const; 31 | 32 | // Updates the Simple Moving Average (and the corresponding deque). 33 | // This method has the same time complexity as the LastNOhlcTicks::Update 34 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 35 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 36 | virtual void Update(const OhlcTick& ohlc_tick); 37 | 38 | private: 39 | // Keeps track of the last N OHLC ticks. 40 | LastNOhlcTicks last_n_ohlc_ticks_; 41 | 42 | // Sum of the closing prices over the last N OHLC ticks (in the deque). 43 | float sum_close_price_ = 0; 44 | }; 45 | 46 | } // namespace trader 47 | 48 | #endif // INDICATORS_SIMPLE_MOVING_AVERAGE_H 49 | -------------------------------------------------------------------------------- /indicators/simple_moving_average_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/simple_moving_average.h" 4 | 5 | #include "gtest/gtest.h" 6 | #include "indicators/test_util.h" 7 | 8 | namespace trader { 9 | using ::trader::testing::PrepareExampleOhlcHistory; 10 | 11 | TEST(SimpleMovingAverageTest, Get3SMAWhenAdding8HourOhlcTicks) { 12 | OhlcHistory ohlc_history; 13 | PrepareExampleOhlcHistory(ohlc_history); 14 | 15 | SimpleMovingAverage simple_moving_average(/*num_ohlc_ticks=*/3, 16 | /*period_size_sec=*/kSecondsPerDay); 17 | 18 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 0); 19 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 0); 20 | 21 | // O: 100 H: 150 L: 80 C: 120 V: 1000 T: 2017-01-01 00:00 22 | // --- Daily History --- 23 | // O: 100 H: 150 L: 80 C: 120 V: 1000 T: 2017-01-01 (Day 1) 24 | simple_moving_average.Update(ohlc_history[0]); 25 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 120); 26 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 1); 27 | 28 | // O: 120 H: 180 L: 100 C: 150 V: 1000 T: 2017-01-01 08:00 29 | // --- Daily History --- 30 | // O: 100 H: 180 L: 80 C: 150 V: 2000 T: 2017-01-01 (Day 1) 31 | simple_moving_average.Update(ohlc_history[1]); 32 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 150); 33 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 1); 34 | 35 | // O: 150 H: 250 L: 100 C: 140 V: 1000 T: 2017-01-01 16:00 36 | // --- Daily History --- 37 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 38 | simple_moving_average.Update(ohlc_history[2]); 39 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 140); 40 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 1); 41 | 42 | // O: 140 H: 150 L: 80 C: 100 V: 1000 T: 2017-01-02 00:00 (+1 Day) 43 | // --- Daily History --- 44 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 45 | // O: 140 H: 150 L: 80 C: 100 V: 1000 T: 2017-01-02 (Day 2) 46 | simple_moving_average.Update(ohlc_history[3]); 47 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 48 | (140.0f + 100.0f) / 2.0f); 49 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 2); 50 | 51 | // O: 100 H: 120 L: 20 C: 50 V: 1000 T: 2017-01-02 08:00 52 | // --- Daily History --- 53 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 54 | // O: 140 H: 150 L: 20 C: 50 V: 2000 T: 2017-01-02 (Day 2) 55 | simple_moving_average.Update(ohlc_history[4]); 56 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 57 | (140.0f + 50.0f) / 2.0f); 58 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 2); 59 | 60 | // O: 50 H: 100 L: 40 C: 80 V: 1000 T: 2017-01-02 16:00 61 | // --- Daily History --- 62 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 63 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 64 | simple_moving_average.Update(ohlc_history[5]); 65 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 66 | (140.0f + 80.0f) / 2.0f); 67 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 2); 68 | 69 | // O: 80 H: 180 L: 50 C: 150 V: 1000 T: 2017-01-03 00:00 (+1 Day) 70 | // --- Daily History --- 71 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 72 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 73 | // O: 80 H: 180 L: 50 C: 150 V: 1000 T: 2017-01-03 (Day 3) 74 | simple_moving_average.Update(ohlc_history[6]); 75 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 76 | (140.0f + 80.0f + 150.0f) / 3.0f); 77 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 78 | 79 | // O: 150 H: 250 L: 120 C: 240 V: 1000 T: 2017-01-03 08:00 80 | // --- Daily History --- 81 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 82 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 83 | // O: 80 H: 250 L: 50 C: 240 V: 2000 T: 2017-01-03 (Day 3) 84 | simple_moving_average.Update(ohlc_history[7]); 85 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 86 | (140.0f + 80.0f + 240.0f) / 3.0f); 87 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 88 | 89 | // O: 240 H: 450 L: 220 C: 400 V: 1000 T: 2017-01-03 16:00 90 | // --- Daily History --- 91 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 92 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 93 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 94 | simple_moving_average.Update(ohlc_history[8]); 95 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 96 | (140.0f + 80.0f + 400.0f) / 3.0f); 97 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 98 | 99 | // O: 400 H: 450 L: 250 C: 300 V: 1000 T: 2017-01-04 00:00 (+1 Day) 100 | // --- Daily History --- 101 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 102 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 103 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 104 | // O: 400 H: 450 L: 250 C: 300 V: 1000 T: 2017-01-04 (Day 4) 105 | simple_moving_average.Update(ohlc_history[9]); 106 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 107 | (80.0f + 400.0f + 300.0f) / 3.0f); 108 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 109 | 110 | // O: 300 H: 700 L: 220 C: 650 V: 1000 T: 2017-01-04 08:00 111 | // --- Daily History --- 112 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 113 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 114 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 115 | // O: 400 H: 700 L: 220 C: 650 V: 2000 T: 2017-01-04 (Day 4) 116 | simple_moving_average.Update(ohlc_history[10]); 117 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 118 | (80.0f + 400.0f + 650.0f) / 3.0f); 119 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 120 | 121 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-04 16:00 122 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 00:00 (+1 Day) 123 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 08:00 124 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 16:00 125 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-06 00:00 (+1 Day) 126 | // O: 650 H: 800 L: 600 C: 750 V: 1000 T: 2017-01-06 08:00 127 | // --- Daily History --- 128 | // O: 100 H: 250 L: 80 C: 140 V: 3000 T: 2017-01-01 (Day 1) 129 | // O: 140 H: 150 L: 20 C: 80 V: 3000 T: 2017-01-02 (Day 2) 130 | // O: 80 H: 450 L: 50 C: 400 V: 3000 T: 2017-01-03 (Day 3) 131 | // O: 400 H: 700 L: 220 C: 650 V: 2000 T: 2017-01-04 (Day 4) 132 | // O: 650 H: 650 L: 650 C: 650 V: 0 T: 2017-01-05 (Day 5) 133 | // O: 650 H: 800 L: 600 C: 750 V: 1000 T: 2017-01-06 (Day 6) 134 | simple_moving_average.Update(ohlc_history[11]); 135 | EXPECT_FLOAT_EQ(simple_moving_average.GetSimpleMovingAverage(), 136 | (650.0f + 650.0f + 750.0f) / 3.0f); 137 | EXPECT_EQ(simple_moving_average.GetNumOhlcTicks(), 3); 138 | } 139 | 140 | } // namespace trader 141 | -------------------------------------------------------------------------------- /indicators/stochastic_oscillator.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/stochastic_oscillator.h" 4 | 5 | namespace trader { 6 | 7 | StochasticOscillator::StochasticOscillator(int num_periods, int period_size_sec) 8 | : last_n_ohlc_ticks_(/*num_ohlc_ticks=*/1, period_size_sec), 9 | sliding_window_min_(/*window_size=*/num_periods), 10 | sliding_window_max_(/*window_size=*/num_periods), 11 | d_fast_(/*window_size=*/3), 12 | d_slow_(/*window_size=*/3) { 13 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 14 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 15 | // We have observed at least 1 OHLC tick. 16 | // The most recent OHLC tick was updated. 17 | assert(num_ohlc_ticks_ >= 1); 18 | LastTickUpdated(new_ohlc_tick); 19 | }); 20 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 21 | [this](const OhlcTick& new_ohlc_tick) { 22 | // This is the very first observed OHLC tick. 23 | assert(num_ohlc_ticks_ == 0); 24 | NewTickAdded(new_ohlc_tick); 25 | }); 26 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 27 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 28 | // We have observed at least 1 OHLC tick. 29 | // New OHLC tick was added. 30 | assert(num_ohlc_ticks_ >= 1); 31 | NewTickAdded(new_ohlc_tick); 32 | }); 33 | } 34 | 35 | float StochasticOscillator::GetLow() const { 36 | return sliding_window_min_.GetSlidingWindowMinimum(); 37 | } 38 | 39 | float StochasticOscillator::GetHigh() const { 40 | return sliding_window_max_.GetSlidingWindowMaximum(); 41 | } 42 | 43 | float StochasticOscillator::GetK() const { return latest_k_; } 44 | 45 | float StochasticOscillator::GetFastD() const { return d_fast_.GetMean(); } 46 | 47 | float StochasticOscillator::GetSlowD() const { return d_slow_.GetMean(); } 48 | 49 | int StochasticOscillator::GetNumOhlcTicks() const { return num_ohlc_ticks_; } 50 | 51 | void StochasticOscillator::Update(const OhlcTick& ohlc_tick) { 52 | last_n_ohlc_ticks_.Update(ohlc_tick); 53 | } 54 | 55 | void StochasticOscillator::LastTickUpdated(const OhlcTick& ohlc_tick) { 56 | sliding_window_min_.UpdateCurrentValue(ohlc_tick.low()); 57 | sliding_window_max_.UpdateCurrentValue(ohlc_tick.high()); 58 | UpdateK(ohlc_tick.close()); 59 | d_fast_.UpdateCurrentValue(GetK()); 60 | d_slow_.UpdateCurrentValue(GetFastD()); 61 | } 62 | 63 | void StochasticOscillator::NewTickAdded(const OhlcTick& ohlc_tick) { 64 | ++num_ohlc_ticks_; 65 | sliding_window_min_.AddNewValue(ohlc_tick.low()); 66 | sliding_window_max_.AddNewValue(ohlc_tick.high()); 67 | UpdateK(ohlc_tick.close()); 68 | d_fast_.AddNewValue(GetK()); 69 | d_slow_.AddNewValue(GetFastD()); 70 | } 71 | 72 | void StochasticOscillator::UpdateK(const float latest_price) { 73 | assert(num_ohlc_ticks_ >= 1); 74 | const float price_min = GetLow(); 75 | const float price_max = GetHigh(); 76 | const float price_span = price_max - price_min; 77 | if (price_span < 1.0e-6f) { 78 | latest_k_ = 50; 79 | return; 80 | } 81 | latest_k_ = 100.0f * (latest_price - price_min) / price_span; 82 | } 83 | 84 | } // namespace trader 85 | -------------------------------------------------------------------------------- /indicators/stochastic_oscillator.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_STOCHASTIC_OSCILLATOR_H 4 | #define INDICATORS_STOCHASTIC_OSCILLATOR_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | #include "indicators/util.h" 9 | 10 | namespace trader { 11 | 12 | // Calculates the Stochastic Oscillator (SO) based on the closing prices over 13 | // the last N OHLC ticks with a specified period size (in seconds). 14 | // Typical values for N are 5, 9, or 14 periods. 15 | // We assume that the period is divisible by the period of update OHLC ticks. 16 | // All Get methods run O(1) time. 17 | // Based on: https://en.wikipedia.org/wiki/Stochastic_oscillator 18 | // and: https://www.investopedia.com/terms/s/stochasticoscillator.asp 19 | class StochasticOscillator { 20 | public: 21 | // Constructor. 22 | // num_periods: Number N of periods over which we want to compute the SO. 23 | // period_size_sec: Period of the OHLC ticks (in seconds). 24 | StochasticOscillator(int num_periods, int period_size_sec); 25 | virtual ~StochasticOscillator() {} 26 | 27 | // Returns the lowest price over the last N OHLC ticks. 28 | virtual float GetLow() const; 29 | 30 | // Returns the highest price over the last N OHLC ticks. 31 | virtual float GetHigh() const; 32 | 33 | // Returns %K := 100 * (Price - Low_N) / (High_N - Low_N). 34 | virtual float GetK() const; 35 | 36 | // Returns %D-Fast := 3-period simple moving average of %K. 37 | virtual float GetFastD() const; 38 | 39 | // Returns %D-Slow := 3-period simple moving average of %D-Fast. 40 | virtual float GetSlowD() const; 41 | 42 | // Returns the number of seen OHLC ticks. 43 | virtual int GetNumOhlcTicks() const; 44 | 45 | // Updates the Stochastic Oscillator. 46 | // This method has the same time complexity as the LastNOhlcTicks::Update 47 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 48 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 49 | virtual void Update(const OhlcTick& ohlc_tick); 50 | 51 | private: 52 | // Auxiliary method called when the last OHLC tick was updated. 53 | void LastTickUpdated(const OhlcTick& ohlc_tick); 54 | // Auxiliary method called when a new OHLC tick was added. 55 | void NewTickAdded(const OhlcTick& ohlc_tick); 56 | // Updates the most recent %K based on the latest price. 57 | void UpdateK(const float latest_price); 58 | 59 | // Keeps track of the current OHLC tick. 60 | LastNOhlcTicks last_n_ohlc_ticks_; 61 | 62 | // Number of observed OHLC ticks. 63 | int num_ohlc_ticks_ = 0; 64 | // Most recent %K := 100 * (Price - Low_N) / (High_N - Low_N). 65 | float latest_k_ = 0; 66 | 67 | SlidingWindowMinimum sliding_window_min_; 68 | SlidingWindowMaximum sliding_window_max_; 69 | SlidingWindowMeanAndVariance d_fast_; 70 | SlidingWindowMeanAndVariance d_slow_; 71 | }; 72 | 73 | } // namespace trader 74 | 75 | #endif // INDICATORS_STOCHASTIC_OSCILLATOR_H 76 | -------------------------------------------------------------------------------- /indicators/test_util.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/test_util.h" 4 | 5 | namespace trader { 6 | namespace testing { 7 | namespace { 8 | static constexpr int kSecondsPer8Hours = 8 * 60 * 60; 9 | 10 | // Adds OHLC tick to the history. Does not set the timestamp_sec. 11 | void AddOhlcTick(float open, float high, float low, float close, 12 | OhlcHistory& ohlc_history) { 13 | ASSERT_LE(low, open); 14 | ASSERT_LE(low, high); 15 | ASSERT_LE(low, close); 16 | ASSERT_GE(high, open); 17 | ASSERT_GE(high, close); 18 | ohlc_history.emplace_back(); 19 | OhlcTick& ohlc_tick = ohlc_history.back(); 20 | ohlc_tick.set_open(open); 21 | ohlc_tick.set_high(high); 22 | ohlc_tick.set_low(low); 23 | ohlc_tick.set_close(close); 24 | ohlc_tick.set_volume(1000.0f); 25 | } 26 | 27 | // Adds OHLC tick to the history period_sec away from the last OHLC tick. 28 | void AddOhlcTickWithPeriod(float open, float high, float low, float close, 29 | int period_sec, OhlcHistory& ohlc_history) { 30 | int64_t timestamp_sec = 1483228800; // 2017-01-01 31 | if (!ohlc_history.empty()) { 32 | ASSERT_FLOAT_EQ(ohlc_history.back().close(), open); 33 | timestamp_sec = ohlc_history.back().timestamp_sec() + period_sec; 34 | } 35 | AddOhlcTick(open, high, low, close, ohlc_history); 36 | ohlc_history.back().set_timestamp_sec(timestamp_sec); 37 | } 38 | 39 | // Adds OHLC tick to the history 8h away from the last OHLC tick. 40 | void Add8HourOhlcTick(float open, float high, float low, float close, 41 | OhlcHistory& ohlc_history) { 42 | AddOhlcTickWithPeriod(open, high, low, close, kSecondsPer8Hours, 43 | ohlc_history); 44 | } 45 | } // namespace 46 | 47 | void PrepareExampleOhlcHistory(OhlcHistory& ohlc_history) { 48 | Add8HourOhlcTick(100, 150, 80, 120, ohlc_history); // 2017-01-01 49 | Add8HourOhlcTick(120, 180, 100, 150, ohlc_history); 50 | Add8HourOhlcTick(150, 250, 100, 140, ohlc_history); 51 | Add8HourOhlcTick(140, 150, 80, 100, ohlc_history); // 2017-01-02 52 | Add8HourOhlcTick(100, 120, 20, 50, ohlc_history); 53 | Add8HourOhlcTick(50, 100, 40, 80, ohlc_history); 54 | Add8HourOhlcTick(80, 180, 50, 150, ohlc_history); // 2017-01-03 55 | Add8HourOhlcTick(150, 250, 120, 240, ohlc_history); 56 | Add8HourOhlcTick(240, 450, 220, 400, ohlc_history); 57 | Add8HourOhlcTick(400, 450, 250, 300, ohlc_history); // 2017-01-04 58 | Add8HourOhlcTick(300, 700, 220, 650, ohlc_history); 59 | Add8HourOhlcTick(650, 800, 600, 750, ohlc_history); // 2017-01-06 08:00 60 | ohlc_history.back().set_timestamp_sec(ohlc_history.back().timestamp_sec() + 61 | 1 * kSecondsPerDay + 62 | 2 * kSecondsPer8Hours); 63 | } 64 | 65 | } // namespace testing 66 | } // namespace trader 67 | -------------------------------------------------------------------------------- /indicators/test_util.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_TEST_UTIL_H 4 | #define INDICATORS_TEST_UTIL_H 5 | 6 | #include "base/base.h" 7 | #include "gtest/gtest.h" 8 | 9 | namespace trader { 10 | namespace testing { 11 | 12 | // Prepares the following OHLC history: 13 | // O: 100 H: 150 L: 80 C: 120 V: 1000 T: 2017-01-01 00:00 14 | // O: 120 H: 180 L: 100 C: 150 V: 1000 T: 2017-01-01 08:00 15 | // O: 150 H: 250 L: 100 C: 140 V: 1000 T: 2017-01-01 16:00 16 | // O: 140 H: 150 L: 80 C: 100 V: 1000 T: 2017-01-02 00:00 (+1 Day) 17 | // O: 100 H: 120 L: 20 C: 50 V: 1000 T: 2017-01-02 08:00 18 | // O: 50 H: 100 L: 40 C: 80 V: 1000 T: 2017-01-02 16:00 19 | // O: 80 H: 180 L: 50 C: 150 V: 1000 T: 2017-01-03 00:00 (+1 Day) 20 | // O: 150 H: 250 L: 120 C: 240 V: 1000 T: 2017-01-03 08:00 21 | // O: 240 H: 450 L: 220 C: 400 V: 1000 T: 2017-01-03 16:00 22 | // O: 400 H: 450 L: 250 C: 300 V: 1000 T: 2017-01-04 00:00 (+1 Day) 23 | // O: 300 H: 700 L: 220 C: 650 V: 1000 T: 2017-01-04 08:00 24 | // --- Gap -- 25 | // O: 650 H: 800 L: 600 C: 750 V: 1000 T: 2017-01-06 08:00 26 | void PrepareExampleOhlcHistory(OhlcHistory& ohlc_history); 27 | 28 | } // namespace testing 29 | } // namespace trader 30 | 31 | #endif // INDICATORS_TEST_UTIL_H 32 | -------------------------------------------------------------------------------- /indicators/util.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/util.h" 4 | 5 | namespace trader { 6 | 7 | float SlidingWindowMeanAndVariance::GetMean() const { 8 | if (num_values_ == 0) { 9 | return 0; 10 | } 11 | return (current_value_ + window_sum_) / GetWindowSize(); 12 | } 13 | 14 | float SlidingWindowMeanAndVariance::GetVariance() const { 15 | if (num_values_ == 0) { 16 | return 0; 17 | } 18 | const int window_size = GetWindowSize(); 19 | const float mean = (current_value_ + window_sum_) / window_size; 20 | return (std::pow(current_value_, 2) + window_sum_2_) / window_size - 21 | std::pow(mean, 2); 22 | } 23 | 24 | float SlidingWindowMeanAndVariance::GetStandardDeviation() const { 25 | return std::sqrt(GetVariance()); 26 | } 27 | 28 | int SlidingWindowMeanAndVariance::GetWindowSize() const { 29 | if (window_size_ == 0) { 30 | return num_values_; 31 | } 32 | return std::min(num_values_, window_size_); 33 | } 34 | 35 | void SlidingWindowMeanAndVariance::AddNewValue(float value) { 36 | if (num_values_ == 0 || window_size_ <= 1) { 37 | if (window_size_ == 0) { 38 | window_sum_ += current_value_; 39 | window_sum_2_ += std::pow(current_value_, 2); 40 | } 41 | current_value_ = value; 42 | ++num_values_; 43 | return; 44 | } 45 | window_.push_back(current_value_); 46 | window_sum_ += current_value_; 47 | window_sum_2_ += std::pow(current_value_, 2); 48 | current_value_ = value; 49 | ++num_values_; 50 | if (window_.size() >= window_size_) { 51 | const float popped_value = window_.front(); 52 | window_sum_ -= popped_value; 53 | window_sum_2_ -= std::pow(popped_value, 2); 54 | window_.pop_front(); 55 | } 56 | } 57 | 58 | void SlidingWindowMeanAndVariance::UpdateCurrentValue(float value) { 59 | assert(num_values_ >= 1); 60 | current_value_ = value; 61 | } 62 | 63 | void ExponentialMovingAverageHelper::AddNewValue(float value, float weight) { 64 | previous_ema_ = current_ema_; 65 | ++num_values_; 66 | UpdateCurrentValue(value, weight); 67 | } 68 | 69 | void ExponentialMovingAverageHelper::UpdateCurrentValue(float value, 70 | float weight) { 71 | assert(num_values_ > 0); 72 | if (num_values_ == 1) { 73 | current_ema_ = value; 74 | } else { 75 | current_ema_ = weight * value + (1.0f - weight) * previous_ema_; 76 | } 77 | } 78 | 79 | void SlidingWindowMinimum::AddNewValue(float value) { 80 | if (num_values_ == 0 || window_size_ == 1) { 81 | current_value_ = value; 82 | current_sliding_window_minimum_ = value; 83 | ++num_values_; 84 | return; 85 | } 86 | // First we push the current_value_ to the window_ deque. 87 | // We need to pop all values that are >= current_value_, since these 88 | // can no longer be minimum. This keeps the window_ deque sorted. 89 | while (!window_.empty() && window_.back().first >= current_value_) { 90 | window_.pop_back(); 91 | } 92 | window_.push_back({current_value_, num_values_}); 93 | // The window covers num_values_ - window_.front().second + 1 values. 94 | // We need this to be at most window_size_ - 1 (excluding the current value). 95 | while (num_values_ - window_.front().second + 1 >= window_size_) { 96 | window_.pop_front(); 97 | } 98 | ++num_values_; 99 | UpdateCurrentValue(value); 100 | } 101 | 102 | void SlidingWindowMinimum::UpdateCurrentValue(float value) { 103 | assert(num_values_ >= 1); 104 | current_value_ = value; 105 | current_sliding_window_minimum_ = 106 | window_.empty() ? current_value_ 107 | : std::min(window_.front().first, current_value_); 108 | } 109 | 110 | } // namespace trader 111 | -------------------------------------------------------------------------------- /indicators/util.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_UTIL_H 4 | #define INDICATORS_UTIL_H 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace trader { 13 | 14 | // Calculates sliding window mean and variance. All methods run in O(1) time. 15 | class SlidingWindowMeanAndVariance { 16 | public: 17 | // Constructor. 18 | // window_size: Size of the sliding window for the SMA. Ignored if zero. 19 | explicit SlidingWindowMeanAndVariance(int window_size) 20 | : window_size_(window_size) {} 21 | ~SlidingWindowMeanAndVariance() {} 22 | 23 | // Returns the mean of all values in the sliding window. 24 | float GetMean() const; 25 | 26 | // Returns the variance of all values in the sliding window. 27 | float GetVariance() const; 28 | 29 | // Returns the standard deviation of all values in the sliding window. 30 | float GetStandardDeviation() const; 31 | 32 | // Returns the current size of the sliding window. 33 | // Returns the total number of added values if the sliding window is ignored. 34 | int GetWindowSize() const; 35 | 36 | // Returns the total number of added values. 37 | int GetNumValues() const { return num_values_; } 38 | 39 | // Adds a new value. 40 | void AddNewValue(float value); 41 | 42 | // Updates the current (most recent) value. 43 | // Assumes that at least one value was added before. 44 | void UpdateCurrentValue(float value); 45 | 46 | private: 47 | // Size of the sliding window. 48 | int window_size_ = 0; 49 | // Current value. 50 | float current_value_ = 0; 51 | // Deque of values within the sliding window (excluding the current value). 52 | std::deque window_; 53 | // Sum over all elements in the sliding window (excluding the current value). 54 | float window_sum_ = 0; 55 | // Sum over all elements squared in the window (excluding the current value). 56 | float window_sum_2_ = 0; 57 | // Total number of added values (including the current value). 58 | int num_values_ = 0; 59 | }; 60 | 61 | // Calculates the Exponential Moving Average (EMA) over the provided values. 62 | // All methods run in O(1) time. 63 | class ExponentialMovingAverageHelper { 64 | public: 65 | ExponentialMovingAverageHelper() {} 66 | ~ExponentialMovingAverageHelper() {} 67 | 68 | // Returns the current Exponential Moving Average. 69 | float GetExponentialMovingAverage() const { return current_ema_; } 70 | 71 | // Returns the number of values considered in the Exponential Moving Average. 72 | int GetNumValues() const { return num_values_; } 73 | 74 | // Adds a new value. 75 | // weight: Weight of the new value in the Exponential Moving Average. 76 | void AddNewValue(float value, float weight); 77 | 78 | // Updates the current (most recent) value. 79 | // Assumes that at least one value was added before. 80 | // weight: Weight of the updated value in the Exponential Moving Average. 81 | void UpdateCurrentValue(float value, float weight); 82 | 83 | private: 84 | // Current Exponential Moving Average (including the current value). 85 | float current_ema_ = 0; 86 | // Previous Exponential Moving Average (excluding the current value). 87 | float previous_ema_ = 0; 88 | // Number of values considered in the Exponential Moving Average. 89 | int num_values_ = 0; 90 | }; 91 | 92 | // Calculates the sliding window minimum over the provided values. 93 | // All methods run in amortized O(1) time. 94 | class SlidingWindowMinimum { 95 | public: 96 | // Constructor. 97 | // window_size: Number of values in the sliding window. 98 | explicit SlidingWindowMinimum(int window_size) : window_size_(window_size) { 99 | assert(window_size > 0); 100 | } 101 | ~SlidingWindowMinimum() {} 102 | 103 | // Returns the current sliding window minimum. 104 | float GetSlidingWindowMinimum() const { 105 | return current_sliding_window_minimum_; 106 | } 107 | 108 | // Returns the total number of added values. 109 | int GetNumValues() const { return num_values_; } 110 | 111 | // Returns the current size of the sliding window. 112 | int GetWindowSize() const { return std::min(num_values_, window_size_); } 113 | 114 | // Adds a new value (and shifts the window by one if more than window_size 115 | // values were added). 116 | void AddNewValue(float value); 117 | 118 | // Updates the current (most recent) value. 119 | // Assumes that at least one value was added before. 120 | void UpdateCurrentValue(float value); 121 | 122 | private: 123 | // Size of the sliding window. 124 | int window_size_ = 0; 125 | // Current (most recently added / updated) value. 126 | float current_value_ = 0; 127 | // Current sliding window minimum (including the current value). 128 | float current_sliding_window_minimum_ = 0; 129 | // Deque of values within the sliding window (excluding the current value). 130 | std::deque> window_; 131 | // Total number of added values (including the current value). 132 | int num_values_ = 0; 133 | }; 134 | 135 | // Calculates the sliding window maximum over the provided values. 136 | // All methods run in amortized O(1) time. 137 | class SlidingWindowMaximum { 138 | public: 139 | // Constructor. 140 | // window_size: Number of values in the sliding window. 141 | explicit SlidingWindowMaximum(int window_size) 142 | : sliding_window_minimum_(window_size) {} 143 | ~SlidingWindowMaximum() {} 144 | 145 | // Returns the current sliding window maximum. 146 | float GetSlidingWindowMaximum() const { 147 | return -sliding_window_minimum_.GetSlidingWindowMinimum(); 148 | } 149 | 150 | // Returns the total number of added values. 151 | int GetNumValues() const { return sliding_window_minimum_.GetNumValues(); } 152 | 153 | // Returns the current size of the sliding window. 154 | int GetWindowSize() const { return sliding_window_minimum_.GetWindowSize(); } 155 | 156 | // Adds a new value (and shifts the window by one if more than window_size 157 | // values were added). 158 | void AddNewValue(float value) { sliding_window_minimum_.AddNewValue(-value); } 159 | 160 | // Updates the current (most recent) value. 161 | // Assumes that at least one value was added before. 162 | void UpdateCurrentValue(float value) { 163 | sliding_window_minimum_.UpdateCurrentValue(-value); 164 | } 165 | 166 | private: 167 | SlidingWindowMinimum sliding_window_minimum_; 168 | }; 169 | 170 | } // namespace trader 171 | 172 | #endif // INDICATORS_UTIL_H 173 | -------------------------------------------------------------------------------- /indicators/volatility.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "indicators/volatility.h" 4 | 5 | namespace trader { 6 | 7 | Volatility::Volatility(int window_size, int period_size_sec) 8 | : last_n_ohlc_ticks_(/*num_ohlc_ticks=*/2, period_size_sec), 9 | sliding_window_variance_(window_size) { 10 | last_n_ohlc_ticks_.RegisterLastTickUpdatedCallback( 11 | [this](const OhlcTick& old_ohlc_tick, const OhlcTick& new_ohlc_tick) { 12 | // We have observed at least 1 OHLC tick. 13 | // The most recent OHLC tick was updated. 14 | assert(num_ohlc_ticks_ >= 1); 15 | portfolio_value_.back() = latest_base_balance_ * new_ohlc_tick.close() + 16 | latest_quote_balance_; 17 | sliding_window_variance_.UpdateCurrentValue(GetLogarithmicReturn()); 18 | }); 19 | last_n_ohlc_ticks_.RegisterNewTickAddedCallback( 20 | [this](const OhlcTick& new_ohlc_tick) { 21 | // This is the first or second observed OHLC tick. 22 | assert(num_ohlc_ticks_ <= 1); 23 | if (num_ohlc_ticks_ == 0) { 24 | assert(portfolio_value_.empty()); 25 | portfolio_value_.push_back(latest_base_balance_ * 26 | new_ohlc_tick.open() + 27 | latest_quote_balance_); 28 | } else { 29 | portfolio_value_.pop_front(); 30 | } 31 | portfolio_value_.push_back(latest_base_balance_ * 32 | new_ohlc_tick.close() + 33 | latest_quote_balance_); 34 | ++num_ohlc_ticks_; 35 | sliding_window_variance_.AddNewValue(GetLogarithmicReturn()); 36 | }); 37 | last_n_ohlc_ticks_.RegisterNewTickAddedAndOldestTickRemovedCallback( 38 | [this](const OhlcTick& removed_ohlc_tick, const OhlcTick& new_ohlc_tick) { 39 | // We have observed at least 2 OHLC ticks. 40 | // New OHLC tick was added. 41 | assert(num_ohlc_ticks_ >= 2); 42 | portfolio_value_.pop_front(); 43 | portfolio_value_.push_back(latest_base_balance_ * 44 | new_ohlc_tick.close() + 45 | latest_quote_balance_); 46 | ++num_ohlc_ticks_; 47 | sliding_window_variance_.AddNewValue(GetLogarithmicReturn()); 48 | }); 49 | } 50 | 51 | float Volatility::GetVolatility() const { 52 | return sliding_window_variance_.GetStandardDeviation(); 53 | } 54 | 55 | int Volatility::GetNumOhlcTicks() const { return num_ohlc_ticks_; } 56 | 57 | void Volatility::Update(const OhlcTick& ohlc_tick, float base_balance, 58 | float quote_balance) { 59 | latest_base_balance_ = base_balance; 60 | latest_quote_balance_ = quote_balance; 61 | last_n_ohlc_ticks_.Update(ohlc_tick); 62 | } 63 | 64 | float Volatility::GetLogarithmicReturn() const { 65 | assert(portfolio_value_.size() == 2); 66 | return std::log(portfolio_value_.at(0) / portfolio_value_.at(1)); 67 | } 68 | 69 | } // namespace trader 70 | -------------------------------------------------------------------------------- /indicators/volatility.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef INDICATORS_VOLATILITY_H 4 | #define INDICATORS_VOLATILITY_H 5 | 6 | #include "base/base.h" 7 | #include "indicators/last_n_ohlc_ticks.h" 8 | #include "indicators/util.h" 9 | 10 | namespace trader { 11 | 12 | // Calculates the volatility of the portfolio, i.e. the standard deviation of 13 | // the (daily) logarithmic returns. 14 | // We assume that the period is divisible by the period of update OHLC ticks. 15 | // Based on: https://en.wikipedia.org/wiki/Volatility_(finance) 16 | class Volatility { 17 | public: 18 | // Constructor. 19 | // window_size: Size of the sliding window. Ignored if zero. 20 | // period_size_sec: Period of the OHLC ticks (in seconds). Typically daily. 21 | Volatility(int window_size, int period_size_sec); 22 | virtual ~Volatility() {} 23 | 24 | // Returns the standard deviation of the logarithmic returns. 25 | virtual float GetVolatility() const; 26 | 27 | // Returns the number of seen OHLC ticks. This method runs in O(1) time. 28 | virtual int GetNumOhlcTicks() const; 29 | 30 | // Updates volatility based on the latest OHLC tick and portfolio balances. 31 | // This method has the same time complexity as the LastNOhlcTicks::Update 32 | // method, i.e. O(1) when the given OHLC tick is near the last OHLC tick. 33 | // We assume that period_size_sec is divisible by the period of ohlc_tick. 34 | virtual void Update(const OhlcTick& ohlc_tick, float base_balance, 35 | float quote_balance); 36 | 37 | private: 38 | // Returns the logarithmic return w.r.t. the previous portfolio value. 39 | float GetLogarithmicReturn() const; 40 | 41 | // Latest portfolio balances. 42 | float latest_base_balance_ = 0; 43 | float latest_quote_balance_ = 0; 44 | // Keeps track of the current and the previous OHLC tick. 45 | LastNOhlcTicks last_n_ohlc_ticks_; 46 | // Keeps track of the current and the previous portfolio value. 47 | // At the beginning we use the opening price of the very first OHLC tick 48 | // to estimate the previous portfolio value. 49 | std::deque portfolio_value_; 50 | 51 | // Number of observed OHLC ticks. 52 | int num_ohlc_ticks_ = 0; 53 | 54 | // Sliding window for computing the variance of the logarithmic returns. 55 | SlidingWindowMeanAndVariance sliding_window_variance_; 56 | }; 57 | 58 | } // namespace trader 59 | 60 | #endif // INDICATORS_VOLATILITY_H 61 | -------------------------------------------------------------------------------- /logging/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = [ 2 | "//:__pkg__", 3 | "//eval:__pkg__", 4 | ]) 5 | 6 | load("@rules_proto//proto:defs.bzl", "proto_library") 7 | 8 | cc_library( 9 | name = "logger", 10 | hdrs = ["logger.h"], 11 | deps = [ 12 | "//base", 13 | "//base:account", 14 | "@com_google_absl//absl/strings", 15 | "@com_google_absl//absl/strings:str_format", 16 | ], 17 | ) 18 | 19 | cc_library( 20 | name = "csv_logger", 21 | srcs = ["csv_logger.cc"], 22 | hdrs = ["csv_logger.h"], 23 | deps = [":logger"], 24 | ) 25 | 26 | cc_test( 27 | name = "csv_logger_test", 28 | srcs = ["csv_logger_test.cc"], 29 | deps = [ 30 | ":csv_logger", 31 | "@com_google_googletest//:gtest_main", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /logging/csv_logger.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "logging/csv_logger.h" 4 | 5 | #include "absl/strings/str_format.h" 6 | 7 | namespace trader { 8 | namespace { 9 | // Returns a CSV representation of the given ohlc_tick. 10 | std::string OhlcTickToCsv(const OhlcTick& ohlc_tick) { 11 | return absl::StrFormat("%d,%.3f,%.3f,%.3f,%.3f,%.3f", // nowrap 12 | ohlc_tick.timestamp_sec(), // nowrap 13 | ohlc_tick.open(), // nowrap 14 | ohlc_tick.high(), // nowrap 15 | ohlc_tick.low(), // nowrap 16 | ohlc_tick.close(), // nowrap 17 | ohlc_tick.volume()); 18 | } 19 | 20 | // Returns a CSV representation of the given account. 21 | std::string AccountToCsv(const Account& account) { 22 | return absl::StrFormat("%.3f,%.3f,%.3f", // nowrap 23 | account.base_balance, // nowrap 24 | account.quote_balance, // nowrap 25 | account.total_fee); 26 | } 27 | 28 | // Returns a CSV representation of the given order. 29 | std::string OrderToCsv(const Order& order) { 30 | return absl::StrFormat( 31 | "%s,%s,%s,%s,%s", // nowrap 32 | Order::Type_Name(order.type()), // nowrap 33 | Order::Side_Name(order.side()), // nowrap 34 | order.oneof_amount_case() == Order::kBaseAmount 35 | ? absl::StrFormat("%.3f", order.base_amount()) 36 | : "", 37 | order.oneof_amount_case() == Order::kQuoteAmount 38 | ? absl::StrFormat("%.3f", order.quote_amount()) 39 | : "", 40 | order.price() > 0 ? absl::StrFormat("%.3f", order.price()) : ""); 41 | } 42 | 43 | // Returns a CSV representation of an empty order. 44 | std::string EmptyOrderToCsv() { return ",,,,"; } 45 | } // namespace 46 | 47 | void CsvLogger::LogExchangeState(const OhlcTick& ohlc_tick, 48 | const Account& account) { 49 | if (exchange_os_ != nullptr) { 50 | *exchange_os_ << absl::StrFormat("%s,%s,%s\n", OhlcTickToCsv(ohlc_tick), 51 | AccountToCsv(account), EmptyOrderToCsv()); 52 | } 53 | } 54 | 55 | void CsvLogger::LogExchangeState(const OhlcTick& ohlc_tick, 56 | const Account& account, const Order& order) { 57 | if (exchange_os_ != nullptr) { 58 | *exchange_os_ << absl::StrFormat("%s,%s,%s\n", OhlcTickToCsv(ohlc_tick), 59 | AccountToCsv(account), OrderToCsv(order)); 60 | } 61 | } 62 | 63 | void CsvLogger::LogTraderState(absl::string_view trader_state) { 64 | if (trader_os_ != nullptr) { 65 | *trader_os_ << trader_state << "\n"; 66 | } 67 | } 68 | 69 | } // namespace trader 70 | -------------------------------------------------------------------------------- /logging/csv_logger.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef LOGGING_CSV_LOGGER_H 4 | #define LOGGING_CSV_LOGGER_H 5 | 6 | #include "logging/logger.h" 7 | 8 | namespace trader { 9 | 10 | // CSV logger of exchange movements and trader internal state(s). 11 | class CsvLogger : public Logger { 12 | public: 13 | // Constructor. Does not take ownership of the provided output streams. 14 | // exchange_os: Output stream for exchange movements. Ignored if nullptr. 15 | // trader_os: Output stream for trader internal state(s). Ignored if nullptr. 16 | CsvLogger(std::ostream* exchange_os, std::ostream* trader_os) 17 | : exchange_os_(exchange_os), trader_os_(trader_os) {} 18 | virtual ~CsvLogger() {} 19 | 20 | // Logs the current ohlc_tick and account. 21 | void LogExchangeState(const OhlcTick& ohlc_tick, 22 | const Account& account) override; 23 | // Logs the current ohlc_tick, account, and order, after executing 24 | // the given order. 25 | void LogExchangeState(const OhlcTick& ohlc_tick, const Account& account, 26 | const Order& order) override; 27 | 28 | // Logs the trader state. 29 | void LogTraderState(absl::string_view trader_state) override; 30 | 31 | private: 32 | std::ostream* exchange_os_; 33 | std::ostream* trader_os_; 34 | }; 35 | 36 | } // namespace trader 37 | 38 | #endif // LOGGING_CSV_LOGGER_H 39 | -------------------------------------------------------------------------------- /logging/csv_logger_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "logging/csv_logger.h" 4 | 5 | #include "gtest/gtest.h" 6 | 7 | namespace trader { 8 | namespace { 9 | void AddOhlcTick(float open, float high, float low, float close, float volume, 10 | int timestamp_sec, OhlcHistory& ohlc_history) { 11 | ASSERT_LE(low, open); 12 | ASSERT_LE(low, high); 13 | ASSERT_LE(low, close); 14 | ASSERT_GE(high, open); 15 | ASSERT_GE(high, close); 16 | ohlc_history.emplace_back(); 17 | OhlcTick& ohlc_tick = ohlc_history.back(); 18 | ohlc_tick.set_open(open); 19 | ohlc_tick.set_high(high); 20 | ohlc_tick.set_low(low); 21 | ohlc_tick.set_close(close); 22 | ohlc_tick.set_volume(volume); 23 | ohlc_tick.set_timestamp_sec(timestamp_sec); 24 | } 25 | 26 | void PrepareExampleOhlcHistory(OhlcHistory& ohlc_history) { 27 | AddOhlcTick(100, 150, 80, 120, 1000, 1483228800, ohlc_history); // 2017-01-01 28 | AddOhlcTick(120, 180, 100, 150, 500, 1483315200, ohlc_history); // 2017-01-02 29 | AddOhlcTick(150, 250, 100, 140, 800, 1483401600, ohlc_history); // 2017-01-03 30 | } 31 | 32 | void PrepareExampleAccount(Account& account) { 33 | account.base_balance = 2.0f; 34 | account.quote_balance = 1000.0f; 35 | account.total_fee = 50.0f; 36 | account.base_unit = 0.0001f; 37 | account.quote_unit = 0.01f; 38 | account.market_liquidity = 0.5f; 39 | account.max_volume_ratio = 0.9f; 40 | } 41 | } // namespace 42 | 43 | TEST(CsvLoggerTest, LogExchangeState) { 44 | OhlcHistory ohlc_history; 45 | PrepareExampleOhlcHistory(ohlc_history); 46 | 47 | Account account; 48 | PrepareExampleAccount(account); 49 | 50 | std::stringstream exchange_os; 51 | CsvLogger logger(&exchange_os, /*trader_os=*/nullptr); 52 | logger.LogExchangeState(ohlc_history[0], account); 53 | logger.LogExchangeState(ohlc_history[1], account); 54 | logger.LogExchangeState(ohlc_history[2], account); 55 | 56 | EXPECT_EQ(exchange_os.str(), 57 | "1483228800,100.000,150.000,80.000,120.000," 58 | "1000.000,2.000,1000.000,50.000,,,,,\n" 59 | "1483315200,120.000,180.000,100.000,150.000," 60 | "500.000,2.000,1000.000,50.000,,,,,\n" 61 | "1483401600,150.000,250.000,100.000,140.000," 62 | "800.000,2.000,1000.000,50.000,,,,,\n"); 63 | } 64 | 65 | TEST(CsvLoggerTest, LogExchangeStateWithOrder) { 66 | OhlcHistory ohlc_history; 67 | PrepareExampleOhlcHistory(ohlc_history); 68 | 69 | Account account; 70 | PrepareExampleAccount(account); 71 | 72 | Order order; 73 | order.set_type(Order::Type::Order_Type_LIMIT); 74 | order.set_side(Order::Side::Order_Side_SELL); 75 | order.set_quote_amount(1.0f); 76 | order.set_price(500.0f); 77 | 78 | std::stringstream exchange_os; 79 | CsvLogger logger(&exchange_os, /*trader_os=*/nullptr); 80 | logger.LogExchangeState(ohlc_history[0], account); 81 | logger.LogExchangeState(ohlc_history[1], account, order); 82 | logger.LogExchangeState(ohlc_history[2], account); 83 | 84 | EXPECT_EQ(exchange_os.str(), 85 | "1483228800,100.000,150.000,80.000,120.000," 86 | "1000.000,2.000,1000.000,50.000,,,,,\n" 87 | "1483315200,120.000,180.000,100.000,150.000," 88 | "500.000,2.000,1000.000,50.000,LIMIT,SELL,,1.000,500.000\n" 89 | "1483401600,150.000,250.000,100.000,140.000," 90 | "800.000,2.000,1000.000,50.000,,,,,\n"); 91 | } 92 | 93 | TEST(CsvLoggerTest, LogTraderState) { 94 | std::stringstream trader_os; 95 | CsvLogger logger(/*exchange_os=*/nullptr, &trader_os); 96 | logger.LogTraderState("state_1"); 97 | logger.LogTraderState("state_2"); 98 | logger.LogTraderState("state_3"); 99 | 100 | EXPECT_EQ(trader_os.str(), "state_1\nstate_2\nstate_3\n"); 101 | } 102 | 103 | } // namespace trader 104 | -------------------------------------------------------------------------------- /logging/logger.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef LOGGING_LOGGER_H 4 | #define LOGGING_LOGGER_H 5 | 6 | #include "absl/strings/string_view.h" 7 | #include "base/account.h" 8 | #include "base/base.h" 9 | 10 | namespace trader { 11 | 12 | // Interface for logging exchange movements and trader internal state(s). 13 | class Logger { 14 | public: 15 | Logger() {} 16 | virtual ~Logger() {} 17 | 18 | // Logs the current ohlc_tick and account. 19 | virtual void LogExchangeState(const OhlcTick& ohlc_tick, 20 | const Account& account) = 0; 21 | // Logs the current ohlc_tick, account, and order, after executing 22 | // the given order. 23 | virtual void LogExchangeState(const OhlcTick& ohlc_tick, 24 | const Account& account, const Order& order) = 0; 25 | 26 | // Logs the trader state. 27 | virtual void LogTraderState(absl::string_view trader_state) = 0; 28 | }; 29 | 30 | } // namespace trader 31 | 32 | #endif // LOGGING_LOGGER_H 33 | -------------------------------------------------------------------------------- /market_data.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "attachments": {}, 5 | "cell_type": "markdown", 6 | "metadata": {}, 7 | "source": [ 8 | "# Market Data\n", 9 | "\n", 10 | "This code allows you to download historical market data from Yahoo Finance." 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": null, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import numpy as np\n", 20 | "import yfinance as yf\n", 21 | "from datetime import datetime\n", 22 | "\n", 23 | "ticker = 'SPY'\n", 24 | "adjust = True\n", 25 | "start_date = datetime(1990, 1, 1)\n", 26 | "end_date = datetime(2022, 12, 31)\n", 27 | "price_frame = yf.download(ticker, start=start_date, end=end_date)\n", 28 | "\n", 29 | "price_frame.reset_index(inplace=True)\n", 30 | "price_frame['Date'] = price_frame['Date'].astype(np.int64) // 10**9\n", 31 | "\n", 32 | "ohlc_cols = ['Open', 'High', 'Low', 'Close']\n", 33 | "if adjust:\n", 34 | " scale = price_frame['Adj Close'] / price_frame['Close']\n", 35 | " for col in ohlc_cols:\n", 36 | " price_frame[col] *= scale\n", 37 | "del price_frame['Adj Close']\n", 38 | "\n", 39 | "price_frame['High'] = price_frame[ohlc_cols].max(axis=1)\n", 40 | "price_frame['Low'] = price_frame[ohlc_cols].min(axis=1)\n", 41 | "\n", 42 | "price_frame.to_csv('data/%s.csv' % ticker, index=False, header=None)\n", 43 | "price_frame[:5]" 44 | ] 45 | } 46 | ], 47 | "metadata": { 48 | "kernelspec": { 49 | "display_name": "base", 50 | "language": "python", 51 | "name": "python3" 52 | }, 53 | "language_info": { 54 | "codemirror_mode": { 55 | "name": "ipython", 56 | "version": 3 57 | }, 58 | "file_extension": ".py", 59 | "mimetype": "text/x-python", 60 | "name": "python", 61 | "nbconvert_exporter": "python", 62 | "pygments_lexer": "ipython3", 63 | "version": "3.10.8" 64 | }, 65 | "orig_nbformat": 4, 66 | "vscode": { 67 | "interpreter": { 68 | "hash": "fc9c3709868116a7f5b4f435d9339d1a041b6f95ac3b22f3d8824ddf09e4a464" 69 | } 70 | } 71 | }, 72 | "nbformat": 4, 73 | "nbformat_minor": 2 74 | } 75 | -------------------------------------------------------------------------------- /trader.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2023 Peter Cerno. All rights reserved. 2 | 3 | #include "absl/flags/flag.h" 4 | #include "absl/flags/parse.h" 5 | #include "absl/memory/memory.h" 6 | #include "absl/status/status.h" 7 | #include "absl/status/statusor.h" 8 | #include "absl/strings/str_cat.h" 9 | #include "absl/strings/str_format.h" 10 | #include "absl/strings/string_view.h" 11 | #include "absl/time/time.h" 12 | #include "base/base.h" 13 | #include "base/side_input.h" 14 | #include "eval/eval.h" 15 | #include "logging/csv_logger.h" 16 | #include "traders/trader_factory.h" 17 | #include "util/proto.h" 18 | #include "util/time.h" 19 | 20 | ABSL_FLAG(std::string, input_ohlc_history_delimited_proto_file, "", 21 | "Input file containing the delimited OhlcRecord protos."); 22 | ABSL_FLAG(std::string, input_side_history_delimited_proto_file, "", 23 | "Input file containing the delimited SideInputRecord protos."); 24 | ABSL_FLAG(std::string, output_exchange_log_file, "", 25 | "Output CSV file containing the exchange log."); 26 | ABSL_FLAG(std::string, output_trader_log_file, "", 27 | "Output file containing the trader-dependent log."); 28 | ABSL_FLAG(std::string, trader, "stop", 29 | "Trader to be executed. [rebalancing, stop]."); 30 | 31 | ABSL_FLAG(std::string, start_time, "2017-01-01", 32 | "Start date-time YYYY-MM-DD [hh:mm:ss] (included)."); 33 | ABSL_FLAG(std::string, end_time, "2021-01-01", 34 | "End date-time YYYY-MM-DD [hh:mm:ss] (excluded)."); 35 | ABSL_FLAG(int, evaluation_period_months, 0, "Evaluation period in months."); 36 | 37 | ABSL_FLAG(double, start_base_balance, 1.0, "Starting base amount."); 38 | ABSL_FLAG(double, start_quote_balance, 0.0, "Starting quote amount."); 39 | 40 | ABSL_FLAG(double, market_liquidity, 0.5, 41 | "Liquidity for executing market (stop) orders."); 42 | ABSL_FLAG(double, max_volume_ratio, 0.5, 43 | "Fraction of tick volume used to fill the limit order."); 44 | ABSL_FLAG(bool, evaluate_batch, false, "Batch evaluation."); 45 | 46 | using namespace trader; 47 | 48 | namespace { 49 | void LogInfo(absl::string_view str) { absl::PrintF("%s\n", str); } 50 | void LogError(absl::string_view str) { absl::FPrintF(stderr, "%s\n", str); } 51 | void CheckOk(const absl::Status status) { 52 | if (!status.ok()) { 53 | LogError(status.message()); 54 | std::exit(EXIT_FAILURE); 55 | } 56 | } 57 | 58 | // Returns the trader's AccountConfig based on the flags (and default values). 59 | AccountConfig GetAccountConfig() { 60 | AccountConfig config; 61 | config.set_start_base_balance(absl::GetFlag(FLAGS_start_base_balance)); 62 | config.set_start_quote_balance(absl::GetFlag(FLAGS_start_quote_balance)); 63 | config.set_base_unit(0.00001f); 64 | config.set_quote_unit(0.01f); 65 | config.mutable_market_order_fee_config()->set_relative_fee(0.005f); 66 | config.mutable_market_order_fee_config()->set_fixed_fee(0); 67 | config.mutable_market_order_fee_config()->set_minimum_fee(0); 68 | config.mutable_limit_order_fee_config()->set_relative_fee(0.005f); 69 | config.mutable_limit_order_fee_config()->set_fixed_fee(0); 70 | config.mutable_limit_order_fee_config()->set_minimum_fee(0); 71 | config.mutable_stop_order_fee_config()->set_relative_fee(0.005f); 72 | config.mutable_stop_order_fee_config()->set_fixed_fee(0); 73 | config.mutable_stop_order_fee_config()->set_minimum_fee(0); 74 | config.set_market_liquidity(absl::GetFlag(FLAGS_market_liquidity)); 75 | config.set_max_volume_ratio(absl::GetFlag(FLAGS_max_volume_ratio)); 76 | return config; 77 | } 78 | 79 | // Returns a vector of records of type T read from the delimited_proto_file. 80 | template 81 | absl::StatusOr> ReadHistory( 82 | const std::string& delimited_proto_file, absl::Time start_time, 83 | absl::Time end_time) { 84 | const absl::Time latency_start_time = absl::Now(); 85 | std::vector history; 86 | const absl::Status status = 87 | ReadDelimitedMessagesFromFile(delimited_proto_file, history); 88 | if (!status.ok()) { 89 | return status; 90 | } 91 | LogInfo( 92 | absl::StrFormat("- Loaded %d records in %.3f seconds", history.size(), 93 | absl::ToDoubleSeconds(absl::Now() - latency_start_time))); 94 | const std::vector history_subset = HistorySubsetCopy( 95 | history, absl::ToUnixSeconds(start_time), absl::ToUnixSeconds(end_time)); 96 | LogInfo( 97 | absl::StrFormat("- Selected %d records within the time period: [%s - %s)", 98 | history_subset.size(), // nowrap 99 | FormatTimeUTC(start_time), // nowrap 100 | FormatTimeUTC(end_time))); 101 | return history_subset; 102 | } 103 | 104 | // Opens the file log_filename for logging purposes. 105 | absl::StatusOr> OpenLogFile( 106 | const std::string& log_filename) { 107 | if (log_filename.empty()) { 108 | return nullptr; 109 | } 110 | if (absl::GetFlag(FLAGS_evaluation_period_months) > 0) { 111 | return absl::FailedPreconditionError( 112 | "Logging disabled when evaluating multiple periods"); 113 | } 114 | auto log_stream = absl::make_unique(); 115 | log_stream->open(log_filename, std::ios::out | std::ios::trunc); 116 | if (!log_stream->is_open()) { 117 | return absl::InvalidArgumentError( 118 | absl::StrFormat("Cannot open the file: %s", log_filename)); 119 | } 120 | return log_stream; 121 | } 122 | 123 | void PrintBatchEvalResults(const std::vector& eval_results, 124 | size_t top_n) { 125 | const size_t eval_count = std::min(top_n, eval_results.size()); 126 | for (size_t eval_index = 0; eval_index < eval_count; ++eval_index) { 127 | const EvaluationResult& eval_result = eval_results.at(eval_index); 128 | LogInfo( 129 | absl::StrFormat("%s: %.5f", eval_result.name(), eval_result.score())); 130 | } 131 | } 132 | 133 | void PrintTraderEvalResult(const EvaluationResult& eval_result) { 134 | LogInfo(absl::StrCat("------------------ period ------------------", 135 | " trader & base gain score t&b volatility")); 136 | for (const EvaluationResult::Period& period : eval_result.period()) { 137 | LogInfo(absl::StrFormat( 138 | "[%s - %s): %9.2f%% %9.2f%% %8.3f %8.3f %8.3f", 139 | FormatTimeUTC(absl::FromUnixSeconds(period.start_timestamp_sec())), 140 | FormatTimeUTC(absl::FromUnixSeconds(period.end_timestamp_sec())), 141 | (period.final_gain() - 1.0f) * 100.0f, 142 | (period.base_final_gain() - 1.0f) * 100.0f, 143 | period.final_gain() / period.base_final_gain(), 144 | period.result().trader_volatility(), 145 | period.result().base_volatility())); 146 | } 147 | } 148 | } // namespace 149 | 150 | int main(int argc, char* argv[]) { 151 | // Verify that the version of the library that we linked against is 152 | // compatible with the version of the headers we compiled against. 153 | GOOGLE_PROTOBUF_VERIFY_VERSION; 154 | 155 | absl::ParseCommandLine(argc, argv); 156 | 157 | const AccountConfig account_config = GetAccountConfig(); 158 | LogInfo("Trader AccountConfig:"); 159 | LogInfo(account_config.DebugString()); 160 | 161 | const absl::StatusOr start_time_status = 162 | ParseTime(absl::GetFlag(FLAGS_start_time)); 163 | CheckOk(start_time_status.status()); 164 | const absl::StatusOr end_time_status = 165 | ParseTime(absl::GetFlag(FLAGS_end_time)); 166 | CheckOk(end_time_status.status()); 167 | const absl::Time start_time = start_time_status.value(); 168 | const absl::Time end_time = end_time_status.value(); 169 | LogInfo(absl::StrFormat("Selected time period:\n[%s - %s)", 170 | FormatTimeUTC(start_time), FormatTimeUTC(end_time))); 171 | 172 | EvaluationConfig eval_config; 173 | eval_config.set_start_timestamp_sec(absl::ToUnixSeconds(start_time)); 174 | eval_config.set_end_timestamp_sec(absl::ToUnixSeconds(end_time)); 175 | eval_config.set_evaluation_period_months( 176 | absl::GetFlag(FLAGS_evaluation_period_months)); 177 | 178 | LogInfo("\nTrader EvaluationConfig:"); 179 | LogInfo(eval_config.DebugString()); 180 | 181 | LogInfo(absl::StrFormat( 182 | "Reading OHLC history from: %s", 183 | absl::GetFlag(FLAGS_input_ohlc_history_delimited_proto_file))); 184 | absl::StatusOr ohlc_history_status = ReadHistory( 185 | absl::GetFlag(FLAGS_input_ohlc_history_delimited_proto_file), // nowrap 186 | start_time, end_time); 187 | CheckOk(ohlc_history_status.status()); 188 | const OhlcHistory& ohlc_history = ohlc_history_status.value(); 189 | 190 | std::unique_ptr side_input; 191 | if (!absl::GetFlag(FLAGS_input_side_history_delimited_proto_file).empty()) { 192 | LogInfo(absl::StrFormat( 193 | "Reading side history from: %s", 194 | absl::GetFlag(FLAGS_input_side_history_delimited_proto_file))); 195 | absl::StatusOr side_history_status = 196 | ReadHistory( 197 | absl::GetFlag(FLAGS_input_side_history_delimited_proto_file), 198 | start_time, end_time); 199 | CheckOk(side_history_status.status()); 200 | side_input = absl::make_unique(side_history_status.value()); 201 | } 202 | 203 | const absl::Time latency_start_time = absl::Now(); 204 | if (absl::GetFlag(FLAGS_evaluate_batch)) { 205 | eval_config.set_fast_eval(true); 206 | LogInfo("\nBatch evaluation:"); 207 | std::vector> trader_emitters = 208 | GetBatchOfTraders(absl::GetFlag(FLAGS_trader)); 209 | std::vector eval_results = 210 | EvaluateBatchOfTraders(account_config, eval_config, ohlc_history, 211 | side_input.get(), trader_emitters); 212 | std::sort(eval_results.begin(), eval_results.end(), 213 | [](const EvaluationResult& lhs, const EvaluationResult& rhs) { 214 | return lhs.score() > rhs.score(); 215 | }); 216 | PrintBatchEvalResults(eval_results, 20); 217 | } else { 218 | eval_config.set_fast_eval(false); 219 | std::unique_ptr trader_emitter = 220 | GetTrader(absl::GetFlag(FLAGS_trader)); 221 | LogInfo(absl::StrFormat("\n%s evaluation:", trader_emitter->GetName())); 222 | absl::StatusOr> exchange_log_stream_status = 223 | OpenLogFile(absl::GetFlag(FLAGS_output_exchange_log_file)); 224 | CheckOk(exchange_log_stream_status.status()); 225 | absl::StatusOr> trader_log_stream_status = 226 | OpenLogFile(absl::GetFlag(FLAGS_output_trader_log_file)); 227 | CheckOk(trader_log_stream_status.status()); 228 | CsvLogger logger(exchange_log_stream_status.value().get(), 229 | trader_log_stream_status.value().get()); 230 | EvaluationResult eval_result = 231 | EvaluateTrader(account_config, eval_config, ohlc_history, 232 | side_input.get(), *trader_emitter, &logger); 233 | PrintTraderEvalResult(eval_result); 234 | } 235 | LogInfo( 236 | absl::StrFormat("\nEvaluated in %.3f seconds", 237 | absl::ToDoubleSeconds(absl::Now() - latency_start_time))); 238 | 239 | // Optional: Delete all global objects allocated by libprotobuf. 240 | google::protobuf::ShutdownProtobufLibrary(); 241 | 242 | return 0; 243 | } 244 | -------------------------------------------------------------------------------- /traders/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//:__pkg__"]) 2 | 3 | load("@rules_proto//proto:defs.bzl", "proto_library") 4 | 5 | proto_library( 6 | name = "trader_config_proto", 7 | srcs = ["trader_config.proto"], 8 | ) 9 | 10 | cc_proto_library( 11 | name = "trader_config_cc_proto", 12 | deps = [":trader_config_proto"], 13 | ) 14 | 15 | cc_library( 16 | name = "traders", 17 | srcs = [ 18 | "rebalancing_trader.cc", 19 | "stop_trader.cc", 20 | ], 21 | hdrs = [ 22 | "rebalancing_trader.h", 23 | "stop_trader.h", 24 | ], 25 | deps = [ 26 | ":trader_config_cc_proto", 27 | "//base:trader", 28 | "@com_google_absl//absl/memory", 29 | "@com_google_absl//absl/strings", 30 | "@com_google_absl//absl/strings:str_format", 31 | ], 32 | ) 33 | 34 | cc_library( 35 | name = "trader_factory", 36 | srcs = ["trader_factory.cc"], 37 | hdrs = ["trader_factory.h"], 38 | deps = [ 39 | ":traders", 40 | "@com_google_absl//absl/strings", 41 | ], 42 | ) 43 | -------------------------------------------------------------------------------- /traders/rebalancing_trader.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "traders/rebalancing_trader.h" 4 | 5 | #include "absl/memory/memory.h" 6 | #include "absl/strings/str_format.h" 7 | 8 | namespace trader { 9 | 10 | void RebalancingTrader::Update(const OhlcTick& ohlc_tick, 11 | const std::vector& side_input_signals, 12 | float base_balance, float quote_balance, 13 | std::vector& orders) { 14 | const int64_t timestamp_sec = ohlc_tick.timestamp_sec(); 15 | const float price = ohlc_tick.close(); 16 | assert(timestamp_sec > last_timestamp_sec_); 17 | assert(price > 0); 18 | assert(base_balance > 0 || quote_balance > 0); 19 | const float portfolio_value = base_balance * price + quote_balance; 20 | const float alpha = trader_config_.alpha(); 21 | const float epsilon = trader_config_.epsilon(); 22 | const float alpha_up = alpha * (1 + epsilon); 23 | const float alpha_down = alpha * (1 - epsilon); 24 | const float beta = base_balance * price / portfolio_value; 25 | if (beta > alpha_up) { 26 | const float market_sell_base_amount = 27 | ((1 - alpha) * portfolio_value - quote_balance) / price; 28 | orders.emplace_back(); 29 | Order& order = orders.back(); 30 | order.set_type(Order_Type_MARKET); 31 | order.set_side(Order_Side_SELL); 32 | order.set_base_amount(market_sell_base_amount); 33 | } else if (beta < alpha_down) { 34 | const float market_buy_base_amount = 35 | (quote_balance - (1 - alpha) * portfolio_value) / price; 36 | orders.emplace_back(); 37 | Order& order = orders.back(); 38 | order.set_type(Order_Type_MARKET); 39 | order.set_side(Order_Side_BUY); 40 | order.set_base_amount(market_buy_base_amount); 41 | } else if (base_balance > 1.0e-6f && quote_balance > 1.0e-6f) { 42 | if (alpha * (1 + epsilon) < 1) { 43 | const float sell_price = 44 | (alpha * (1 + epsilon) * quote_balance) / (1 - alpha * (1 + epsilon)); 45 | if (sell_price > price && sell_price < 100.0f * price) { 46 | const float sell_base_amount = base_balance * epsilon / (1 + epsilon); 47 | orders.emplace_back(); 48 | Order& sell_order = orders.back(); 49 | sell_order.set_type(Order_Type_LIMIT); 50 | sell_order.set_side(Order_Side_SELL); 51 | sell_order.set_base_amount(sell_base_amount); 52 | sell_order.set_price(sell_price); 53 | } 54 | } 55 | const float buy_price = 56 | (alpha * (1 - epsilon) * quote_balance) / (1 - alpha * (1 - epsilon)); 57 | if (buy_price < price && buy_price > price / 100.0f) { 58 | const float buy_base_amount = base_balance * epsilon / (1 - epsilon); 59 | orders.emplace_back(); 60 | Order& buy_order = orders.back(); 61 | buy_order.set_type(Order_Type_LIMIT); 62 | buy_order.set_side(Order_Side_BUY); 63 | buy_order.set_base_amount(buy_base_amount); 64 | buy_order.set_price(buy_price); 65 | } 66 | } 67 | last_base_balance_ = base_balance; 68 | last_quote_balance_ = quote_balance; 69 | last_timestamp_sec_ = timestamp_sec; 70 | last_close_ = price; 71 | } 72 | 73 | std::string RebalancingTrader::GetInternalState() const { 74 | return absl::StrFormat("%d,%.3f,%.3f,%.3f", last_timestamp_sec_, 75 | last_base_balance_, last_quote_balance_, last_close_); 76 | } 77 | 78 | std::string RebalancingTraderEmitter::GetName() const { 79 | return absl::StrFormat("rebalancing-trader[%.3f|%.3f]", 80 | trader_config_.alpha(), trader_config_.epsilon()); 81 | } 82 | 83 | std::unique_ptr RebalancingTraderEmitter::NewTrader() const { 84 | return absl::make_unique(trader_config_); 85 | } 86 | 87 | std::vector> 88 | RebalancingTraderEmitter::GetBatchOfTraders( 89 | const std::vector& alphas, const std::vector& epsilons) { 90 | std::vector> batch; 91 | batch.reserve(alphas.size() * epsilons.size()); 92 | for (const float alpha : alphas) 93 | for (const float epsilon : epsilons) { 94 | RebalancingTraderConfig trader_config; 95 | trader_config.set_alpha(alpha); 96 | trader_config.set_epsilon(epsilon); 97 | batch.emplace_back(new RebalancingTraderEmitter(trader_config)); 98 | } 99 | return batch; 100 | } 101 | 102 | } // namespace trader 103 | -------------------------------------------------------------------------------- /traders/rebalancing_trader.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef TRADERS_REBALANCING_TRADER_H 4 | #define TRADERS_REBALANCING_TRADER_H 5 | 6 | #include "base/trader.h" 7 | #include "traders/trader_config.pb.h" 8 | 9 | namespace trader { 10 | 11 | // RebalancingTrader keeps the base (crypto) currency value to quote value 12 | // ratio constant. 13 | class RebalancingTrader : public Trader { 14 | public: 15 | explicit RebalancingTrader(const RebalancingTraderConfig& trader_config) 16 | : trader_config_(trader_config) {} 17 | virtual ~RebalancingTrader() {} 18 | 19 | void Update(const OhlcTick& ohlc_tick, 20 | const std::vector& side_input_signals, float base_balance, 21 | float quote_balance, std::vector& orders) override; 22 | std::string GetInternalState() const override; 23 | 24 | private: 25 | RebalancingTraderConfig trader_config_; 26 | // Last seen trader account balance. 27 | float last_base_balance_ = 0.0f; 28 | float last_quote_balance_ = 0.0f; 29 | // Last seen UNIX timestamp (in seconds). 30 | int64_t last_timestamp_sec_ = 0; 31 | // Last seen close price. 32 | float last_close_ = 0.0f; 33 | }; 34 | 35 | // Emitter that emits RebalancingTraders. 36 | class RebalancingTraderEmitter : public TraderEmitter { 37 | public: 38 | explicit RebalancingTraderEmitter( 39 | const RebalancingTraderConfig& trader_config) 40 | : trader_config_(trader_config) {} 41 | virtual ~RebalancingTraderEmitter() {} 42 | 43 | std::string GetName() const override; 44 | std::unique_ptr NewTrader() const override; 45 | 46 | static std::vector> GetBatchOfTraders( 47 | const std::vector& alphas, const std::vector& epsilons); 48 | 49 | private: 50 | RebalancingTraderConfig trader_config_; 51 | }; 52 | 53 | } // namespace trader 54 | 55 | #endif // TRADERS_REBALANCING_TRADER_H 56 | -------------------------------------------------------------------------------- /traders/stop_trader.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "traders/stop_trader.h" 4 | 5 | #include "absl/memory/memory.h" 6 | #include "absl/strings/str_format.h" 7 | 8 | namespace trader { 9 | 10 | void StopTrader::Update(const OhlcTick& ohlc_tick, 11 | const std::vector& side_input_signals, 12 | float base_balance, float quote_balance, 13 | std::vector& orders) { 14 | const int64_t timestamp_sec = ohlc_tick.timestamp_sec(); 15 | const float price = ohlc_tick.close(); 16 | assert(timestamp_sec > last_timestamp_sec_); 17 | assert(price > 0); 18 | assert(base_balance > 0 || quote_balance > 0); 19 | const Mode mode = 20 | (base_balance * price >= quote_balance) ? Mode::LONG : Mode::CASH; 21 | if (timestamp_sec >= last_timestamp_sec_ + max_allowed_gap_sec_ || 22 | mode != mode_) { 23 | if (mode == Mode::LONG) { 24 | stop_order_price_ = (1 - trader_config_.stop_order_margin()) * price; 25 | } else { 26 | assert(mode == Mode::CASH); 27 | stop_order_price_ = (1 + trader_config_.stop_order_margin()) * price; 28 | } 29 | } else { 30 | assert(mode == mode_); 31 | UpdateStopOrderPrice(mode, timestamp_sec, price); 32 | } 33 | last_base_balance_ = base_balance; 34 | last_quote_balance_ = quote_balance; 35 | last_timestamp_sec_ = timestamp_sec; 36 | last_close_ = price; 37 | mode_ = mode; 38 | EmitStopOrder(price, orders); 39 | } 40 | 41 | void StopTrader::UpdateStopOrderPrice(Mode mode, int64_t timestamp_sec, 42 | float price) { 43 | const float sampling_rate_sec = std::min(static_cast(kSecondsPerDay), 44 | timestamp_sec - last_timestamp_sec_); 45 | const float ticks_per_day = kSecondsPerDay / sampling_rate_sec; 46 | if (mode == Mode::LONG) { 47 | const float stop_order_increase_threshold = 48 | (1 - trader_config_.stop_order_move_margin()) * price; 49 | if (stop_order_price_ <= stop_order_increase_threshold) { 50 | const float stop_order_increase_per_tick = 51 | std::exp(std::log(1 + trader_config_.stop_order_increase_per_day()) / 52 | ticks_per_day) - 53 | 1; 54 | stop_order_price_ = std::max( 55 | stop_order_price_, 56 | std::min(stop_order_increase_threshold, 57 | (1 + stop_order_increase_per_tick) * stop_order_price_)); 58 | } 59 | } else { 60 | assert(mode == Mode::CASH); 61 | const float stop_order_decrease_threshold = 62 | (1 + trader_config_.stop_order_move_margin()) * price; 63 | if (stop_order_price_ >= stop_order_decrease_threshold) { 64 | const float stop_order_decrease_per_tick = 65 | 1 - 66 | std::exp(std::log(1 - trader_config_.stop_order_decrease_per_day()) / 67 | ticks_per_day); 68 | stop_order_price_ = std::min( 69 | stop_order_price_, 70 | std::max(stop_order_decrease_threshold, 71 | (1 - stop_order_decrease_per_tick) * stop_order_price_)); 72 | } 73 | } 74 | } 75 | 76 | void StopTrader::EmitStopOrder(float price, std::vector& orders) const { 77 | orders.emplace_back(); 78 | Order& order = orders.back(); 79 | order.set_type(Order_Type_STOP); 80 | if (mode_ == Mode::LONG) { 81 | order.set_side(Order_Side_SELL); 82 | order.set_base_amount(last_base_balance_); 83 | } else { 84 | assert(mode_ == Mode::CASH); 85 | order.set_side(Order_Side_BUY); 86 | order.set_quote_amount(last_quote_balance_); 87 | } 88 | order.set_price(stop_order_price_); 89 | } 90 | 91 | std::string StopTrader::GetInternalState() const { 92 | return absl::StrFormat("%d,%.3f,%.3f,%.3f,%s,%.3f", last_timestamp_sec_, 93 | last_base_balance_, last_quote_balance_, last_close_, 94 | mode_ == Mode::LONG ? "LONG" : "CASH", 95 | stop_order_price_); 96 | } 97 | 98 | std::string StopTraderEmitter::GetName() const { 99 | return absl::StrFormat("stop-trader[%.3f|%.3f|%.3f|%.3f]", 100 | trader_config_.stop_order_margin(), 101 | trader_config_.stop_order_move_margin(), 102 | trader_config_.stop_order_increase_per_day(), 103 | trader_config_.stop_order_decrease_per_day()); 104 | } 105 | 106 | std::unique_ptr StopTraderEmitter::NewTrader() const { 107 | return absl::make_unique(trader_config_); 108 | } 109 | 110 | std::vector> 111 | StopTraderEmitter::GetBatchOfTraders( 112 | const std::vector& stop_order_margins, 113 | const std::vector& stop_order_move_margins, 114 | const std::vector& stop_order_increases_per_day, 115 | const std::vector& stop_order_decreases_per_day) { 116 | std::vector> batch; 117 | batch.reserve(stop_order_margins.size() * stop_order_move_margins.size() * 118 | stop_order_increases_per_day.size() * 119 | stop_order_decreases_per_day.size()); 120 | for (const float stop_order_margin : stop_order_margins) 121 | for (const float stop_order_move_margin : stop_order_move_margins) 122 | for (const float stop_order_increase_per_day : 123 | stop_order_increases_per_day) 124 | for (const float stop_order_decrease_per_day : 125 | stop_order_decreases_per_day) { 126 | StopTraderConfig trader_config; 127 | trader_config.set_stop_order_margin(stop_order_margin); 128 | trader_config.set_stop_order_move_margin(stop_order_move_margin); 129 | trader_config.set_stop_order_increase_per_day( 130 | stop_order_increase_per_day); 131 | trader_config.set_stop_order_decrease_per_day( 132 | stop_order_decrease_per_day); 133 | batch.emplace_back(new StopTraderEmitter(trader_config)); 134 | } 135 | return batch; 136 | } 137 | 138 | } // namespace trader 139 | -------------------------------------------------------------------------------- /traders/stop_trader.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef TRADERS_STOP_TRADER_H 4 | #define TRADERS_STOP_TRADER_H 5 | 6 | #include "base/trader.h" 7 | #include "traders/trader_config.pb.h" 8 | 9 | namespace trader { 10 | 11 | // Stop trader. Emits exactly one stop order per OHLC tick. 12 | class StopTrader : public Trader { 13 | public: 14 | explicit StopTrader(const StopTraderConfig& trader_config) 15 | : trader_config_(trader_config) {} 16 | virtual ~StopTrader() {} 17 | 18 | void Update(const OhlcTick& ohlc_tick, 19 | const std::vector& side_input_signals, float base_balance, 20 | float quote_balance, std::vector& orders) override; 21 | std::string GetInternalState() const override; 22 | 23 | private: 24 | // Enumeration of possible trader modes. 25 | enum class Mode { 26 | NONE, // Undefined. 27 | LONG, // Trader holds most of its assets in the base (crypto) currency. 28 | CASH // Trader holds most of its assets in the quote currency. 29 | }; 30 | 31 | StopTraderConfig trader_config_; 32 | // Last seen trader account balance. 33 | float last_base_balance_ = 0.0f; 34 | float last_quote_balance_ = 0.0f; 35 | // Last seen UNIX timestamp (in seconds). 36 | int64_t last_timestamp_sec_ = 0; 37 | // Last seen close price. 38 | float last_close_ = 0.0f; 39 | // Last trader mode. 40 | Mode mode_ = Mode::NONE; 41 | // Last base (crypto) currency price for the stop order. 42 | float stop_order_price_ = 0; 43 | 44 | // Maximum allowed timestamp gap (in seconds). 45 | // When we encounter such gap, we re-initialize the trader. 46 | int max_allowed_gap_sec_ = 1 * 60 * 60; // 1 hour. 47 | 48 | // Updates the trader stop order price. 49 | void UpdateStopOrderPrice(Mode mode, int64_t timestamp_sec, float price); 50 | // Emits the stop order based on the (updated) internal trader state. 51 | void EmitStopOrder(float price, std::vector& orders) const; 52 | }; 53 | 54 | // Emitter that emits StopTraders. 55 | class StopTraderEmitter : public TraderEmitter { 56 | public: 57 | explicit StopTraderEmitter(const StopTraderConfig& trader_config) 58 | : trader_config_(trader_config) {} 59 | virtual ~StopTraderEmitter() {} 60 | 61 | std::string GetName() const override; 62 | std::unique_ptr NewTrader() const override; 63 | 64 | static std::vector> GetBatchOfTraders( 65 | const std::vector& stop_order_margins, 66 | const std::vector& stop_order_move_margins, 67 | const std::vector& stop_order_increases_per_day, 68 | const std::vector& stop_order_decreases_per_day); 69 | 70 | private: 71 | StopTraderConfig trader_config_; 72 | }; 73 | 74 | } // namespace trader 75 | 76 | #endif // TRADERS_STOP_TRADER_H 77 | -------------------------------------------------------------------------------- /traders/stop_trader.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Stop Trader\n", 8 | "\n", 9 | "The goal of the `stop` trader is to explore whether it makes sense to set up stop-losses (or stop-buys) when entering a long position (or when selling everything).\n", 10 | "\n", 11 | "Imagine that you buy a certain amount $a$ of a cryptocurrency (e.g. `BTC`) at price $p$. Is it a good idea to set up a stop-loss at some lower price $p' < p$? If so, how to determine the price $p'$? What if later the price $p$ goes up? Should we then adjust (increase) the stop-loss price $p'$ as well? These are some of the questions that we would like to answer by evaluating many instances of the `stop` trader over the `BTCUSD` price history.\n", 12 | "\n", 13 | "At any given moment the trader has (on its account) some amount $b$ of a base (crypto)currency (e.g. `BTC`) and some amount $q$ of a quote currency (e.g. `USD`). Let $p$ denote the base (crypto)currency price (e.g. `BTCUSD` price). If $b * p \\ge q$ we say that the trader is in the `LONG` state. Otherwise, we say that the trader is in the `CASH` state.\n", 14 | "\n", 15 | "* If the trader gets into the `LONG` state we define the stop-loss at the price $p' := (1 - \\alpha) * p < p$, where $\\alpha \\in (0; 1)$ is a trader hyper-parameter (called `stop_order_margin`). As long as the trader is in the `LONG` state we only allow the price of the stop-loss $p'$ to increase over time. When the price $p$ drops and hits the stop-loss the trader automatically switches to the `CASH` state (by selling all its $b$ base (crypto)currency at price $p'$).\n", 16 | "* Similarly, if the trader gets into the `CASH` state we define the stop-buy at the price $p' := (1 + \\alpha) * p > p$. As long as the trader is in the `CASH` state we only allow the price of the stop-buy $p'$ to decrease over time. When the price $p$ increases and hits the stop-buy the trader automatically switches to the `LONG` state (by buying as much as possible base (crypto)currency at price $p'$).\n", 17 | "\n", 18 | "One can easily observe that if there were no transaction fees and if we could always execute the stop-sells (stop-buys) at exactly the specified price $p'$, this strategy would never be worse than the Buy-And-HODL strategy. Moreover, we would (under these ideal circumstances) never loose any money (only potentially gain). Note: If you think that these assumptions (no transaction fees etc.) are far-fetched then take a look at the assumptions behind the famous [Black-Scholes model](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model) (they are even more unrealistic).\n", 19 | "\n", 20 | "Unfortunately, we do have non-negligible transaction fees and the stop orders are (usually) not executed at exactly the desired price $p'$, but at a worse price (especially if there is not enough liquidity). Therefore, we need to tune the hyper-parameters of the trader so that we do not incur too many transaction fees and still stay profitable.\n", 21 | "\n", 22 | "In addition to the above-mentioned `stop_order_margin` $\\alpha$ we have the following hyper-parameters:\n", 23 | "\n", 24 | "* `stop_order_move_margin` $\\beta$: (A) If the trader is in the `LONG` state and the stop-loss price: $p' \\le (1 - \\beta) * p$ we allow the stop-loss price to increase. (B) Similarly, if the trader is in the `CASH` state and the stop-buy price $p' \\ge (1 + \\beta) * p$ we allow the stop-buy price to decrease.\n", 25 | "* `stop_order_increase_per_day` $\\gamma_+$: In the case (A) we allow the stop-loss price to increase by at most $\\gamma_+ * 100\\%$ per day, although we do not allow the stop-loss price to exceed the $(1 - \\beta) * p$ threshold.\n", 26 | "* `stop_order_decrease_per_day` $\\gamma_-$: In the case (B) we allow the stop-buy price to decrease by at most $\\gamma_- * 100\\%$ per day, although we do not allow the stop-buy price to drop below the $(1 + \\beta) * p$ threshold.\n", 27 | "\n", 28 | "Batch evaluation of the `stop` trader reveals that setting $\\alpha = \\beta = 0.1$, $\\gamma_+ = 0.01$, and $\\gamma_- = 0.1$ gives a reasonable performance (although there are many other settings which perform similarly). With these specific parameters the trader performs as follows (evaluated over 6 month time periods within the 4 year time period: `[2017-01-01 - 2021-01-01)` with 5 minute sampling rate):\n", 29 | "\n", 30 | "```\n", 31 | "bazel run :trader -- \\\n", 32 | " --input_ohlc_history_delimited_proto_file=\"/$(pwd)/data/bitstampUSD_5min.dpb\" \\\n", 33 | " --trader=\"stop\" \\\n", 34 | " --start_date_utc=\"2017-01-01\" \\\n", 35 | " --end_date_utc=\"2021-01-01\" \\\n", 36 | " --evaluation_period_months=6 \\\n", 37 | " --start_base_balance=1.0 \\\n", 38 | " --start_quote_balance=0.0\n", 39 | "```\n", 40 | "\n", 41 | "Output (shortened):\n", 42 | "\n", 43 | "```\n", 44 | "...\n", 45 | "Reading OHLC history from: .../data/bitstampUSD_5min.dpb\n", 46 | "- Loaded 420768 records in 0.782 seconds\n", 47 | "- Selected 420768 records within the time period: [2017-01-01 00:00:00 - 2021-01-01 00:00:00)\n", 48 | "\n", 49 | "stop-trader[0.100|0.100|0.010|0.100] evaluation:\n", 50 | "------------------ period ------------------ trader & base gain score t&b volatility\n", 51 | "[2017-01-01 00:00:00 - 2017-07-01 00:00:00): 90.13% 155.13% 0.745 0.744 0.767\n", 52 | "[2017-02-01 00:00:00 - 2017-08-01 00:00:00): 159.53% 196.26% 0.876 0.768 0.845\n", 53 | "[2017-03-01 00:00:00 - 2017-09-01 00:00:00): 255.64% 297.85% 0.894 0.801 0.877\n", 54 | "[2017-04-01 00:00:00 - 2017-10-01 00:00:00): 314.13% 302.96% 1.028 0.846 0.921\n", 55 | "[2017-05-01 00:00:00 - 2017-11-01 00:00:00): 393.33% 376.78% 1.035 0.872 0.946\n", 56 | "...\n", 57 | "[2020-03-01 00:00:00 - 2020-09-01 00:00:00): 59.34% 36.01% 1.171 0.651 0.982\n", 58 | "[2020-04-01 00:00:00 - 2020-10-01 00:00:00): 44.16% 67.60% 0.860 0.546 0.571\n", 59 | "[2020-05-01 00:00:00 - 2020-11-01 00:00:00): 8.95% 59.90% 0.681 0.501 0.512\n", 60 | "[2020-06-01 00:00:00 - 2020-12-01 00:00:00): 79.72% 108.97% 0.860 0.490 0.517\n", 61 | "[2020-07-01 00:00:00 - 2021-01-01 00:00:00): 206.57% 218.20% 0.963 0.522 0.542\n", 62 | "\n", 63 | "Evaluated in 10.021 seconds\n", 64 | "\n", 65 | "```\n", 66 | "\n", 67 | "As you can see, the `stop` trader sometimes beats the Buy-And-HODL strategy (although not always). Over the whole 4 year time period: `[2017-01-01 - 2021-01-01)` with 5 minute sampling rate:\n", 68 | "\n", 69 | "```\n", 70 | "...\n", 71 | "Reading OHLC history from: .../data/bitstampUSD_5min.dpb\n", 72 | "- Loaded 420768 records in 1.064 seconds\n", 73 | "- Selected 420768 records within the time period: [2017-01-01 00:00:00 - 2021-01-01 00:00:00)\n", 74 | "\n", 75 | "stop-trader[0.100|0.100|0.010|0.100] evaluation:\n", 76 | "------------------ period ------------------ trader & base gain score t&b volatility\n", 77 | "[2017-01-01 00:00:00 - 2021-01-01 00:00:00): 3220.23% 2900.17% 1.107 0.713 0.828\n", 78 | "\n", 79 | "Evaluated in 1.842 seconds\n", 80 | "```\n", 81 | "\n", 82 | "the `stop` trader would beat the Buy-And-HODL strategy only by 10%. That being said, there are many other (suboptimal) choices of the hyper-parameters that would lead to much worse performance. Thus it is questionable whether an average person would be able to set the stop-losses / stop-buys better than this algorithm. If not then an average person would be better off with HODLing without any stop-losses." 83 | ] 84 | } 85 | ], 86 | "metadata": { 87 | "kernelspec": { 88 | "display_name": "Python 3", 89 | "language": "python", 90 | "name": "python3" 91 | }, 92 | "language_info": { 93 | "codemirror_mode": { 94 | "name": "ipython", 95 | "version": 3 96 | }, 97 | "file_extension": ".py", 98 | "mimetype": "text/x-python", 99 | "name": "python", 100 | "nbconvert_exporter": "python", 101 | "pygments_lexer": "ipython3", 102 | "version": "3.9.1" 103 | } 104 | }, 105 | "nbformat": 4, 106 | "nbformat_minor": 4 107 | } 108 | -------------------------------------------------------------------------------- /traders/trader_config.proto: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | syntax = "proto2"; 4 | 5 | package trader; 6 | 7 | // Rebalancing trader configuration. 8 | message RebalancingTraderConfig { 9 | // Desired alpha-allocation. 10 | // For example, alpha = 0.7 means that we would like to keep 70% 11 | // of the total portfolio value in the base (crypto)currency, 12 | // and 30% in the quote currency. 13 | optional float alpha = 1; 14 | // Maximum allowed deviation from the desired alpha-allocation. 15 | // We allow the actual base (crypto)currency allocation to be within 16 | // the interval: (alpha * (1 - epsilon); alpha * (1 + epsilon). 17 | optional float epsilon = 2; 18 | } 19 | 20 | // Stop trader configuration. 21 | message StopTraderConfig { 22 | // Margin for setting the stop order price w.r.t. the current price. 23 | optional float stop_order_margin = 1; 24 | // Margin for moving the stop order price w.r.t. the current price. 25 | optional float stop_order_move_margin = 2; 26 | // Maximum relative stop order price increase per day. 27 | optional float stop_order_increase_per_day = 3; 28 | // Maximum relative stop order price decrease per day. 29 | optional float stop_order_decrease_per_day = 4; 30 | } 31 | -------------------------------------------------------------------------------- /traders/trader_factory.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "traders/trader_factory.h" 4 | 5 | #include "traders/rebalancing_trader.h" 6 | #include "traders/stop_trader.h" 7 | #include "traders/trader_config.pb.h" 8 | 9 | namespace trader { 10 | namespace { 11 | static constexpr char kRebalancingTraderName[] = "rebalancing"; 12 | static constexpr char kStopTraderName[] = "stop"; 13 | 14 | // Returns the default rebalancing trader emitter. 15 | std::unique_ptr GetDefaultRebalancingTraderEmitter() { 16 | RebalancingTraderConfig config; 17 | config.set_alpha(0.7f); 18 | config.set_epsilon(0.05f); 19 | return std::unique_ptr(new RebalancingTraderEmitter(config)); 20 | } 21 | 22 | // Returns the default batch of rebalancing traders. 23 | std::vector> GetBatchOfRebalancingTraders() { 24 | return RebalancingTraderEmitter::GetBatchOfTraders( 25 | /*alphas=*/{0.1f, 0.3f, 0.5f, 0.7f, 0.9f}, 26 | /*epsilons=*/{0.01f, 0.05f, 0.1f, 0.2f}); 27 | } 28 | 29 | // Returns the default stop trader emitter. 30 | std::unique_ptr GetDefaultStopTraderEmitter() { 31 | StopTraderConfig config; 32 | config.set_stop_order_margin(0.1f); 33 | config.set_stop_order_move_margin(0.1f); 34 | config.set_stop_order_increase_per_day(0.01f); 35 | config.set_stop_order_decrease_per_day(0.1f); 36 | return std::unique_ptr(new StopTraderEmitter(config)); 37 | } 38 | 39 | // Returns the default batch of stop traders. 40 | std::vector> GetBatchOfStopTraders() { 41 | return StopTraderEmitter::GetBatchOfTraders( 42 | /*stop_order_margins=*/{0.05, 0.1, 0.15, 0.2}, 43 | /*stop_order_move_margins=*/{0.05, 0.1, 0.15, 0.2}, 44 | /*stop_order_increases_per_day=*/{0.01, 0.05, 0.1}, 45 | /*stop_order_decreases_per_day=*/{0.01, 0.05, 0.1}); 46 | } 47 | } // namespace 48 | 49 | std::unique_ptr GetTrader(absl::string_view trader_name) { 50 | if (trader_name == kRebalancingTraderName) { 51 | return GetDefaultRebalancingTraderEmitter(); 52 | } else { 53 | assert(trader_name == kStopTraderName); 54 | return GetDefaultStopTraderEmitter(); 55 | } 56 | } 57 | 58 | std::vector> GetBatchOfTraders( 59 | absl::string_view trader_name) { 60 | if (trader_name == kRebalancingTraderName) { 61 | return GetBatchOfRebalancingTraders(); 62 | } else { 63 | assert(trader_name == kStopTraderName); 64 | return GetBatchOfStopTraders(); 65 | } 66 | } 67 | 68 | } // namespace trader 69 | -------------------------------------------------------------------------------- /traders/trader_factory.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef TRADERS_TRADER_FACTORY_H 4 | #define TRADERS_TRADER_FACTORY_H 5 | 6 | #include "absl/strings/string_view.h" 7 | #include "base/trader.h" 8 | 9 | namespace trader { 10 | 11 | // Returns a new instance of TraderEmitter for the given trader_name. 12 | std::unique_ptr GetTrader(absl::string_view trader_name); 13 | 14 | // Returns batch of TraderEmitters for the given trader_name. 15 | std::vector> GetBatchOfTraders( 16 | absl::string_view trader_name); 17 | 18 | } // namespace trader 19 | 20 | #endif // TRADERS_TRADER_FACTORY_H 21 | -------------------------------------------------------------------------------- /util/BUILD: -------------------------------------------------------------------------------- 1 | package(default_visibility = ["//visibility:public"]) 2 | 3 | load("@rules_proto//proto:defs.bzl", "proto_library") 4 | 5 | proto_library( 6 | name = "example_proto", 7 | srcs = ["example.proto"], 8 | ) 9 | 10 | cc_proto_library( 11 | name = "example_cc_proto", 12 | deps = [":example_proto"], 13 | ) 14 | 15 | cc_library( 16 | name = "time", 17 | srcs = ["time.cc"], 18 | hdrs = ["time.h"], 19 | deps = [ 20 | "@com_google_absl//absl/status:statusor", 21 | "@com_google_absl//absl/strings", 22 | "@com_google_absl//absl/time", 23 | ], 24 | ) 25 | 26 | cc_test( 27 | name = "time_test", 28 | srcs = ["time_test.cc"], 29 | deps = [ 30 | ":time", 31 | "@com_google_googletest//:gtest_main", 32 | ], 33 | ) 34 | 35 | cc_library( 36 | name = "proto", 37 | hdrs = ["proto.h"], 38 | deps = [ 39 | "@com_google_absl//absl/status", 40 | "@com_google_absl//absl/status:statusor", 41 | "@com_google_absl//absl/strings", 42 | "@com_google_protobuf//:protobuf_lite", 43 | ], 44 | ) 45 | 46 | cc_test( 47 | name = "proto_test", 48 | srcs = ["proto_test.cc"], 49 | deps = [ 50 | ":example_cc_proto", 51 | ":proto", 52 | "@com_google_googletest//:gtest_main", 53 | ], 54 | ) 55 | -------------------------------------------------------------------------------- /util/example.proto: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | syntax = "proto2"; 4 | 5 | package trader; 6 | 7 | // Base (crypto) currency traded at at a specific price and time. 8 | message PriceRecord { 9 | // UNIX timestamp (in seconds). 10 | optional int64 timestamp_sec = 1; 11 | // Base (crypto) currency price (e.g. BTC/USD price). 12 | optional float price = 2; 13 | // Traded base (crypto) currency volume. 14 | optional float volume = 3; 15 | } 16 | 17 | // Open, high, low, and close prices for specific time interval. 18 | // The duration of the time interval is assumed implicitly. 19 | message OhlcTick { 20 | // UNIX timestamp (in seconds) of the start of the time interval. 21 | optional int64 timestamp_sec = 1; 22 | // Opening base (crypto) currency price at the start of the time interval. 23 | optional float open = 2; 24 | // Highest base (crypto) currency price during the time interval. 25 | optional float high = 3; 26 | // Lowest base (crypto) currency price during the time interval. 27 | optional float low = 4; 28 | // Closing base (crypto) currency price at the end of the time interval. 29 | optional float close = 5; 30 | // Total traded volume during the time interval. 31 | optional float volume = 6; 32 | } 33 | -------------------------------------------------------------------------------- /util/proto.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef UTIL_PROTO_H 4 | #define UTIL_PROTO_H 5 | 6 | #include 7 | #ifdef HAVE_ZLIB 8 | #include 9 | #endif 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "absl/status/status.h" 23 | #include "absl/status/statusor.h" 24 | #include "absl/strings/str_format.h" 25 | 26 | namespace trader { 27 | // Signal returned by the reader (see below). 28 | enum class ReaderSignal { kContinue, kBreak }; 29 | using ReaderStatus = absl::StatusOr; 30 | 31 | // Reads delimited messages from the (compressed) input stream and applies the 32 | // function "reader" on them. 33 | template 34 | absl::Status ReadDelimitedMessagesFromIStream( 35 | std::istream& stream, std::function reader) { 36 | google::protobuf::io::IstreamInputStream in_stream(&stream); 37 | #ifdef HAVE_ZLIB 38 | google::protobuf::io::GzipInputStream gzip_stream(&in_stream); 39 | #else 40 | google::protobuf::io::ZeroCopyInputStream& gzip_stream = in_stream; 41 | #endif 42 | while (true) { 43 | T message; 44 | bool clean_eof; 45 | bool parsed = google::protobuf::util::ParseDelimitedFromZeroCopyStream( 46 | &message, &gzip_stream, &clean_eof); 47 | if (!parsed) { 48 | if (clean_eof) { 49 | return absl::OkStatus(); 50 | } 51 | return absl::InvalidArgumentError("Cannot parse the input stream"); 52 | } 53 | ReaderStatus reader_status = reader(message); 54 | if (!reader_status.ok()) { 55 | return absl::AbortedError(reader_status.status().message()); 56 | } 57 | switch (reader_status.value()) { 58 | case ReaderSignal::kContinue: 59 | break; 60 | case ReaderSignal::kBreak: 61 | return absl::OkStatus(); 62 | } 63 | } 64 | return absl::OkStatus(); 65 | } 66 | 67 | // Reads delimited messages from the (compressed) input stream. 68 | template 69 | absl::Status ReadDelimitedMessagesFromIStream(std::istream& stream, 70 | std::vector& messages) { 71 | return ReadDelimitedMessagesFromIStream( 72 | stream, [&messages](const T& message) -> ReaderStatus { 73 | messages.push_back(message); 74 | return ReaderSignal::kContinue; 75 | }); 76 | } 77 | 78 | // Reads delimited messages from the (compressed) input file and applies the 79 | // function "reader" on them. 80 | template 81 | absl::Status ReadDelimitedMessagesFromFile( 82 | const std::string& file_name, 83 | std::function reader) { 84 | std::fstream in_fstream(file_name, std::ios::in | std::ios::binary); 85 | if (!in_fstream) { 86 | return absl::InvalidArgumentError( 87 | absl::StrFormat("Cannot open the input file: %s", file_name)); 88 | } 89 | return ReadDelimitedMessagesFromIStream(in_fstream, reader); 90 | } 91 | 92 | // Reads delimited messages from the (compressed) input file. 93 | template 94 | absl::Status ReadDelimitedMessagesFromFile(const std::string& file_name, 95 | std::vector& messages) { 96 | return ReadDelimitedMessagesFromFile( 97 | file_name, [&messages](const T& message) -> ReaderStatus { 98 | messages.push_back(message); 99 | return ReaderSignal::kContinue; 100 | }); 101 | } 102 | 103 | // Writes (and compresses) delimited messages to the output file. 104 | template 105 | absl::Status WriteDelimitedMessagesToOStream(InputIterator first, 106 | InputIterator last, 107 | std::ostream& stream, 108 | bool compress) { 109 | google::protobuf::io::OstreamOutputStream out_stream(&stream); 110 | #ifdef HAVE_ZLIB 111 | google::protobuf::io::GzipOutputStream::Options options; 112 | options.format = google::protobuf::io::GzipOutputStream::GZIP; 113 | options.compression_level = compress ? Z_DEFAULT_COMPRESSION : 0; 114 | google::protobuf::io::GzipOutputStream gzip_stream(&out_stream, options); 115 | #else 116 | google::protobuf::io::ZeroCopyOutputStream& gzip_stream = out_stream; 117 | #endif 118 | while (first != last) { 119 | bool serialized = 120 | google::protobuf::util::SerializeDelimitedToZeroCopyStream( 121 | *first, &gzip_stream); 122 | if (!serialized) { 123 | return absl::InvalidArgumentError(absl::StrFormat( 124 | "Cannot serialize the message:\n%s", first->DebugString())); 125 | } 126 | ++first; 127 | } 128 | return absl::OkStatus(); 129 | } 130 | 131 | // Writes (and compresses) delimited messages to the output file. 132 | template 133 | absl::Status WriteDelimitedMessagesToFile(InputIterator first, 134 | InputIterator last, 135 | const std::string& file_name, 136 | bool compress) { 137 | std::fstream out_fstream(file_name, 138 | std::ios::out | std::ios::trunc | std::ios::binary); 139 | if (!out_fstream) { 140 | return absl::InvalidArgumentError( 141 | absl::StrFormat("Cannot open the input file: %s", file_name)); 142 | } 143 | return WriteDelimitedMessagesToOStream(first, last, out_fstream, compress); 144 | } 145 | 146 | } // namespace trader 147 | 148 | #endif // UTIL_PROTO_H 149 | -------------------------------------------------------------------------------- /util/proto_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "util/proto.h" 4 | 5 | #include 6 | 7 | #include "gtest/gtest.h" 8 | #include "util/example.pb.h" 9 | 10 | namespace trader { 11 | 12 | TEST(ReadWriteDelimitedTest, ReadWriteEmptyStream) { 13 | std::istringstream iss; 14 | std::vector messages; 15 | ASSERT_TRUE(ReadDelimitedMessagesFromIStream(iss, messages).ok()); 16 | ASSERT_EQ(messages.size(), 0); 17 | } 18 | 19 | TEST(ReadWriteDelimitedTest, ReadWriteSinglePriceRecord) { 20 | std::ostringstream oss; 21 | std::vector input_messages; 22 | input_messages.emplace_back(); 23 | input_messages.back().set_timestamp_sec(1483228800); 24 | input_messages.back().set_price(700.0f); 25 | input_messages.back().set_volume(1.5e4f); 26 | ASSERT_TRUE(WriteDelimitedMessagesToOStream(input_messages.begin(), 27 | input_messages.end(), oss, 28 | /*compress=*/true) 29 | .ok()); 30 | std::istringstream iss(oss.str()); 31 | std::vector messages; 32 | ASSERT_TRUE(ReadDelimitedMessagesFromIStream(iss, messages).ok()); 33 | ASSERT_EQ(messages.size(), 1); 34 | EXPECT_EQ(messages[0].timestamp_sec(), 1483228800); 35 | EXPECT_FLOAT_EQ(messages[0].price(), 700.0f); 36 | EXPECT_FLOAT_EQ(messages[0].volume(), 1.5e4f); 37 | } 38 | 39 | TEST(ReadWriteDelimitedTest, ReadWriteMultipleOhlcTicks) { 40 | static constexpr int kNumTicks = 10; 41 | std::ostringstream oss; 42 | std::vector input_messages; 43 | for (int i = 0; i < kNumTicks; ++i) { 44 | input_messages.emplace_back(); 45 | input_messages.back().set_open(100.0f + 10.0f * i); 46 | input_messages.back().set_high(120.0f + 20.0f * i); 47 | input_messages.back().set_low(80.0f + 10.0f * i); 48 | input_messages.back().set_close(110.0f + 10.0f * i); 49 | input_messages.back().set_volume(1.5e4f + 1.0e3f * i); 50 | } 51 | ASSERT_TRUE(WriteDelimitedMessagesToOStream(input_messages.begin(), 52 | input_messages.end(), oss, 53 | /*compress=*/true) 54 | .ok()); 55 | std::istringstream iss(oss.str()); 56 | std::vector messages; 57 | ASSERT_TRUE(ReadDelimitedMessagesFromIStream(iss, messages).ok()); 58 | ASSERT_EQ(messages.size(), kNumTicks); 59 | for (int i = 0; i < kNumTicks; ++i) { 60 | EXPECT_FLOAT_EQ(messages[i].open(), 100.0f + 10.0f * i); 61 | EXPECT_FLOAT_EQ(messages[i].high(), 120.0f + 20.0f * i); 62 | EXPECT_FLOAT_EQ(messages[i].low(), 80.0f + 10.0f * i); 63 | EXPECT_FLOAT_EQ(messages[i].close(), 110.0f + 10.0f * i); 64 | EXPECT_FLOAT_EQ(messages[i].volume(), 1.5e4f + 1.0e3f * i); 65 | } 66 | } 67 | 68 | } // namespace trader 69 | -------------------------------------------------------------------------------- /util/time.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "util/time.h" 4 | 5 | #include 6 | 7 | #include "absl/strings/str_cat.h" 8 | 9 | namespace trader { 10 | namespace { 11 | const char* const kAllowedTimeFormats[]{ 12 | absl::RFC3339_sec, "%Y-%m-%d %H:%M:%S %Ez", 13 | "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %Ez", 14 | "%Y-%m-%d", 15 | }; 16 | const size_t kNumberOfAllowedTimeFormats = 17 | std::extent::value; 18 | } // namespace 19 | 20 | absl::StatusOr ParseTime(absl::string_view datetime) { 21 | absl::Time time; 22 | for (size_t i = 0; i < kNumberOfAllowedTimeFormats; ++i) { 23 | if (absl::ParseTime(kAllowedTimeFormats[i], datetime, &time, 24 | /*err=*/nullptr)) { 25 | return time; 26 | } 27 | } 28 | 29 | return absl::InvalidArgumentError( 30 | absl::StrCat("Cannot parse datetime: ", datetime)); 31 | } 32 | 33 | std::string FormatTimeUTC(absl::Time time) { 34 | return absl::FormatTime("%Y-%m-%d %H:%M:%S", time, absl::UTCTimeZone()); 35 | } 36 | 37 | absl::Time AddMonthsToTime(absl::Time time, int months) { 38 | const auto cs = absl::ToCivilSecond(time, absl::UTCTimeZone()); 39 | return absl::FromCivil( 40 | absl::CivilSecond(cs.year(), cs.month() + months, cs.day(), // nowrap 41 | cs.hour(), cs.minute(), cs.second()), 42 | absl::UTCTimeZone()); 43 | } 44 | 45 | int64_t AddMonthsToTimestampSec(int64_t timestamp_sec, int months) { 46 | return absl::ToUnixSeconds( 47 | AddMonthsToTime(absl::FromUnixSeconds(timestamp_sec), months)); 48 | } 49 | 50 | } // namespace trader 51 | -------------------------------------------------------------------------------- /util/time.h: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #ifndef UTIL_TIME_H 4 | #define UTIL_TIME_H 5 | 6 | #include "absl/status/statusor.h" 7 | #include "absl/strings/string_view.h" 8 | #include "absl/time/time.h" 9 | 10 | namespace trader { 11 | 12 | // Parses the input datetime string to absl::Time. 13 | // Supports ISO 8601 format for date and time with UTC offset. 14 | // Supported formats: 15 | // %Y-%m-%d (defaults to UTC timezone) 16 | // %Y-%m-%d %Ez (%Ez is RFC3339-compatible UTC offset (+hh:mm or -hh:mm)) 17 | // %Y-%m-%d %H:%M:%S (defaults to UTC timezone) 18 | // %Y-%m-%d %H:%M:%S %Ez (e.g. 1970-01-01 00:00:00 +00:00) 19 | // %Y-%m-%d%ET%H:%M:%S%Ez (e.g. 1970-01-01T00:00:00+00:00) 20 | absl::StatusOr ParseTime(absl::string_view datetime); 21 | 22 | // Returns time formated as: %Y-%m-%d %H:%M:%S (in the UTC timezone). 23 | std::string FormatTimeUTC(absl::Time time); 24 | 25 | // Adds specified number of months to the given absl::Time. 26 | absl::Time AddMonthsToTime(absl::Time time, int months); 27 | 28 | // Adds specified number of months to the given UNIX timestamp (in sec). 29 | int64_t AddMonthsToTimestampSec(int64_t timestamp_sec, int months); 30 | 31 | } // namespace trader 32 | 33 | #endif // UTIL_TIME_H 34 | -------------------------------------------------------------------------------- /util/time_test.cc: -------------------------------------------------------------------------------- 1 | // Copyright © 2021 Peter Cerno. All rights reserved. 2 | 3 | #include "util/time.h" 4 | 5 | #include "gtest/gtest.h" 6 | 7 | namespace trader { 8 | namespace { 9 | const absl::TimeZone utc = absl::UTCTimeZone(); 10 | } // namespace 11 | 12 | TEST(ParseTimeTest, Basic_Y_m_d_Format) { 13 | absl::StatusOr time_status = ParseTime("Hello World!"); 14 | EXPECT_EQ(time_status.status(), 15 | absl::Status(absl::StatusCode::kInvalidArgument, 16 | "Cannot parse datetime: Hello World!")); 17 | 18 | time_status = ParseTime("1970-01-01"); 19 | ASSERT_TRUE(time_status.ok()); 20 | EXPECT_EQ(absl::FormatTime(time_status.value(), utc), 21 | "1970-01-01T00:00:00+00:00"); 22 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "1970-01-01 00:00:00"); 23 | 24 | time_status = ParseTime("2000-02-29"); 25 | ASSERT_TRUE(time_status.ok()); 26 | EXPECT_EQ(absl::ToUnixSeconds(time_status.value()), 951782400); 27 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2000-02-29 00:00:00"); 28 | 29 | time_status = ParseTime("2017-01-01"); 30 | ASSERT_TRUE(time_status.ok()); 31 | EXPECT_EQ(absl::ToUnixSeconds(time_status.value()), 1483228800); 32 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2017-01-01 00:00:00"); 33 | 34 | time_status = ParseTime("2017-01-01 +02:00"); 35 | ASSERT_TRUE(time_status.ok()); 36 | EXPECT_EQ(absl::ToUnixSeconds(time_status.value()), 1483221600); 37 | 38 | EXPECT_FALSE(ParseTime("2017-00-01").ok()); 39 | EXPECT_FALSE(ParseTime("2017-13-01").ok()); 40 | EXPECT_FALSE(ParseTime("2017-01-00").ok()); 41 | EXPECT_FALSE(ParseTime("2017-01-32").ok()); 42 | } 43 | 44 | TEST(ParseTimeTest, Extended_Y_m_d_H_M_S_Format) { 45 | absl::StatusOr time_status = ParseTime("1970-01-01 00:00:00"); 46 | ASSERT_TRUE(time_status.ok()); 47 | EXPECT_EQ(absl::FormatTime(time_status.value(), utc), 48 | "1970-01-01T00:00:00+00:00"); 49 | 50 | time_status = ParseTime("2000-02-29 00:00:00"); 51 | ASSERT_TRUE(time_status.ok()); 52 | EXPECT_EQ(absl::ToUnixSeconds(time_status.value()), 951782400); 53 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2000-02-29 00:00:00"); 54 | 55 | time_status = ParseTime("2017-01-01 00:05:00"); 56 | ASSERT_TRUE(time_status.ok()); 57 | EXPECT_EQ(absl::ToUnixSeconds(time_status.value()), 1483229100); 58 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2017-01-01 00:05:00"); 59 | 60 | time_status = ParseTime("2017-01-01 16:25:15 +02:00"); 61 | ASSERT_TRUE(time_status.ok()); 62 | EXPECT_EQ(absl::FormatTime(time_status.value(), utc), 63 | "2017-01-01T14:25:15+00:00"); 64 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2017-01-01 14:25:15"); 65 | 66 | EXPECT_FALSE(ParseTime("2017-01-01 24:00:00").ok()); 67 | EXPECT_FALSE(ParseTime("2017-01-01 00:60:00").ok()); 68 | EXPECT_FALSE(ParseTime("2017-01-01 00:00:70").ok()); 69 | } 70 | 71 | TEST(ParseTimeTest, RFC3339_Format) { 72 | absl::StatusOr time_status = 73 | ParseTime("2017-01-01T16:25:15+02:00"); 74 | ASSERT_TRUE(time_status.ok()); 75 | EXPECT_EQ(absl::FormatTime(time_status.value(), utc), 76 | "2017-01-01T14:25:15+00:00"); 77 | EXPECT_EQ(FormatTimeUTC(time_status.value()), "2017-01-01 14:25:15"); 78 | } 79 | 80 | TEST(AddMonthsToTimeTest, Basic) { 81 | absl::StatusOr time_status = ParseTime("2021-03-01"); 82 | ASSERT_TRUE(time_status.ok()); 83 | 84 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 0), utc), 85 | "2021-03-01T00:00:00+00:00"); 86 | EXPECT_EQ(AddMonthsToTimestampSec(1614556800, 0), 1614556800); 87 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 1), utc), 88 | "2021-04-01T00:00:00+00:00"); 89 | EXPECT_EQ(AddMonthsToTimestampSec(1614556800, 1), 1617235200); 90 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 15), utc), 91 | "2022-06-01T00:00:00+00:00"); 92 | EXPECT_EQ(AddMonthsToTimestampSec(1614556800, 15), 1654041600); 93 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), -1), utc), 94 | "2021-02-01T00:00:00+00:00"); 95 | EXPECT_EQ(AddMonthsToTimestampSec(1614556800, -1), 1612137600); 96 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), -3), utc), 97 | "2020-12-01T00:00:00+00:00"); 98 | EXPECT_EQ(AddMonthsToTimestampSec(1614556800, -3), 1606780800); 99 | 100 | time_status = ParseTime("2017-01-01T16:25:15+02:00"); 101 | ASSERT_TRUE(time_status.ok()); 102 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 0), utc), 103 | "2017-01-01T14:25:15+00:00"); 104 | EXPECT_EQ(AddMonthsToTimestampSec(1483280715, 0), 1483280715); 105 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 5), utc), 106 | "2017-06-01T14:25:15+00:00"); 107 | EXPECT_EQ(AddMonthsToTimestampSec(1483280715, 5), 1496327115); 108 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), 12), utc), 109 | "2018-01-01T14:25:15+00:00"); 110 | EXPECT_EQ(AddMonthsToTimestampSec(1483280715, 12), 1514816715); 111 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), -1), utc), 112 | "2016-12-01T14:25:15+00:00"); 113 | EXPECT_EQ(AddMonthsToTimestampSec(1483280715, -1), 1480602315); 114 | EXPECT_EQ(absl::FormatTime(AddMonthsToTime(time_status.value(), -12), utc), 115 | "2016-01-01T14:25:15+00:00"); 116 | EXPECT_EQ(AddMonthsToTimestampSec(1483280715, -12), 1451658315); 117 | } 118 | 119 | } // namespace trader 120 | --------------------------------------------------------------------------------