diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58185b27c..c9a967834 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,6 +72,10 @@ pip3.6 install mypy mypy freqtrade ``` +## Getting started + +Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started. + ## (Core)-Committer Guide ### Process: Pull Requests diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index c2b40ba81..9d82fc2d5 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -4,6 +4,9 @@ TAG=$(echo "${TRAVIS_BRANCH}" | sed -e "s/\//_/") +# Add commit and commit_message to docker container +echo "${TRAVIS_COMMIT} ${TRAVIS_COMMIT_MESSAGE}" > freqtrade_commit + if [ "${TRAVIS_EVENT_TYPE}" = "cron" ]; then echo "event ${TRAVIS_EVENT_TYPE}: full rebuild - skipping cache" docker build -t freqtrade:${TAG} . diff --git a/config.json.example b/config.json.example index bbd9648da..323ff711e 100644 --- a/config.json.example +++ b/config.json.example @@ -57,7 +57,7 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "total_capital_in_stake_currency": 0.5, + "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/config_binance.json.example b/config_binance.json.example index 7773a8c39..3d11f317a 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -59,7 +59,7 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "total_capital_in_stake_currency": 0.5, + "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/config_full.json.example b/config_full.json.example index 89f494c7d..0427f8700 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -43,6 +43,13 @@ "buy": "gtc", "sell": "gtc", }, + "pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume" + } + }, "exchange": { "name": "bittrex", "key": "your_exchange_key", @@ -72,7 +79,8 @@ "edge": { "enabled": false, "process_throttle_secs": 3600, - "calculate_since_number_of_days": 2, + "calculate_since_number_of_days": 7, + "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/docs/bot-usage.md b/docs/bot-usage.md index ec8873b12..114e7613e 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -36,7 +36,7 @@ optional arguments: --strategy-path PATH specify additional strategy lookup path --dynamic-whitelist [INT] dynamically generate and update whitelist based on 24h - BaseVolume (default: 20) + BaseVolume (default: 20) DEPRECATED --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: sqlite:///tradesv3.sqlite) @@ -89,6 +89,8 @@ This is very simple. Copy paste your strategy file into the folder ### How to use --dynamic-whitelist? +> Dynamic-whitelist is deprecated. Please move your configurations to the configuration as outlined [here](docs/configuration.md#Dynamic-Pairlists) + Per default `--dynamic-whitelist` will retrieve the 20 currencies based on BaseVolume. This value can be changed when you run the script. diff --git a/docs/configuration.md b/docs/configuration.md index 5b8baa43b..c4c0bed28 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -34,8 +34,8 @@ The table below will list all configuration parameters. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids. | `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. -| `bid_strategy.check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book. -| `bid_strategy.check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. +| `bid_strategy. check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book. +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. | `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks. | `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. @@ -52,6 +52,8 @@ The table below will list all configuration parameters. | `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`. | `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision. | `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal` +| `pairlist.method` | StaticPairList | No | Use Static whitelist. [More information below](#dynamic-pairlists). +| `pairlist.config` | None | No | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. @@ -147,7 +149,7 @@ This can be set in the configuration or in the strategy. Configuration overwrite If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. The below is the default which is used if this is not configured in either Strategy or configuration. -``` json +``` python "order_types": { "buy": "limit", "sell": "limit", @@ -211,13 +213,38 @@ creating trades. Once you will be happy with your bot performance, you can switch it to production mode. +### Dynamic Pairlists + +Dynamic pairlists select pairs for you based on the logic configured. +The bot runs against all pairs (with that stake) on the exchange, and a number of assets (`number_assets`) is selected based on the selected criteria. + +By *default*, a Static Pairlist is used (configured as `"pair_whitelist"` under the `"exchange"` section of this configuration). + +#### Available Pairlist methods + +* `"StaticPairList"` + * uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist` +* `"VolumePairList"` + * Formerly available as `--dynamic-whitelist []` + * Selects `number_assets` top pairs based on `sort_key`, which can be one of `askVolume`, `bidVolume` and `quoteVolume`, defaults to `quoteVolume`. + +```json +"pairlist": { + "method": "VolumePairList", + "config": { + "number_assets": 20, + "sort_key": "quoteVolume" + } + }, +``` + ## Switch to production mode In production mode, the bot will engage your money. Be careful a wrong strategy can lose all your money. Be aware of what you are doing when you run it in production mode. -### To switch your bot in production mode: +### To switch your bot in production mode 1. Edit your `config.json` file diff --git a/docs/developer.md b/docs/developer.md new file mode 100644 index 000000000..9137f16ca --- /dev/null +++ b/docs/developer.md @@ -0,0 +1,70 @@ +# Development Help + +This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running. + +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions. + + +## Module + +### Dynamic Pairlist + +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. + +Whatever your motivations are - This should get you off the ground in trying to develop a new Pairlist provider. + +First of all, have a look at the [VolumePairList](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/pairlist/VolumePairList.py) provider, and best copy this file with a name of your new Pairlist Provider. + +This is a simple provider, which however serves as a good example on how to start developing. + +Next, modify the classname of the provider (ideally align this with the Filename). + +The base-class provides the an instance of the bot (`self._freqtrade`), as well as the configuration (`self._config`), and initiates both `_blacklist` and `_whitelist`. + +```python + self._freqtrade = freqtrade + self._config = config + self._whitelist = self._config['exchange']['pair_whitelist'] + self._blacklist = self._config['exchange'].get('pair_blacklist', []) +``` + + +Now, let's step through the methods which require actions: + +#### configuration + +Configuration for PairListProvider is done in the bot configuration file in the element `"pairlist"`. +This Pairlist-object may contain a `"config"` dict with additional configurations for the configured pairlist. +By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the whitelist. Please follow this to ensure a consistent user experience. + +Additional elements can be configured as needed. `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. + +#### short_desc + +Returns a description used for Telegram messages. +This should contain the name of the Provider, as well as a short description containing the number of assets. Please follow the format `"PairlistName - top/bottom X pairs"`. + +#### refresh_pairlist + +Override this method and run all calculations needed in this method. +This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. + +Assign the resulting whiteslist to `self._whitelist` and `self._blacklist` respectively. These will then be used to run the bot in this iteration. Pairs with open trades will be added to the whitelist to have the sell-methods run correctly. + +Please also run `self._validate_whitelist(pairs)` and to check and remove pairs with inactive markets. This function is available in the Parent class (`StaticPairList`) and should ideally not be overwritten. + +##### sample + +``` python + def refresh_pairlist(self) -> None: + # Generate dynamic whitelist + pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) + # Validate whitelist to only have active market pairs + self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs] +``` + +#### _gen_pair_whitelist + +This is a simple method used by `VolumePairList` - however serves as a good example. +It implements caching (`@cached(TTLCache(maxsize=1, ttl=1800))`) as well as a configuration option to allow different (but similar) strategies to work with the same PairListProvider. diff --git a/docs/edge.md b/docs/edge.md index e5575554b..829910484 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,9 +82,7 @@ Edge dictates the stake amount for each trade to the bot according to the follow Allowed capital at risk is calculated as follows: -**allowed capital at risk** = **total capital** X **allowed risk per trade** - -**total capital** is your stake amount. +**allowed capital at risk** = **capital_available_percentage** X **allowed risk per trade** **Stoploss** is calculated as described above against historical data. @@ -92,14 +90,20 @@ Your position size then will be: **position size** = **allowed capital at risk** / **stoploss** -Example: -Let's say your stake amount is 3 ETH, you would allow 1% of risk for each trade. thus your allowed capital at risk would be **3 x 0.01 = 0.03 ETH**. Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.03 / 0.02= 1.5ETH**.
+Example:
+Let's say the stake currency is ETH and you have 10 ETH on the exchange, your **capital_available_percentage** is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**.
+Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5ETH**.
+Bot takes a position of 2.5ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on BTC/ETH market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25ETH (call it trade 2).
+Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.
+Now you have two trades open. The Bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5ETH**. But there are already 4ETH blocked in two previous trades. So the position size for this third trade would be 1ETH.
+Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5ETH.
+So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75**. ## Configurations Edge has following configurations: #### enabled -If true, then Edge will run periodically
+If true, then Edge will run periodically.
(default to false) #### process_throttle_secs @@ -108,19 +112,24 @@ How often should Edge run in seconds?
#### calculate_since_number_of_days Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy -Note that it downloads historical data so increasing this number would lead to slowing down the bot
+Note that it downloads historical data so increasing this number would lead to slowing down the bot.
(default to 7) +#### capital_available_percentage +This is the percentage of the total capital on exchange in stake currency.
+As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
+(default to 0.5) + #### allowed_risk -Percentage of allowed risk per trade
+Percentage of allowed risk per trade.
(default to 0.01 [1%]) #### stoploss_range_min -Minimum stoploss
+Minimum stoploss.
(default to -0.01) #### stoploss_range_max -Maximum stoploss
+Maximum stoploss.
(default to -0.10) #### stoploss_range_step diff --git a/docs/index.md b/docs/index.md index 43890b053..9eb0d445c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,4 +35,5 @@ Pull-request. Do not hesitate to reach us on - [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) - [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md) -- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)) +- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md) +- [Developer Docs](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 84e1a0f77..a33848c5f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -115,7 +115,8 @@ class Arguments(object): self.parser.add_argument( '--dynamic-whitelist', help='dynamically generate and update whitelist' - ' based on 24h BaseVolume (default: %(const)s)', + ' based on 24h BaseVolume (default: %(const)s)' + ' DEPRECATED.', dest='dynamic_whitelist', const=constants.DYNAMIC_WHITELIST, type=int, diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index feec0cb43..5c7da3413 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -110,10 +110,14 @@ class Configuration(object): # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: - config.update({'dynamic_whitelist': self.args.dynamic_whitelist}) - logger.info( - 'Parameter --dynamic-whitelist detected. ' - 'Using dynamically generated whitelist. ' + # Update to volumePairList (the previous default) + config['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': self.args.dynamic_whitelist} + } + logger.warning( + 'Parameter --dynamic-whitelist has been deprecated, ' + 'and will be completely replaced by the whitelist dict in the future. ' + 'For now: using dynamically generated whitelist based on VolumePairList. ' '(not applicable with Backtesting and Hyperopt)' ) @@ -275,7 +279,7 @@ class Configuration(object): :return: Returns the config if valid, otherwise throw an exception """ try: - validate(conf, constants.CONF_SCHEMA) + validate(conf, constants.CONF_SCHEMA, Draft4Validator) return conf except ValidationError as exception: logger.critical( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 4c3b86e7e..b2393c2b7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -17,7 +17,7 @@ REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] - +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] TICKER_INTERVAL_MINUTES = { '1m': 1, @@ -134,6 +134,14 @@ CONF_SCHEMA = { 'ignore_roi_if_buy_signal_true': {'type': 'boolean'} } }, + 'pairlist': { + 'type': 'object', + 'properties': { + 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, + 'config': {'type': 'object'} + }, + 'required': ['method'] + }, 'telegram': { 'type': 'object', 'properties': { @@ -202,6 +210,7 @@ CONF_SCHEMA = { "process_throttle_secs": {'type': 'integer', 'minimum': 600}, "calculate_since_number_of_days": {'type': 'integer'}, "allowed_risk": {'type': 'number'}, + "capital_available_percentage": {'type': 'number'}, "stoploss_range_min": {'type': 'number'}, "stoploss_range_max": {'type': 'number'}, "stoploss_range_step": {'type': 'number'}, @@ -210,7 +219,8 @@ CONF_SCHEMA = { "min_trade_number": {'type': 'number'}, "max_trade_duration_minute": {'type': 'integer'}, "remove_pumps": {'type': 'boolean'} - } + }, + 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] } }, 'anyOf': [ diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4cb0dbc31..49acbd3e7 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -9,6 +9,7 @@ import utils_find_1st as utf1st from pandas import DataFrame import freqtrade.optimize as optimize +from freqtrade import constants, OperationalException from freqtrade.arguments import Arguments from freqtrade.arguments import TimeRange from freqtrade.strategy.interface import SellType @@ -52,8 +53,17 @@ class Edge(): self.edge_config = self.config.get('edge', {}) self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs + self._final_pairs: list = [] - self._total_capital: float = self.config['stake_amount'] + # checking max_open_trades. it should be -1 as with Edge + # the number of trades is determined by position size + if self.config['max_open_trades'] != -1: + logger.critical('max_open_trades should be -1 in config !') + + if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: + raise OperationalException('Edge works only with unlimited stake amount') + + self._capital_percentage: float = self.edge_config.get('capital_available_percentage') self._allowed_risk: float = self.edge_config.get('allowed_risk') self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) self._last_updated: int = 0 # Timestamp of pairs last updated time @@ -150,11 +160,25 @@ class Edge(): return True - def stake_amount(self, pair: str) -> float: - stoploss = self._cached_pairs[pair].stoploss - allowed_capital_at_risk = round(self._total_capital * self._allowed_risk, 5) - position_size = abs(round((allowed_capital_at_risk / stoploss), 5)) - return position_size + def stake_amount(self, pair: str, free_capital: float, + total_capital: float, capital_in_trade: float) -> float: + stoploss = self.stoploss(pair) + available_capital = (total_capital + capital_in_trade) * self._capital_percentage + allowed_capital_at_risk = available_capital * self._allowed_risk + max_position_size = abs(allowed_capital_at_risk / stoploss) + position_size = min(max_position_size, free_capital) + if pair in self._cached_pairs: + logger.info( + 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' + ' capital in trade: %s, free capital: %s, total capital: %s,' + ' stoploss: %s, available capital: %s.', + self._cached_pairs[pair].winrate, + self._cached_pairs[pair].expectancy, + position_size, pair, + capital_in_trade, free_capital, total_capital, + stoploss, available_capital + ) + return round(position_size, 15) def stoploss(self, pair: str) -> float: if pair in self._cached_pairs: @@ -168,7 +192,6 @@ class Edge(): """ Filters out and sorts "pairs" according to Edge calculated pairs """ - final = [] for pair, info in self._cached_pairs.items(): if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ @@ -176,12 +199,14 @@ class Edge(): pair in pairs: final.append(pair) - if final: - logger.info('Edge validated only %s', final) - else: - logger.info('Edge removed all pairs as no pair with minimum expectancy was found !') + if self._final_pairs != final: + self._final_pairs = final + if self._final_pairs: + logger.info('Edge validated only %s', self._final_pairs) + else: + logger.info('Edge removed all pairs as no pair with minimum expectancy was found !') - return final + return self._final_pairs def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: """ @@ -202,9 +227,11 @@ class Edge(): # 0.05% is 0.0005 # fee = 0.001 - stake = self.config.get('stake_amount') + # we set stake amount to an arbitrary amount. + # as it doesn't change the calculation. + # all returned values are relative. they are percentages. + stake = 0.015 fee = self.fee - open_fee = fee / 2 close_fee = fee / 2 diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 291d16ff1..907734313 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,8 +12,6 @@ from typing import Any, Callable, Dict, List, Optional import arrow from requests.exceptions import RequestException -from cachetools import TTLCache, cached - from freqtrade import (DependencyException, OperationalException, TemporaryError, __version__, constants, persistence) from freqtrade.exchange import Exchange @@ -21,7 +19,7 @@ from freqtrade.wallets import Wallets from freqtrade.edge import Edge from freqtrade.persistence import Trade from freqtrade.rpc import RPCManager, RPCMessageType -from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers import StrategyResolver, PairListResolver from freqtrade.state import State from freqtrade.strategy.interface import SellType, IStrategy from freqtrade.exchange.exchange_helpers import order_book_to_dataframe @@ -59,6 +57,8 @@ class FreqtradeBot(object): self.persistence = None self.exchange = Exchange(self.config) self.wallets = Wallets(self.exchange) + pairlistname = self.config.get('pairlist', {}).get('method', 'StaticPairList') + self.pairlists = PairListResolver(pairlistname, self, self.config).pairlist # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ @@ -108,7 +108,7 @@ class FreqtradeBot(object): }) logger.info('Changing state to: %s', state.name) if state == State.RUNNING: - self.rpc.startup_messages(self.config) + self.rpc.startup_messages(self.config, self.pairlists) if state == State.STOPPED: time.sleep(1) @@ -146,16 +146,9 @@ class FreqtradeBot(object): """ state_changed = False try: - nb_assets = self.config.get('dynamic_whitelist', None) - # Refresh whitelist based on wallet maintenance - sanitized_list = self._refresh_whitelist( - self._gen_pair_whitelist( - self.config['stake_currency'] - ) if nb_assets else self.config['exchange']['pair_whitelist'] - ) - - # Keep only the subsets of pairs wanted (up to nb_assets) - self.active_pair_whitelist = sanitized_list[:nb_assets] if nb_assets else sanitized_list + # Refresh whitelist + self.pairlists.refresh_pairlist() + self.active_pair_whitelist = self.pairlists.whitelist # Calculating Edge positiong # Should be called before refresh_tickers @@ -203,63 +196,6 @@ class FreqtradeBot(object): self.state = State.STOPPED return state_changed - @cached(TTLCache(maxsize=1, ttl=1800)) - def _gen_pair_whitelist(self, base_currency: str, key: str = 'quoteVolume') -> List[str]: - """ - Updates the whitelist with with a dynamically generated list - :param base_currency: base currency as str - :param key: sort key (defaults to 'quoteVolume') - :return: List of pairs - """ - - if not self.exchange.exchange_has('fetchTickers'): - raise OperationalException( - 'Exchange does not support dynamic whitelist.' - 'Please edit your config and restart the bot' - ) - - tickers = self.exchange.get_tickers() - # check length so that we make sure that '/' is actually in the string - tickers = [v for k, v in tickers.items() - if len(k.split('/')) == 2 and k.split('/')[1] == base_currency] - - sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) - pairs = [s['symbol'] for s in sorted_tickers] - return pairs - - def _refresh_whitelist(self, whitelist: List[str]) -> List[str]: - """ - Check available markets and remove pair from whitelist if necessary - :param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to - trade - :return: the list of pairs the user wants to trade without the one unavailable or - black_listed - """ - sanitized_whitelist = whitelist - markets = self.exchange.get_markets() - - markets = [m for m in markets if m['quote'] == self.config['stake_currency']] - known_pairs = set() - for market in markets: - pair = market['symbol'] - # pair is not int the generated dynamic market, or in the blacklist ... ignore it - if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []): - continue - # else the pair is valid - known_pairs.add(pair) - # Market is not active - if not market['active']: - sanitized_whitelist.remove(pair) - logger.info( - 'Ignoring %s from whitelist. Market is not active.', - pair - ) - - # We need to remove pairs that are unknown - final_list = [x for x in sanitized_whitelist if x in known_pairs] - - return final_list - def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float: """ Calculates bid target between current ask price and last price @@ -302,7 +238,12 @@ class FreqtradeBot(object): :return: float: Stake Amount """ if self.edge: - stake_amount = self.edge.stake_amount(pair) + return self.edge.stake_amount( + pair, + self.wallets.get_free(self.config['stake_currency']), + self.wallets.get_total(self.config['stake_currency']), + Trade.total_open_trades_stakes() + ) else: stake_amount = self.config['stake_amount'] diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a2189f6c1..fbe81812d 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -55,7 +55,7 @@ class EdgeCli(object): 'average duration (min)'] for result in results.items(): - if result[1].nb_trades > 0: + if result[1].nb_trades > 0 and result[1].winrate > 0.60: tabular_data.append([ result[0], result[1].stoploss, diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py new file mode 100644 index 000000000..6b5b0db4b --- /dev/null +++ b/freqtrade/pairlist/IPairList.py @@ -0,0 +1,91 @@ +""" +Static List provider + +Provides lists as configured in config.json + + """ +import logging +from abc import ABC, abstractmethod +from typing import List + +logger = logging.getLogger(__name__) + + +class IPairList(ABC): + + def __init__(self, freqtrade, config: dict) -> None: + self._freqtrade = freqtrade + self._config = config + self._whitelist = self._config['exchange']['pair_whitelist'] + self._blacklist = self._config['exchange'].get('pair_blacklist', []) + + @property + def name(self) -> str: + """ + Gets name of the class + -> no need to overwrite in subclasses + """ + return self.__class__.__name__ + + @property + def whitelist(self) -> List[str]: + """ + Has the current whitelist + -> no need to overwrite in subclasses + """ + return self._whitelist + + @property + def blacklist(self) -> List[str]: + """ + Has the current blacklist + -> no need to overwrite in subclasses + """ + return self._blacklist + + @abstractmethod + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + + @abstractmethod + def refresh_pairlist(self) -> None: + """ + Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively + -> Please overwrite in subclasses + """ + + def _validate_whitelist(self, whitelist: List[str]) -> List[str]: + """ + Check available markets and remove pair from whitelist if necessary + :param whitelist: the sorted list (based on BaseVolume) of pairs the user might want to + trade + :return: the list of pairs the user wants to trade without the one unavailable or + black_listed + """ + sanitized_whitelist = whitelist + markets = self._freqtrade.exchange.get_markets() + + # Filter to markets in stake currency + markets = [m for m in markets if m['quote'] == self._config['stake_currency']] + known_pairs = set() + + for market in markets: + pair = market['symbol'] + # pair is not int the generated dynamic market, or in the blacklist ... ignore it + if pair not in whitelist or pair in self.blacklist: + continue + # else the pair is valid + known_pairs.add(pair) + # Market is not active + if not market['active']: + sanitized_whitelist.remove(pair) + logger.info( + 'Ignoring %s from whitelist. Market is not active.', + pair + ) + + # We need to remove pairs that are unknown + return [x for x in sanitized_whitelist if x in known_pairs] diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py new file mode 100644 index 000000000..5896e814a --- /dev/null +++ b/freqtrade/pairlist/StaticPairList.py @@ -0,0 +1,30 @@ +""" +Static List provider + +Provides lists as configured in config.json + + """ +import logging + +from freqtrade.pairlist.IPairList import IPairList + +logger = logging.getLogger(__name__) + + +class StaticPairList(IPairList): + + def __init__(self, freqtrade, config: dict) -> None: + super().__init__(freqtrade, config) + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name}: {self.whitelist}" + + def refresh_pairlist(self) -> None: + """ + Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively + """ + self._whitelist = self._validate_whitelist(self._config['exchange']['pair_whitelist']) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py new file mode 100644 index 000000000..262e4bf59 --- /dev/null +++ b/freqtrade/pairlist/VolumePairList.py @@ -0,0 +1,75 @@ +""" +Static List provider + +Provides lists as configured in config.json + + """ +import logging +from typing import List +from cachetools import TTLCache, cached + +from freqtrade.pairlist.IPairList import IPairList +from freqtrade import OperationalException +logger = logging.getLogger(__name__) + +SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] + + +class VolumePairList(IPairList): + + def __init__(self, freqtrade, config: dict) -> None: + super().__init__(freqtrade, config) + self._whitelistconf = self._config.get('pairlist', {}).get('config') + if 'number_assets' not in self._whitelistconf: + raise OperationalException( + f'`number_assets` not specified. Please check your configuration ' + 'for "pairlist.config.number_assets"') + self._number_pairs = self._whitelistconf['number_assets'] + self._sort_key = self._whitelistconf.get('sort_key', 'quoteVolume') + + if not self._freqtrade.exchange.exchange_has('fetchTickers'): + raise OperationalException( + 'Exchange does not support dynamic whitelist.' + 'Please edit your config and restart the bot' + ) + if not self._validate_keys(self._sort_key): + raise OperationalException( + f'key {self._sort_key} not in {SORT_VALUES}') + + def _validate_keys(self, key): + return key in SORT_VALUES + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name} - top {self._whitelistconf['number_assets']} volume pairs." + + def refresh_pairlist(self) -> None: + """ + Refreshes pairlists and assigns them to self._whitelist and self._blacklist respectively + -> Please overwrite in subclasses + """ + # Generate dynamic whitelist + pairs = self._gen_pair_whitelist(self._config['stake_currency'], self._sort_key) + # Validate whitelist to only have active market pairs + self._whitelist = self._validate_whitelist(pairs)[:self._number_pairs] + + @cached(TTLCache(maxsize=1, ttl=1800)) + def _gen_pair_whitelist(self, base_currency: str, key: str) -> List[str]: + """ + Updates the whitelist with with a dynamically generated list + :param base_currency: base currency as str + :param key: sort key (defaults to 'quoteVolume') + :return: List of pairs + """ + + tickers = self._freqtrade.exchange.get_tickers() + # check length so that we make sure that '/' is actually in the string + tickers = [v for k, v in tickers.items() + if len(k.split('/')) == 2 and k.split('/')[1] == base_currency] + + sorted_tickers = sorted(tickers, reverse=True, key=lambda t: t[key]) + pairs = [s['symbol'] for s in sorted_tickers] + return pairs diff --git a/freqtrade/pairlist/__init__.py b/freqtrade/pairlist/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 592a88acb..a393eb318 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -14,6 +14,7 @@ from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker +from sqlalchemy import func from sqlalchemy.pool import StaticPool from freqtrade import OperationalException @@ -99,6 +100,9 @@ def check_migrate(engine) -> None: # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") + # drop indexes on backup table + for index in inspector.get_indexes(table_back_name): + engine.execute(f"drop index {index['name']}") # let SQLAlchemy create the schema as required _DECL_BASE.metadata.create_all(engine) @@ -349,3 +353,14 @@ class Trade(_DECL_BASE): ) profit_percent = (close_trade_price / open_trade_price) - 1 return float(f"{profit_percent:.8f}") + + @staticmethod + def total_open_trades_stakes() -> float: + """ + Calculates total invested amount in open trades + in stake currency + """ + total_open_stake_amount = Trade.session.query(func.sum(Trade.stake_amount))\ + .filter(Trade.is_open.is_(True))\ + .scalar() + return total_open_stake_amount or 0 diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 84e3bcdcd..da2987b27 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,3 +1,4 @@ from freqtrade.resolvers.iresolver import IResolver # noqa: F401 from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 +from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index da7b65648..eb91c0e89 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -52,11 +52,14 @@ class HyperOptResolver(IResolver): abs_paths.insert(0, Path(extra_dir)) for _path in abs_paths: - hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, - object_name=hyperopt_name) - if hyperopt: - logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, _path) - return hyperopt + try: + hyperopt = self._search_object(directory=_path, object_type=IHyperOpt, + object_name=hyperopt_name) + if hyperopt: + logger.info('Using resolved hyperopt %s from \'%s\'', hyperopt_name, _path) + return hyperopt + except FileNotFoundError: + logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) raise ImportError( "Impossible to load Hyperopt '{}'. This class does not exist" diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py new file mode 100644 index 000000000..286cea5bf --- /dev/null +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -0,0 +1,59 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom hyperopts +""" +import logging +from pathlib import Path + +from freqtrade.pairlist.IPairList import IPairList +from freqtrade.resolvers import IResolver + +logger = logging.getLogger(__name__) + + +class PairListResolver(IResolver): + """ + This class contains all the logic to load custom hyperopt class + """ + + __slots__ = ['pairlist'] + + def __init__(self, pairlist_name: str, freqtrade, config: dict) -> None: + """ + Load the custom class from config parameter + :param config: configuration dictionary or None + """ + self.pairlist = self._load_pairlist(pairlist_name, kwargs={'freqtrade': freqtrade, + 'config': config}) + + def _load_pairlist( + self, pairlist_name: str, kwargs: dict) -> IPairList: + """ + Search and loads the specified pairlist. + :param pairlist_name: name of the module to import + :param extra_dir: additional directory to search for the given pairlist + :return: PairList instance or None + """ + current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() + + abs_paths = [ + current_path.parent.parent.joinpath('user_data/pairlist'), + current_path, + ] + + for _path in abs_paths: + try: + pairlist = self._search_object(directory=_path, object_type=IPairList, + object_name=pairlist_name, + kwargs=kwargs) + if pairlist: + logger.info('Using resolved pairlist %s from \'%s\'', pairlist_name, _path) + return pairlist + except FileNotFoundError: + logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) + + raise ImportError( + "Impossible to load Pairlist '{}'. This class does not exist" + " or contains Python code errors".format(pairlist_name) + ) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 50cbd27ef..6ca32e4be 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -446,7 +446,8 @@ class RPC(object): def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" - res = {'method': self._freqtrade.config.get('dynamic_whitelist', 0) or 'static', + res = {'method': self._freqtrade.pairlists.name, + 'length': len(self._freqtrade.pairlists.whitelist), 'whitelist': self._freqtrade.active_pair_whitelist } return res diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 74a4e3bdc..de861677d 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -52,7 +52,7 @@ class RPCManager(object): logger.debug('Forwarding message to rpc.%s', mod.name) mod.send_msg(msg) - def startup_messages(self, config) -> None: + def startup_messages(self, config, pairlist) -> None: if config.get('dry_run', False): self.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, @@ -72,14 +72,8 @@ class RPCManager(object): f'*Ticker Interval:* `{ticker_interval}`\n' f'*Strategy:* `{strategy_name}`' }) - if config.get('dynamic_whitelist', False): - top_pairs = 'top volume ' + str(config.get('dynamic_whitelist', 20)) - specific_pairs = '' - else: - top_pairs = 'whitelisted' - specific_pairs = '\n' + ', '.join(config['exchange'].get('pair_whitelist', '')) self.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Searching for {top_pairs} {stake_currency} pairs to buy and sell...' - f'{specific_pairs}' + 'status': f'Searching for {stake_currency} pairs to buy and sell ' + f'based on {pairlist.short_desc()}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 032593e9e..56c58f2cc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -448,10 +448,8 @@ class Telegram(RPC): """ try: whitelist = self._rpc_whitelist() - if whitelist['method'] == 'static': - message = f"Using static whitelist with `{len(whitelist['whitelist'])}` pairs \n" - else: - message = f"Dynamic whitelist with `{whitelist['method']}` pairs\n" + + message = f"Using whitelist `{whitelist['method']}` with {whitelist['length']} pairs\n" message += f"`{', '.join(whitelist['whitelist'])}`" logger.debug(message) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 0c38019e3..df1a1cdc4 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -10,6 +10,7 @@ import arrow import pytest from telegram import Chat, Message, Update +from freqtrade import constants from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.edge import Edge, PairInfo @@ -63,7 +64,6 @@ def patch_edge(mocker) -> None: 'LTC/BTC': PairInfo(-0.21, 0.66, 3.71, 0.50, 1.71, 11, 20), } )) - mocker.patch('freqtrade.edge.Edge.stoploss', MagicMock(return_value=-0.20)) mocker.patch('freqtrade.edge.Edge.calculate', MagicMock(return_value=True)) @@ -788,10 +788,13 @@ def buy_order_fee(): @pytest.fixture(scope="function") def edge_conf(default_conf): + default_conf['max_open_trades'] = -1 + default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT default_conf['edge'] = { "enabled": True, "process_throttle_secs": 1800, "calculate_since_number_of_days": 14, + "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/freqtrade/tests/edge/test_edge.py b/freqtrade/tests/edge/test_edge.py index 50c4ade3d..008413ff1 100644 --- a/freqtrade/tests/edge/test_edge.py +++ b/freqtrade/tests/edge/test_edge.py @@ -123,9 +123,9 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None: assert res.close_time == _get_frame_time_from_offset(trade.close_tick) -def test_adjust(mocker, default_conf): - freqtrade = get_patched_freqtradebot(mocker, default_conf) - edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy) +def test_adjust(mocker, edge_conf): + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -138,9 +138,9 @@ def test_adjust(mocker, default_conf): assert(edge.adjust(pairs) == ['E/F', 'C/D']) -def test_stoploss(mocker, default_conf): - freqtrade = get_patched_freqtradebot(mocker, default_conf) - edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy) +def test_stoploss(mocker, edge_conf): + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -152,9 +152,9 @@ def test_stoploss(mocker, default_conf): assert edge.stoploss('E/F') == -0.01 -def test_nonexisting_stoploss(mocker, default_conf): - freqtrade = get_patched_freqtradebot(mocker, default_conf) - edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy) +def test_nonexisting_stoploss(mocker, edge_conf): + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), @@ -164,6 +164,42 @@ def test_nonexisting_stoploss(mocker, default_conf): assert edge.stoploss('N/O') == -0.1 +def test_stake_amount(mocker, edge_conf): + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + free = 100 + total = 100 + in_trade = 25 + assert edge.stake_amount('E/F', free, total, in_trade) == 31.25 + + free = 20 + total = 100 + in_trade = 25 + assert edge.stake_amount('E/F', free, total, in_trade) == 20 + + free = 0 + total = 100 + in_trade = 25 + assert edge.stake_amount('E/F', free, total, in_trade) == 0 + + +def test_nonexisting_stake_amount(mocker, edge_conf): + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( + return_value={ + 'E/F': PairInfo(-0.11, 0.66, 3.71, 0.50, 1.71, 10, 60), + } + )) + # should use strategy stoploss + assert edge.stake_amount('N/O', 1, 2, 1) == 0.15 + + def _validate_ohlc(buy_ohlc_sell_matrice): for index, ohlc in enumerate(buy_ohlc_sell_matrice): # if not high < open < low or not high < close < low @@ -246,12 +282,12 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals return pairdata -def test_edge_process_downloaded_data(mocker, default_conf): - default_conf['datadir'] = None - freqtrade = get_patched_freqtradebot(mocker, default_conf) +def test_edge_process_downloaded_data(mocker, edge_conf): + edge_conf['datadir'] = None + freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) mocker.patch('freqtrade.optimize.load_data', mocked_load_data) - edge = Edge(default_conf, freqtrade.exchange, freqtrade.strategy) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert edge.calculate() assert len(edge._cached_pairs) == 2 diff --git a/freqtrade/tests/pairlist/__init__.py b/freqtrade/tests/pairlist/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/tests/pairlist/test_pairlist.py b/freqtrade/tests/pairlist/test_pairlist.py new file mode 100644 index 000000000..9f90aac6e --- /dev/null +++ b/freqtrade/tests/pairlist/test_pairlist.py @@ -0,0 +1,170 @@ +# pragma pylint: disable=missing-docstring,C0103,protected-access + +from unittest.mock import MagicMock + +from freqtrade import OperationalException +from freqtrade.constants import AVAILABLE_PAIRLISTS +from freqtrade.resolvers import PairListResolver +from freqtrade.tests.conftest import get_patched_freqtradebot +import pytest + +# whitelist, blacklist + + +@pytest.fixture(scope="function") +def whitelist_conf(default_conf): + default_conf['stake_currency'] = 'BTC' + default_conf['exchange']['pair_whitelist'] = [ + 'ETH/BTC', + 'TKN/BTC', + 'TRST/BTC', + 'SWT/BTC', + 'BCC/BTC' + ] + default_conf['exchange']['pair_blacklist'] = [ + 'BLK/BTC' + ] + default_conf['pairlist'] = {'method': 'StaticPairList', + 'config': {'number_assets': 3} + } + + return default_conf + + +def test_load_pairlist_noexist(mocker, markets, default_conf): + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) + with pytest.raises(ImportError, + match=r"Impossible to load Pairlist 'NonexistingPairList'." + r" This class does not exist or contains Python code errors"): + PairListResolver('NonexistingPairList', freqtradebot, default_conf).pairlist + + +def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf): + + freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) + freqtradebot.pairlists.refresh_pairlist() + # List ordered by BaseVolume + whitelist = ['ETH/BTC', 'TKN/BTC'] + # Ensure all except those in whitelist are removed + assert set(whitelist) == set(freqtradebot.pairlists.whitelist) + # Ensure config dict hasn't been changed + assert (whitelist_conf['exchange']['pair_whitelist'] == + freqtradebot.config['exchange']['pair_whitelist']) + + +def test_refresh_pairlists(mocker, markets, whitelist_conf): + freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) + freqtradebot.pairlists.refresh_pairlist() + # List ordered by BaseVolume + whitelist = ['ETH/BTC', 'TKN/BTC'] + # Ensure all except those in whitelist are removed + assert set(whitelist) == set(freqtradebot.pairlists.whitelist) + assert whitelist_conf['exchange']['pair_blacklist'] == freqtradebot.pairlists.blacklist + + +def test_refresh_pairlist_dynamic(mocker, markets, tickers, whitelist_conf): + whitelist_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 5} + } + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_markets=markets, + get_tickers=tickers, + exchange_has=MagicMock(return_value=True) + ) + freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + + # argument: use the whitelist dynamically by exchange-volume + whitelist = ['ETH/BTC', 'TKN/BTC'] + freqtradebot.pairlists.refresh_pairlist() + + assert whitelist == freqtradebot.pairlists.whitelist + + whitelist_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {} + } + with pytest.raises(OperationalException, + match=r'`number_assets` not specified. Please check your configuration ' + r'for "pairlist.config.number_assets"'): + PairListResolver('VolumePairList', freqtradebot, whitelist_conf).pairlist + + +def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): + freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty) + + # argument: use the whitelist dynamically by exchange-volume + whitelist = [] + whitelist_conf['exchange']['pair_whitelist'] = [] + freqtradebot.pairlists.refresh_pairlist() + pairslist = whitelist_conf['exchange']['pair_whitelist'] + + assert set(whitelist) == set(pairslist) + + +def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, markets, tickers) -> None: + whitelist_conf['pairlist']['method'] = 'VolumePairList' + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + + # Test to retrieved BTC sorted on quoteVolume (default) + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='quoteVolume') + assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC'] + + # Test to retrieve BTC sorted on bidVolume + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='BTC', key='bidVolume') + assert whitelist == ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'BLK/BTC'] + + # Test with USDT sorted on quoteVolume (default) + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='USDT', key='quoteVolume') + assert whitelist == ['TKN/USDT', 'ETH/USDT', 'LTC/USDT', 'BLK/USDT'] + + # Test with ETH (our fixture does not have ETH, so result should be empty) + whitelist = freqtrade.pairlists._gen_pair_whitelist(base_currency='ETH', key='quoteVolume') + assert whitelist == [] + + +def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: + default_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 10} + } + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) + + with pytest.raises(OperationalException): + get_patched_freqtradebot(mocker, default_conf) + + +@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) +def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): + whitelist_conf['pairlist']['method'] = pairlist + mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + + assert freqtrade.pairlists.name == pairlist + assert pairlist in freqtrade.pairlists.short_desc() + assert isinstance(freqtrade.pairlists.whitelist, list) + assert isinstance(freqtrade.pairlists.blacklist, list) + + whitelist = ['ETH/BTC', 'TKN/BTC'] + new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) + + assert set(whitelist) == set(new_whitelist) + + whitelist = ['ETH/BTC', 'TKN/BTC', 'TRX/ETH'] + new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) + # TRX/ETH was removed + assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) + + whitelist = ['ETH/BTC', 'TKN/BTC', 'BLK/BTC'] + new_whitelist = freqtrade.pairlists._validate_whitelist(whitelist) + # BLK/BTC is in blacklist ... + assert set(['ETH/BTC', 'TKN/BTC']) == set(new_whitelist) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index ff72ef634..2b271af31 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -655,18 +655,22 @@ def test_rpc_whitelist(mocker, default_conf) -> None: freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() - assert ret['method'] == 'static' + assert ret['method'] == 'StaticPairList' assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: patch_coinmarketcap(mocker) patch_exchange(mocker) - default_conf['dynamic_whitelist'] = 4 + default_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 4} + } + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) ret = rpc._rpc_whitelist() - assert ret['method'] == 4 + assert ret['method'] == 'VolumePairList' + assert ret['length'] == 4 assert ret['whitelist'] == default_conf['exchange']['pair_whitelist'] diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index cbb858522..15d9c20c6 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -121,15 +121,17 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.startup_messages(default_conf) + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] telegram_mock.reset_mock() default_conf['dry_run'] = True - default_conf['dynamic_whitelist'] = 20 + default_conf['whitelist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 20} + } - rpc_manager.startup_messages(default_conf) + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) assert telegram_mock.call_count == 3 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 900f583eb..cd4445a1e 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1025,7 +1025,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: telegram._whitelist(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert ('Using static whitelist with `4` pairs \n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' + assert ('Using whitelist `StaticPairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' in msg_mock.call_args_list[0][0][0]) @@ -1037,14 +1037,17 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - default_conf['dynamic_whitelist'] = 4 + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + default_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 4} + } freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._whitelist(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert ('Dynamic whitelist with `4` pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' + assert ('Using whitelist `VolumePairList` with 4 pairs\n`ETH/BTC, LTC/BTC, XRP/BTC, NEO/BTC`' in msg_mock.call_args_list[0][0][0]) diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py deleted file mode 100644 index 38df3cb38..000000000 --- a/freqtrade/tests/test_acl_pair.py +++ /dev/null @@ -1,87 +0,0 @@ -# pragma pylint: disable=missing-docstring,C0103,protected-access - -from unittest.mock import MagicMock - -from freqtrade.tests.conftest import get_patched_freqtradebot - -import pytest - -# whitelist, blacklist, filtering, all of that will -# eventually become some rules to run on a generic ACL engine -# perhaps try to anticipate that by using some python package - - -@pytest.fixture(scope="function") -def whitelist_conf(default_conf): - default_conf['stake_currency'] = 'BTC' - default_conf['exchange']['pair_whitelist'] = [ - 'ETH/BTC', - 'TKN/BTC', - 'TRST/BTC', - 'SWT/BTC', - 'BCC/BTC' - ] - default_conf['exchange']['pair_blacklist'] = [ - 'BLK/BTC' - ] - - return default_conf - - -def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf): - - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - - mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) - refreshedwhitelist = freqtradebot._refresh_whitelist( - whitelist_conf['exchange']['pair_whitelist'] + ['XXX/BTC'] - ) - # List ordered by BaseVolume - whitelist = ['ETH/BTC', 'TKN/BTC'] - # Ensure all except those in whitelist are removed - assert whitelist == refreshedwhitelist - - -def test_refresh_whitelist(mocker, markets, whitelist_conf): - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - - mocker.patch('freqtrade.exchange.Exchange.get_markets', markets) - refreshedwhitelist = freqtradebot._refresh_whitelist( - whitelist_conf['exchange']['pair_whitelist']) - - # List ordered by BaseVolume - whitelist = ['ETH/BTC', 'TKN/BTC'] - # Ensure all except those in whitelist are removed - assert whitelist == refreshedwhitelist - - -def test_refresh_whitelist_dynamic(mocker, markets, tickers, whitelist_conf): - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_markets=markets, - get_tickers=tickers, - exchange_has=MagicMock(return_value=True) - ) - - # argument: use the whitelist dynamically by exchange-volume - whitelist = ['ETH/BTC', 'TKN/BTC'] - - refreshedwhitelist = freqtradebot._refresh_whitelist( - freqtradebot._gen_pair_whitelist(whitelist_conf['stake_currency']) - ) - - assert whitelist == refreshedwhitelist - - -def test_refresh_whitelist_dynamic_empty(mocker, markets_empty, whitelist_conf): - freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty) - - # argument: use the whitelist dynamically by exchange-volume - whitelist = [] - whitelist_conf['exchange']['pair_whitelist'] = [] - freqtradebot._refresh_whitelist(whitelist) - pairslist = whitelist_conf['exchange']['pair_whitelist'] - - assert set(whitelist) == set(pairslist) diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index e09aeb1df..d28ab30af 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -17,7 +17,8 @@ def test_parse_args_none() -> None: def test_parse_args_defaults() -> None: args = Arguments([], '').get_parsed_arg() assert args.config == 'config.json' - assert args.dynamic_whitelist is None + assert args.strategy_path is None + assert args.datadir is None assert args.loglevel == 0 diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 23fefd3cd..be381770b 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -6,7 +6,7 @@ import logging from unittest.mock import MagicMock import pytest -from jsonschema import validate, ValidationError +from jsonschema import validate, ValidationError, Draft4Validator from freqtrade import constants from freqtrade import OperationalException @@ -102,7 +102,7 @@ def test_load_config(default_conf, mocker) -> None: assert validated_conf.get('strategy') == 'DefaultStrategy' assert validated_conf.get('strategy_path') is None - assert 'dynamic_whitelist' not in validated_conf + assert 'edge' not in validated_conf def test_load_config_with_params(default_conf, mocker) -> None: @@ -119,7 +119,8 @@ def test_load_config_with_params(default_conf, mocker) -> None: configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf.get('dynamic_whitelist') == 10 + assert validated_conf.get('pairlist', {}).get('method') == 'VolumePairList' + assert validated_conf.get('pairlist', {}).get('config').get('number_assets') == 10 assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' @@ -132,7 +133,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: )) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -151,7 +151,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: )) arglist = [ - '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', '--strategy-path', '/some/path' ] @@ -194,8 +193,9 @@ def test_show_info(default_conf, mocker, caplog) -> None: configuration.get_config() assert log_has( - 'Parameter --dynamic-whitelist detected. ' - 'Using dynamically generated whitelist. ' + 'Parameter --dynamic-whitelist has been deprecated, ' + 'and will be completely replaced by the whitelist dict in the future. ' + 'For now: using dynamically generated whitelist based on VolumePairList. ' '(not applicable with Backtesting and Hyperopt)', caplog.record_tuples ) @@ -486,4 +486,4 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_validate_default_conf(default_conf) -> None: - validate(default_conf, constants.CONF_SCHEMA) + validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index be84829e2..70cf8edc5 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -136,37 +136,6 @@ def test_throttle_with_assets(mocker, default_conf) -> None: assert result == -1 -def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - - # Test to retrieved BTC sorted on quoteVolume (default) - whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC') - assert whitelist == ['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC'] - - # Test to retrieve BTC sorted on bidVolume - whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC', key='bidVolume') - assert whitelist == ['LTC/BTC', 'TKN/BTC', 'ETH/BTC', 'BLK/BTC'] - - # Test with USDT sorted on quoteVolume (default) - whitelist = freqtrade._gen_pair_whitelist(base_currency='USDT') - assert whitelist == ['TKN/USDT', 'ETH/USDT', 'LTC/USDT', 'BLK/USDT'] - - # Test with ETH (our fixture does not have ETH, so result should be empty) - whitelist = freqtrade._gen_pair_whitelist(base_currency='ETH') - assert whitelist == [] - - -def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False)) - - with pytest.raises(OperationalException): - freqtrade._gen_pair_whitelist(base_currency='BTC') - - def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -248,7 +217,7 @@ def test_edge_called_in_process(mocker, edge_conf) -> None: patch_exchange(mocker) freqtrade = FreqtradeBot(edge_conf) - freqtrade._refresh_whitelist = _refresh_whitelist + freqtrade.pairlists._validate_whitelist = _refresh_whitelist patch_get_signal(freqtrade) freqtrade._process() assert freqtrade.active_pair_whitelist == ['NEO/BTC', 'LTC/BTC'] @@ -260,8 +229,8 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: patch_edge(mocker) freqtrade = FreqtradeBot(edge_conf) - assert freqtrade._get_trade_stake_amount('NEO/BTC') == (0.001 * 0.01) / 0.20 - assert freqtrade._get_trade_stake_amount('LTC/BTC') == (0.001 * 0.01) / 0.20 + assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 + assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None: @@ -342,6 +311,39 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets, assert freqtrade.handle_trade(trade) is False +def test_total_open_trades_stakes(mocker, default_conf, ticker, + limit_buy_order, fee, markets) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + default_conf['stake_amount'] = 0.0000098751 + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + get_markets=markets + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.create_trade() + trade = Trade.query.first() + + assert trade is not None + assert trade.stake_amount == 0.0000098751 + assert trade.is_open + assert trade.open_date is not None + + freqtrade.create_trade() + trade = Trade.query.order_by(Trade.id.desc()).first() + + assert trade is not None + assert trade.stake_amount == 0.0000098751 + assert trade.is_open + assert trade.open_date is not None + + assert Trade.total_open_trades_stakes() == 1.97502e-05 + + def test_get_min_pair_stake_amount(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2570,6 +2572,9 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order def test_startup_messages(default_conf, mocker): - default_conf['dynamic_whitelist'] = 20 + default_conf['pairlist'] = {'method': 'VolumePairList', + 'config': {'number_assets': 20} + } + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index d0a209f40..a7b21bc1d 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -446,6 +446,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog): # Create table using the old format engine.execute(create_table_old) + engine.execute("create index ix_trades_is_open on trades(is_open)") + engine.execute("create index ix_trades_pair on trades(pair)") engine.execute(insert_table_old) # fake previous backup diff --git a/freqtrade/tests/test_wallets.py b/freqtrade/tests/test_wallets.py index 88366a869..8d9adc74c 100644 --- a/freqtrade/tests/test_wallets.py +++ b/freqtrade/tests/test_wallets.py @@ -58,6 +58,8 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets.wallets['GAS'].used == 0.1 assert freqtrade.wallets.wallets['GAS'].total == 0.260439 assert freqtrade.wallets.get_free('GAS') == 0.270739 + assert freqtrade.wallets.get_used('GAS') == 0.1 + assert freqtrade.wallets.get_total('GAS') == 0.260439 def test_sync_wallet_missing_data(mocker, default_conf): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index bf6f8b027..59d8fa3da 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -40,6 +40,28 @@ class Wallets(object): else: return 0 + def get_used(self, currency) -> float: + + if self.exchange._conf['dry_run']: + return 999.9 + + balance = self.wallets.get(currency) + if balance and balance.used: + return balance.used + else: + return 0 + + def get_total(self, currency) -> float: + + if self.exchange._conf['dry_run']: + return 999.9 + + balance = self.wallets.get(currency) + if balance and balance.total: + return balance.total + else: + return 0 + def update(self) -> None: balances = self.exchange.get_balances() diff --git a/requirements.txt b/requirements.txt index 2befc4d5c..72c416b60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.17.581 +ccxt==1.18.13 SQLAlchemy==1.2.14 python-telegram-bot==11.1.0 arrow==0.12.1