diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dad1443b..f259129d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -125,7 +125,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -184,6 +184,17 @@ jobs: run: | ./tests/test_docs.sh + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Documentation build + run: | + pip install -r docs/requirements-docs.txt + pip install mkdocs + mkdocs build + - name: Slack Notification uses: homoluctus/slatify@v1.8.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) @@ -224,7 +235,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 diff --git a/config.json.example b/config.json.example index ab517b77c..af45dac74 100644 --- a/config.json.example +++ b/config.json.example @@ -5,15 +5,15 @@ "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "timeframe": "5m", - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, diff --git a/config_full.json.example b/config_full.json.example index 659580fb1..45c5c695c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -7,7 +7,7 @@ "amount_reserve_percent": 0.05, "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "timeframe": "5m", "trailing_stop": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index fd0b2b95d..5f3b57854 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -27,12 +27,11 @@ "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false - }, "exchange": { "name": "kraken", - "key": "", - "secret": "", + "key": "your_exchange_key", + "secret": "your_exchange_key", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": true, diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 1843efdcb..40bc72bc5 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -5,6 +5,3 @@ FROM freqtradeorg/freqtrade:${sourceimage} COPY requirements-plot.txt /freqtrade/ RUN pip install -r requirements-plot.txt --no-cache-dir - -# Empty the ENTRYPOINT to allow all commands -ENTRYPOINT [] diff --git a/docs/bot-usage.md b/docs/bot-usage.md index a07a34b94..4d07435c7 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -353,7 +353,7 @@ optional arguments: class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: DefaultHyperOptLoss, + Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily. diff --git a/docs/configuration.md b/docs/configuration.md index d6e26f80e..f8e8aabcd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -574,144 +574,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t ``` Obviously, if only one side is using limit orders, different pricing combinations can be used. - -## Pairlists and Pairlist Handlers - -Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. - -In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). - -Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. - -If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. - -Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. - -### Available Pairlist Handlers - -* [`StaticPairList`](#static-pair-list) (default, if not configured differently) -* [`VolumePairList`](#volume-pair-list) -* [`AgeFilter`](#agefilter) -* [`PrecisionFilter`](#precisionfilter) -* [`PriceFilter`](#pricefilter) -* [`ShuffleFilter`](#shufflefilter) -* [`SpreadFilter`](#spreadfilter) - -!!! Tip "Testing pairlists" - Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility subcommand to test your configuration quickly. - -#### Static Pair List - -By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. - -It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. - -```json -"pairlists": [ - {"method": "StaticPairList"} - ], -``` - -#### Volume Pair List - -`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). - -When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. - -When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. - -The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). - -`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: - -* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours. - -```json -"pairlists": [{ - "method": "VolumePairList", - "number_assets": 20, - "sort_key": "quoteVolume", - "refresh_period": 1800, -}], -``` - -#### AgeFilter - -Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). - -When pairs are first listed on an exchange they can suffer huge price drops and volatility -in the first few days while the pair goes through its price-discovery period. Bots can often -be caught out buying before the pair has finished dropping in price. - -This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. - -#### PrecisionFilter - -Filters low-value coins which would not allow setting stoplosses. - -#### PriceFilter - -The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: - -* `min_price` -* `max_price` -* `low_price_ratio` - -The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. -This option is disabled by default, and will only apply if set to > 0. - -The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. -This option is disabled by default, and will only apply if set to > 0. - -The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. -This option is disabled by default, and will only apply if set to > 0. - -For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. - -Calculation example: - -Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. - -!!! Warning "Low priced pairs" - Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding. - -#### ShuffleFilter - -Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. - -!!! Tip - You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. - -#### SpreadFilter - -Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`). - -Example: - -If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. - -### Full example of Pairlist Handlers - -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 priceunit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. - -```json -"exchange": { - "pair_whitelist": [], - "pair_blacklist": ["BNB/BTC"] -}, -"pairlists": [ - { - "method": "VolumePairList", - "number_assets": 20, - "sort_key": "quoteVolume", - }, - {"method": "AgeFilter", "min_days_listed": 10}, - {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter", "seed": 42} - ], -``` +--8<-- "includes/pairlists.md" ## Switch to Dry-run mode diff --git a/docs/developer.md b/docs/developer.md index 788e961cd..8ef816d5d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -96,7 +96,7 @@ Below is an outline of exception inheritance hierarchy: ## Modules -### Dynamic Pairlist +### Pairlists You have a great idea for a new pair selection algorithm you would like to try out? Great. Hopefully you also want to contribute this back upstream. diff --git a/docs/edge.md b/docs/edge.md index 500c3c833..7442f1927 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,20 +82,34 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
- Your potential profit is calculated as:
+ Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10). + + Your potential profit is calculated as: + $\begin{aligned} - \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ - &= (15 - 10) * \frac{100}{15}\\ - &= 33.33 - \end{aligned}$
- Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:
+ \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\ + &= (15 - 10) * (100 / 10) \\ + &= 50 + \end{aligned}$ + + Since the price might go to $0, the $100 dollars invested could turn into 0. + + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). + + $\begin{aligned} + \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\ + &= (10 - 8.5) * (100 / 10)\\ + &= 15 + \end{aligned}$ + + We can compute the Risk Reward Ratio as follows: + $\begin{aligned} R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ - &= \frac{33.33}{100}\\ - &= 0.333... + &= \frac{50}{15}\\ + &= 3.33 \end{aligned}$
- What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. + What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 91bc32e48..fc7a0dd93 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -221,7 +221,7 @@ This class should be in its own file within the `user_data/hyperopts/` directory Currently, the following loss functions are builtin: -* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md new file mode 100644 index 000000000..ae4ec818d --- /dev/null +++ b/docs/includes/pairlists.md @@ -0,0 +1,137 @@ +## Pairlists and Pairlist Handlers + +Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. + +In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). + +Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. + +If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. + +Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. + +### Available Pairlist Handlers + +* [`StaticPairList`](#static-pair-list) (default, if not configured differently) +* [`VolumePairList`](#volume-pair-list) +* [`AgeFilter`](#agefilter) +* [`PrecisionFilter`](#precisionfilter) +* [`PriceFilter`](#pricefilter) +* [`ShuffleFilter`](#shufflefilter) +* [`SpreadFilter`](#spreadfilter) + +!!! Tip "Testing pairlists" + Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. + +#### Static Pair List + +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. + +It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. + +```json +"pairlists": [ + {"method": "StaticPairList"} + ], +``` + +#### Volume Pair List + +`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). + +When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. + +When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. + +The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). + +`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: + +* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours. + +```json +"pairlists": [{ + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + "refresh_period": 1800, +}], +``` + +#### AgeFilter + +Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). + +When pairs are first listed on an exchange they can suffer huge price drops and volatility +in the first few days while the pair goes through its price-discovery period. Bots can often +be caught out buying before the pair has finished dropping in price. + +This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. + +#### PrecisionFilter + +Filters low-value coins which would not allow setting stoplosses. + +#### PriceFilter + +The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: + +* `min_price` +* `max_price` +* `low_price_ratio` + +The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. +This option is disabled by default, and will only apply if set to > 0. + +The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. +This option is disabled by default, and will only apply if set to > 0. + +The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. +This option is disabled by default, and will only apply if set to > 0. + +For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. + +Calculation example: + +Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. + +!!! Warning "Low priced pairs" + Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding. + +#### ShuffleFilter + +Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. + +!!! Tip + You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. + +#### SpreadFilter + +Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`). + +Example: + +If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. + +### Full example of Pairlist Handlers + +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. + +```json +"exchange": { + "pair_whitelist": [], + "pair_blacklist": ["BNB/BTC"] +}, +"pairlists": [ + { + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + }, + {"method": "AgeFilter", "min_days_listed": 10}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + {"method": "ShuffleFilter", "seed": 42} + ], +``` diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 66225d6d4..69ae33649 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,3 @@ mkdocs-material==6.0.2 mdx_truly_sane_lists==1.2 +pymdown-extensions==8.0.1 diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 14d5fcd84..a6cdef864 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -312,12 +312,17 @@ The name of the variable can be chosen at will, but should be prefixed with `cus class Awesomestrategy(IStrategy): # Create custom dictionary cust_info = {} + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Check if the entry already exists + if not metadata["pair"] in self._cust_info: + # Create empty entry for this pair + self._cust_info[metadata["pair"]] = {} + if "crosstime" in self.cust_info[metadata["pair"]: - self.cust_info[metadata["pair"]["crosstime"] += 1 + self.cust_info[metadata["pair"]]["crosstime"] += 1 else: - self.cust_info[metadata["pair"]["crosstime"] = 1 + self.cust_info[metadata["pair"]]["crosstime"] = 1 ``` !!! Warning diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f991f6a4d..8ea945ae7 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -257,8 +257,8 @@ AVAILABLE_CLI_OPTIONS = { help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, ' - 'SortinoHyperOptLoss, SortinoHyperOptLossDaily.', + 'ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, ' + 'SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily.', metavar='NAME', ), "hyperoptexportfilename": Arg( diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e81ecf871..9e6076dfb 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -205,14 +205,14 @@ def start_show_trades(args: Dict[str, Any]) -> None: """ import json - from freqtrade.persistence import Trade, init + from freqtrade.persistence import Trade, init_db config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: raise OperationalException("--db-url is required for this command.") logger.info(f'Using DB: "{config["db_url"]}"') - init(config['db_url'], clean_open_orders=False) + init_db(config['db_url'], clean_open_orders=False) tfilter = [] if config.get('trade_ids'): diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6af685712..513fba9e7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -9,10 +9,9 @@ from typing import Any, Dict, Optional, Tuple, Union import numpy as np import pandas as pd -from freqtrade import persistence from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db logger = logging.getLogger(__name__) @@ -218,7 +217,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF Can also serve as protection to load the correct result. :return: Dataframe containing Trades """ - persistence.init(db_url, clean_open_orders=False) + init_db(db_url, clean_open_orders=False) columns = ["pair", "open_date", "close_date", "profit", "profit_percent", "open_rate", "close_rate", "amount", "trade_duration", "sell_reason", diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 6a95ad91f..a40b63d67 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -310,8 +310,10 @@ class Edge: # Calculating number of losing trades, average win and average loss df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades'] - df['average_win'] = df['profit_sum'] / df['nb_win_trades'] - df['average_loss'] = df['loss_sum'] / df['nb_loss_trades'] + df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0, + df['profit_sum'] / df['nb_win_trades']) + df['average_loss'] = np.where(df['nb_loss_trades'] == 0, 0.0, + df['loss_sum'] / df['nb_loss_trades']) # Win rate = number of profitable trades / number of trades df['winrate'] = df['nb_win_trades'] / df['nb_trades'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index cbcf961bc..5b58d7a95 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance +from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b85802aad..099f282a2 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -20,20 +20,9 @@ class Binance(Exchange): "order_time_in_force": ['gtc', 'fok', 'ioc'], "trades_pagination": "id", "trades_pagination_arg": "fromId", + "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: - """ - get order book level 2 from exchange - - 20180619: binance support limits but only on specific range - """ - limit_range = [5, 10, 20, 50, 100, 500, 1000] - # get next-higher step in the limit_range list - limit = min(list(filter(lambda x: limit <= x, limit_range))) - - return super().fetch_l2_order_book(pair, limit) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py new file mode 100644 index 000000000..4318f9cf0 --- /dev/null +++ b/freqtrade/exchange/bittrex.py @@ -0,0 +1,23 @@ +""" Bittrex exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bittrex(Exchange): + """ + Bittrex exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [1, 25, 500], + } diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bbb94e61f..c0d737f26 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -53,7 +53,7 @@ class Exchange: "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", - + "l2_limit_range": None, } _ft_has: Dict = {} @@ -1069,6 +1069,16 @@ class Exchange: return self.fetch_stoploss_order(order_id, pair) return self.fetch_order(order_id, pair) + @staticmethod + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + """ + Get next greater value in the list. + Used by fetch_l2_order_book if the api only supports a limited range + """ + if not limit_range: + return limit + return min([x for x in limit_range if limit <= x] + [max(limit_range)]) + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ @@ -1077,9 +1087,10 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) try: - return self._api.fetch_l2_order_book(pair, limit) + return self._api.fetch_l2_order_book(pair, limit1) except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2bdd8da4b..cfc68a3ec 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from freqtrade import __version__, constants, persistence +from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, Trade, cleanup_db, init_db from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -58,8 +58,8 @@ class FreqtradeBot: # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. - self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) + self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) @@ -68,7 +68,7 @@ class FreqtradeBot: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) self.wallets = Wallets(self.config, self.exchange) @@ -123,7 +123,7 @@ class FreqtradeBot: self.check_for_open_trades() self.rpc.cleanup() - persistence.cleanup() + cleanup_db() def startup(self) -> None: """ diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 071693f8d..359d0d0e4 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -56,8 +56,8 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = if log: logger.info(f'dumping json to "{filename}"') - with gzip.open(filename, 'w') as fp: - rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) + with gzip.open(filename, 'w') as fpz: + rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE) else: if log: logger.info(f'dumping json to "{filename}"') diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 9e780d0ea..9dbdc4403 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -1,5 +1,5 @@ """ -DefaultHyperOptLoss +ShortTradeDurHyperOptLoss This module defines the default HyperoptLoss class which is being used for Hyperoptimization. """ @@ -26,7 +26,7 @@ EXPECTED_MAX_PROFIT = 3.0 MAX_ACCEPTED_TRADE_DURATION = 300 -class DefaultHyperOptLoss(IHyperOptLoss): +class ShortTradeDurHyperOptLoss(IHyperOptLoss): """ Defines the default loss function for hyperopt """ @@ -50,3 +50,7 @@ class DefaultHyperOptLoss(IHyperOptLoss): duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = trade_loss + profit_loss + duration_loss return result + + +# Create an alias for This to allow the legacy Method to work as well. +DefaultHyperOptLoss = ShortTradeDurHyperOptLoss diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 67a96cc60..6b5bd11e7 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -36,7 +36,7 @@ class IPairList(ABC): self._pairlist_pos = pairlist_pos self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._last_refresh = 0 - self._log_cache = TTLCache(maxsize=1024, ttl=self.refresh_period) + self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) @property def name(self) -> str: diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index ee2e40267..a3ec13e98 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup, init +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8455a3b77..e5acbf937 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -29,7 +29,7 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(db_url: str, clean_open_orders: bool = False) -> None: +def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers @@ -72,7 +72,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: clean_dry_run_db() -def cleanup() -> None: +def cleanup_db() -> None: """ Flushes all pending operations to disk. :return: None @@ -399,7 +399,7 @@ class Trade(_DECL_BASE): self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup() + cleanup_db() def close(self, rate: float) -> None: """ diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4e262b1ec..f31d7b0b5 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -563,7 +563,7 @@ class ApiServer(RPC): config.update({ 'strategy': strategy, }) - results = self._rpc_analysed_history_full(config, pair, timeframe, timerange) + results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) return jsonify(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b89284acf..911b2d731 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -656,8 +656,9 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _convert_dataframe_to_dict(self, strategy: str, pair: str, timeframe: str, - dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: + @staticmethod + def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame, + last_analyzed: datetime) -> Dict[str, Any]: has_content = len(dataframe) != 0 buy_signals = 0 sell_signals = 0 @@ -711,7 +712,8 @@ class RPC: return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed) - def _rpc_analysed_history_full(self, config, pair: str, timeframe: str, + @staticmethod + def _rpc_analysed_history_full(config, pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(timerange) @@ -726,8 +728,8 @@ class RPC: strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) - return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, - df_analyzed, arrow.Arrow.utcnow().datetime) + return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, + df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: diff --git a/mkdocs.yml b/mkdocs.yml index 26494ae45..8d1ce1cfe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,16 +55,16 @@ markdown_extensions: permalink: true - pymdownx.arithmatex: generic: true - - pymdownx.caret - - pymdownx.critic - pymdownx.details - pymdownx.inlinehilite - pymdownx.magiclink - - pymdownx.mark + - pymdownx.pathconverter - pymdownx.smartsymbols + - pymdownx.snippets: + base_path: docs + check_paths: true - pymdownx.tabbed - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - - pymdownx.tilde - mdx_truly_sane_lists diff --git a/requirements-dev.txt b/requirements-dev.txt index 0710882a4..916bb2ec2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,13 +7,13 @@ coveralls==2.1.2 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.782 +mypy==0.790 pytest==6.1.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 -isort==5.5.4 +isort==5.6.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.6 +nbconvert==6.0.7 diff --git a/requirements.txt b/requirements.txt index 51313c32c..35f1e4c18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,12 @@ numpy==1.19.2 -pandas==1.1.2 +pandas==1.1.3 -ccxt==1.35.22 -SQLAlchemy==1.3.19 -python-telegram-bot==12.8 -arrow==0.16.0 +ccxt==1.36.12 +multidict==4.7.6 +aiohttp==3.6.3 +SQLAlchemy==1.3.20 +python-telegram-bot==13.0 +arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 urllib3==1.25.10 @@ -32,7 +34,7 @@ flask-jwt-extended==3.24.1 flask-cors==3.0.9 # Support for colorized terminal output -colorama==0.4.3 +colorama==0.4.4 # Building config files interactively questionary==1.6.0 -prompt-toolkit==3.0.7 +prompt-toolkit==3.0.8 diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 5b125697c..713386a8e 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1149,7 +1149,7 @@ def test_start_list_data(testdatadir, capsys): @pytest.mark.usefixtures("init_persistence") def test_show_trades(mocker, fee, capsys, caplog): - mocker.patch("freqtrade.persistence.init") + mocker.patch("freqtrade.persistence.init_db") create_mock_trades(fee) args = [ "show-trades", diff --git a/tests/conftest.py b/tests/conftest.py index 2153fd327..520b53b31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,13 +13,13 @@ import numpy as np import pytest from telegram import Chat, Message, Update -from freqtrade import constants, persistence +from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -131,7 +131,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - persistence.init(config['db_url']) + init_db(config['db_url']) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -219,7 +219,7 @@ def patch_coingekko(mocker) -> None: @pytest.fixture(scope='function') def init_persistence(default_conf): - persistence.init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) @pytest.fixture(scope="function") @@ -297,7 +297,7 @@ def default_conf(testdatadir): @pytest.fixture def update(): _update = Update(0) - _update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) + _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) return _update diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 7696dd96a..1592fac10 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -114,7 +114,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): create_mock_trades(fee) # remove init so it does not init again - init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) + init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index c2ecf4b80..a64dce908 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -132,7 +132,7 @@ def test_orderbook(mocker, default_conf, order_book_l2): res = dp.orderbook('ETH/BTC', 5) assert order_book_l2.call_count == 1 assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC' - assert order_book_l2.call_args_list[0][0][1] == 5 + assert order_book_l2.call_args_list[0][0][1] >= 5 assert type(res) is dict assert 'bids' in res diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index f19590490..a4bfa1085 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -499,3 +499,61 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,): assert final['TEST/BTC'].stoploss == -0.9 assert final['TEST/BTC'].nb_trades == len(trades_df) - 1 assert round(final['TEST/BTC'].winrate, 10) == 0.0 + + +def test_process_expectancy_only_wins(mocker, edge_conf, fee,): + edge_conf['edge']['min_trade_number'] = 2 + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + + freqtrade.exchange.get_fee = fee + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + trades = [ + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_index': 1, + 'close_index': 1, + 'trade_duration': '', + 'open_rate': 15, + 'close_rate': 17, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 10, + 'close_rate': 20, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_index': 6, + 'close_index': 7, + 'trade_duration': '', + 'open_rate': 26, + 'close_rate': 134, + 'exit_type': 'sell_signal'} + ] + + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert final['TEST/BTC'].nb_trades == len(trades_df) + assert round(final['TEST/BTC'].winrate, 10) == 1.0 + assert round(final['TEST/BTC'].risk_reward_ratio, 10) == float('inf') + assert round(final['TEST/BTC'].expectancy, 10) == float('inf') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7be9c77ac..19f2c7239 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Binance, Exchange, Kraken +from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, @@ -148,11 +148,19 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + + exchange = ExchangeResolver.load_exchange('huobi', default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() + exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Bittrex) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + caplog.clear() + exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) @@ -1438,6 +1446,27 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert log_has("Async code raised an exception: TypeError", caplog) +def test_get_next_limit_in_list(): + limit_range = [5, 10, 20, 50, 100, 500, 1000] + assert Exchange.get_next_limit_in_list(1, limit_range) == 5 + assert Exchange.get_next_limit_in_list(5, limit_range) == 5 + assert Exchange.get_next_limit_in_list(6, limit_range) == 10 + assert Exchange.get_next_limit_in_list(9, limit_range) == 10 + assert Exchange.get_next_limit_in_list(10, limit_range) == 10 + assert Exchange.get_next_limit_in_list(11, limit_range) == 20 + assert Exchange.get_next_limit_in_list(19, limit_range) == 20 + assert Exchange.get_next_limit_in_list(21, limit_range) == 50 + assert Exchange.get_next_limit_in_list(51, limit_range) == 100 + assert Exchange.get_next_limit_in_list(1000, limit_range) == 1000 + # Going over the limit ... + assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 + assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + + assert Exchange.get_next_limit_in_list(21, None) == 21 + assert Exchange.get_next_limit_in_list(100, None) == 100 + assert Exchange.get_next_limit_in_list(1000, None) == 1000 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name): default_conf['exchange']['name'] = exchange_name @@ -1450,6 +1479,19 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name) assert 'asks' in order_book assert len(order_book['bids']) == 10 assert len(order_book['asks']) == 10 + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + + for val in [1, 5, 10, 12, 20, 50, 100]: + api_mock.fetch_l2_order_book.reset_mock() + + order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=val) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + # Not all exchanges support all limits for orderbook + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == next_limit @pytest.mark.parametrize("exchange_name", EXCHANGES) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f699473f7..41ad6f5de 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -17,7 +17,7 @@ from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss +from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.state import RunMode @@ -33,7 +33,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'hyperopt': 'DefaultHyperOpt', - 'hyperopt_loss': 'DefaultHyperOptLoss', + 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), 'epochs': 1, 'timerange': None, @@ -239,12 +239,12 @@ def test_hyperoptlossresolver_noname(default_conf): def test_hyperoptlossresolver(mocker, default_conf) -> None: - hl = DefaultHyperOptLoss + hl = ShortTradeDurHyperOptLoss mocker.patch( 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', MagicMock(return_value=hl) ) - default_conf.update({'hyperopt_loss': 'DefaultHyperoptLoss'}) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) x = HyperOptLossResolver.load_hyperoptloss(default_conf) assert hasattr(x, "hyperopt_loss_function") @@ -287,7 +287,7 @@ def test_start(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -311,7 +311,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -329,7 +329,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -384,7 +384,7 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c62282cf0..230df0df9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -82,7 +82,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: assert log_has(message_str, caplog) -def test_cleanup(default_conf, mocker) -> None: +def test_cleanup(default_conf, mocker, ) -> None: updater_mock = MagicMock() updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) @@ -92,13 +92,9 @@ def test_cleanup(default_conf, mocker) -> None: assert telegram._updater.stop.call_count == 1 -def test_authorized_only(default_conf, mocker, caplog) -> None: +def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - chat = Chat(0, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) patch_get_signal(bot, (True, False)) @@ -114,7 +110,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) + update.message = Message(randint(1, 100), datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -127,12 +123,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_exception(default_conf, mocker, caplog) -> None: +def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -146,7 +139,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker, fee, ticker,) -> None: +def test_telegram_status(default_conf, update, mocker) -> None: update.message.chat.id = "123" default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8af3e12a7..bb7ff26e7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,8 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade -from freqtrade.persistence.models import Order +from freqtrade.persistence import Order, Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -66,7 +65,7 @@ def test_process_stopped(mocker, default_conf) -> None: def test_bot_cleanup(mocker, default_conf, caplog) -> None: - mock_cleanup = mocker.patch('freqtrade.persistence.cleanup') + mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.cleanup() diff --git a/tests/test_main.py b/tests/test_main.py index 9106d4c12..f55aea336 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -65,7 +65,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -83,7 +83,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -104,7 +104,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -155,7 +155,7 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None: reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) @@ -178,7 +178,7 @@ def test_reconfigure(mocker, default_conf) -> None: mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index adfa18876..4216565ac 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -8,13 +8,13 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'scoped_session' in type(Trade.session).__name__ @@ -24,7 +24,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -33,7 +33,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -42,7 +42,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -53,7 +53,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' @@ -482,7 +482,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(insert_table_old) engine.execute(insert_table_old2) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -661,7 +661,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -904,7 +904,7 @@ def test_to_json(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee): - init(default_conf['db_url']) + init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', stake_amount=0.001,