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,