Merge branch 'develop' into json-defaults
This commit is contained in:
		| @@ -22,6 +22,7 @@ requirements: | |||||||
|   - requirements.txt |   - requirements.txt | ||||||
|   - requirements-dev.txt |   - requirements-dev.txt | ||||||
|   - requirements-plot.txt |   - requirements-plot.txt | ||||||
|  |   - requirements-pi.txt | ||||||
|  |  | ||||||
|  |  | ||||||
| # configure the branch prefix the bot is using | # configure the branch prefix the bot is using | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| FROM python:3.7.2-slim-stretch | FROM python:3.7.3-slim-stretch | ||||||
|  |  | ||||||
| RUN apt-get update \ | RUN apt-get update \ | ||||||
|     && apt-get -y install curl build-essential libssl-dev \ |     && apt-get -y install curl build-essential libssl-dev \ | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								Dockerfile.pi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Dockerfile.pi
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | FROM balenalib/raspberrypi3-debian:stretch | ||||||
|  |  | ||||||
|  | RUN [ "cross-build-start" ] | ||||||
|  |  | ||||||
|  | RUN apt-get update \ | ||||||
|  |  && apt-get -y install wget curl build-essential libssl-dev libffi-dev \ | ||||||
|  |  && apt-get clean | ||||||
|  |  | ||||||
|  | # Prepare environment | ||||||
|  | RUN mkdir /freqtrade | ||||||
|  | WORKDIR /freqtrade | ||||||
|  |  | ||||||
|  | # Install TA-lib | ||||||
|  | COPY build_helpers/ta-lib-0.4.0-src.tar.gz /freqtrade/ | ||||||
|  | RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \ | ||||||
|  |  && cd /freqtrade/ta-lib/ \ | ||||||
|  |  && ./configure \ | ||||||
|  |  && make \ | ||||||
|  |  && make install \ | ||||||
|  |  && rm /freqtrade/ta-lib-0.4.0-src.tar.gz | ||||||
|  |  | ||||||
|  | ENV LD_LIBRARY_PATH /usr/local/lib | ||||||
|  |  | ||||||
|  | # Install berryconda | ||||||
|  | RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \ | ||||||
|  |  && bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \ | ||||||
|  |  && rm Berryconda3-2.0.0-Linux-armv7l.sh | ||||||
|  |  | ||||||
|  | # Install dependencies | ||||||
|  | COPY requirements-pi.txt /freqtrade/ | ||||||
|  | RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ | ||||||
|  |  && ~/berryconda3/bin/pip install -r requirements-pi.txt --no-cache-dir | ||||||
|  |  | ||||||
|  | # Install and execute | ||||||
|  | COPY . /freqtrade/ | ||||||
|  | RUN ~/berryconda3/bin/pip install -e . --no-cache-dir | ||||||
|  |  | ||||||
|  | RUN [ "cross-build-end" ] | ||||||
|  |  | ||||||
|  | ENTRYPOINT ["/root/berryconda3/bin/python","./freqtrade/main.py"] | ||||||
| @@ -30,7 +30,8 @@ | |||||||
|         "secret": "your_exchange_secret", |         "secret": "your_exchange_secret", | ||||||
|         "ccxt_config": {"enableRateLimit": true}, |         "ccxt_config": {"enableRateLimit": true}, | ||||||
|         "ccxt_async_config": { |         "ccxt_async_config": { | ||||||
|             "enableRateLimit": false |             "enableRateLimit": true, | ||||||
|  |             "rateLimit": 500 | ||||||
|         }, |         }, | ||||||
|         "pair_whitelist": [ |         "pair_whitelist": [ | ||||||
|             "ETH/BTC", |             "ETH/BTC", | ||||||
|   | |||||||
| @@ -30,7 +30,8 @@ | |||||||
|         "secret": "your_exchange_secret", |         "secret": "your_exchange_secret", | ||||||
|         "ccxt_config": {"enableRateLimit": true}, |         "ccxt_config": {"enableRateLimit": true}, | ||||||
|         "ccxt_async_config": { |         "ccxt_async_config": { | ||||||
|             "enableRateLimit": false |             "enableRateLimit": true, | ||||||
|  |             "rateLimit": 200 | ||||||
|         }, |         }, | ||||||
|         "pair_whitelist": [ |         "pair_whitelist": [ | ||||||
|             "AST/BTC", |             "AST/BTC", | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ | |||||||
|         "ccxt_config": {"enableRateLimit": true}, |         "ccxt_config": {"enableRateLimit": true}, | ||||||
|         "ccxt_async_config": { |         "ccxt_async_config": { | ||||||
|             "enableRateLimit": false, |             "enableRateLimit": false, | ||||||
|  |             "rateLimit": 500, | ||||||
|             "aiohttp_trust_env": false |             "aiohttp_trust_env": false | ||||||
|         }, |         }, | ||||||
|         "pair_whitelist": [ |         "pair_whitelist": [ | ||||||
|   | |||||||
| @@ -46,7 +46,6 @@ Mandatory Parameters are marked as **Required**. | |||||||
| | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | ||||||
| | `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | | `exchange.pair_whitelist` | [] | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | ||||||
| | `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | | `exchange.pair_blacklist` | [] | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param. | ||||||
| | `exchange.ccxt_rate_limit` | True | DEPRECATED!! Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange. |  | ||||||
| | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | ||||||
| | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange  and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange  and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | ||||||
| | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | ||||||
| @@ -70,8 +69,8 @@ Mandatory Parameters are marked as **Required**. | |||||||
| | `strategy` | DefaultStrategy | Defines Strategy class to use. | | `strategy` | DefaultStrategy | Defines Strategy class to use. | ||||||
| | `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). | | `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). | ||||||
| | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | ||||||
| | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. |  | ||||||
| | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | ||||||
|  | | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | ||||||
|  |  | ||||||
| ### Parameters in the strategy | ### Parameters in the strategy | ||||||
|  |  | ||||||
| @@ -217,6 +216,7 @@ The below is the default which is used if this is not configured in either strat | |||||||
|     the bot would recreate one. |     the bot would recreate one. | ||||||
|  |  | ||||||
| ### Understand order_time_in_force | ### Understand order_time_in_force | ||||||
|  |  | ||||||
| The `order_time_in_force` configuration parameter defines the policy by which the order | The `order_time_in_force` configuration parameter defines the policy by which the order | ||||||
| is executed on the exchange. Three commonly used time in force are: | is executed on the exchange. Three commonly used time in force are: | ||||||
|  |  | ||||||
| @@ -252,9 +252,9 @@ The possible values are: `gtc` (default), `fok` or `ioc`. | |||||||
|     This is an ongoing work. For now it is supported only for binance and only for buy orders. |     This is an ongoing work. For now it is supported only for binance and only for buy orders. | ||||||
|     Please don't change the default value unless you know what you are doing. |     Please don't change the default value unless you know what you are doing. | ||||||
|  |  | ||||||
| ### What values for exchange.name? | ### Exchange configuration | ||||||
|  |  | ||||||
| Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency | Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency | ||||||
| exchange markets and trading APIs. The complete up-to-date list can be found in the | exchange markets and trading APIs. The complete up-to-date list can be found in the | ||||||
| [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested | [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested | ||||||
| with only Bittrex and Binance. | with only Bittrex and Binance. | ||||||
| @@ -266,6 +266,30 @@ The bot was tested with the following exchanges: | |||||||
|  |  | ||||||
| Feel free to test other exchanges and submit your PR to improve the bot. | Feel free to test other exchanges and submit your PR to improve the bot. | ||||||
|  |  | ||||||
|  | #### Sample exchange configuration | ||||||
|  |  | ||||||
|  | A exchange configuration for "binance" would look as follows: | ||||||
|  |  | ||||||
|  | ```json | ||||||
|  | "exchange": { | ||||||
|  |     "name": "binance", | ||||||
|  |     "key": "your_exchange_key", | ||||||
|  |     "secret": "your_exchange_secret", | ||||||
|  |     "ccxt_config": {"enableRateLimit": true}, | ||||||
|  |     "ccxt_async_config": { | ||||||
|  |         "enableRateLimit": true, | ||||||
|  |         "rateLimit": 200 | ||||||
|  |     }, | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | This configuration enables binance, as well as rate limiting to avoid bans from the exchange. | ||||||
|  | `"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. | ||||||
|  |  | ||||||
|  | !!! Note | ||||||
|  |     Optimal settings for rate limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. | ||||||
|  |     We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step.  | ||||||
|  |  | ||||||
|  |  | ||||||
| ### What values can be used for fiat_display_currency? | ### What values can be used for fiat_display_currency? | ||||||
|  |  | ||||||
| The `fiat_display_currency` configuration parameter sets the base currency to use for the | The `fiat_display_currency` configuration parameter sets the base currency to use for the | ||||||
|   | |||||||
| @@ -95,9 +95,9 @@ git checkout develop | |||||||
| git checkout -b new_release | git checkout -b new_release | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| * edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) | * Edit `freqtrade/__init__.py` and add the desired version (for example `0.18.0`) | ||||||
| * Commit this part | * Commit this part | ||||||
| * push that branch to the remote and create a PR | * push that branch to the remote and create a PR against the master branch | ||||||
|  |  | ||||||
| ### create changelog from git commits | ### create changelog from git commits | ||||||
|  |  | ||||||
| @@ -108,10 +108,12 @@ git log --oneline --no-decorate --no-merges master..develop | |||||||
|  |  | ||||||
| ### Create github release / tag | ### Create github release / tag | ||||||
|  |  | ||||||
|  | * Use the button "Draft a new release" in the Github UI (subsection releases) | ||||||
| * Use the version-number specified as tag.  | * Use the version-number specified as tag.  | ||||||
| * Use "master" as reference (this step comes after the above PR is merged). | * Use "master" as reference (this step comes after the above PR is merged). | ||||||
| * use the above changelog as release comment (as codeblock) | * Use the above changelog as release comment (as codeblock) | ||||||
|  |  | ||||||
| ### After-release | ### After-release | ||||||
|  |  | ||||||
| * update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`) | * Update version in develop to next valid version and postfix that with `-dev` (`0.18.0 -> 0.18.1-dev`). | ||||||
|  | * Create a PR against develop to update that branch. | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								docs/faq.md
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								docs/faq.md
									
									
									
									
									
								
							| @@ -19,8 +19,7 @@ of course constantly aim to improve the bot but it will _always_ be a | |||||||
| gamble, which should leave you with modest wins on monthly basis but | gamble, which should leave you with modest wins on monthly basis but | ||||||
| you can't say much from few trades. | you can't say much from few trades. | ||||||
|  |  | ||||||
| #### I’d like to change the stake amount. Can I just stop the bot with | #### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? | ||||||
| /stop and then change the config.json and run it again? |  | ||||||
|  |  | ||||||
| Not quite. Trades are persisted to a database but the configuration is | Not quite. Trades are persisted to a database but the configuration is | ||||||
| currently only read when the bot is killed and restarted. `/stop` more | currently only read when the bot is killed and restarted. `/stop` more | ||||||
| @@ -31,16 +30,16 @@ like pauses. You can stop your bot, adjust settings and start it again. | |||||||
| That's great. We have a nice backtesting and hyperoptimizing setup. See | That's great. We have a nice backtesting and hyperoptimizing setup. See | ||||||
| the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). | the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). | ||||||
|  |  | ||||||
| #### Is there a setting to only SELL the coins being held and not | #### Is there a setting to only SELL the coins being held and not perform anymore BUYS? | ||||||
| perform anymore BUYS? |  | ||||||
|  |  | ||||||
| You can use the `/forcesell all` command from Telegram. | You can use the `/forcesell all` command from Telegram. | ||||||
|  |  | ||||||
| ### Hyperopt module | ### Hyperopt module | ||||||
|  |  | ||||||
| #### How many epoch do I need to get a good Hyperopt result? | #### How many epoch do I need to get a good Hyperopt result? | ||||||
|  |  | ||||||
| Per default Hyperopts without `-e` or `--epochs` parameter will only | Per default Hyperopts without `-e` or `--epochs` parameter will only | ||||||
| run 100 epochs, means 100 evals of your triggers, guards, .... Too few | run 100 epochs, means 100 evals of your triggers, guards, ... Too few | ||||||
| to find a great result (unless if you are very lucky), so you probably | to find a great result (unless if you are very lucky), so you probably | ||||||
| have to run it for 10.000 or more. But it will take an eternity to | have to run it for 10.000 or more. But it will take an eternity to | ||||||
| compute. | compute. | ||||||
| @@ -64,10 +63,10 @@ Finding a great Hyperopt results takes time. | |||||||
| If you wonder why it takes a while to find great hyperopt results | If you wonder why it takes a while to find great hyperopt results | ||||||
|  |  | ||||||
| This answer was written during the under the release 0.15.1, when we had: | This answer was written during the under the release 0.15.1, when we had: | ||||||
|  |  | ||||||
| - 8 triggers | - 8 triggers | ||||||
| - 9 guards: let's say we evaluate even 10 values from each | - 9 guards: let's say we evaluate even 10 values from each | ||||||
| - 1 stoploss calculation: let's say we want 10 values from that too to | - 1 stoploss calculation: let's say we want 10 values from that too to be evaluated | ||||||
| be evaluated |  | ||||||
|  |  | ||||||
| The following calculation is still very rough and not very precise | The following calculation is still very rough and not very precise | ||||||
| but it will give the idea. With only these triggers and guards there is | but it will give the idea. With only these triggers and guards there is | ||||||
| @@ -82,10 +81,9 @@ of the search space. | |||||||
| The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. | The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. | ||||||
|  |  | ||||||
| You can find further info on expectancy, winrate, risk management and position size in the following sources: | You can find further info on expectancy, winrate, risk management and position size in the following sources: | ||||||
| * https://www.tradeciety.com/ultimate-math-guide-for-traders/ | - https://www.tradeciety.com/ultimate-math-guide-for-traders/ | ||||||
| * http://www.vantharp.com/tharp-concepts/expectancy.asp | - http://www.vantharp.com/tharp-concepts/expectancy.asp | ||||||
| * https://samuraitradingacademy.com/trading-expectancy/ | - https://samuraitradingacademy.com/trading-expectancy/ | ||||||
| * https://www.learningmarkets.com/determining-expectancy-in-your-trading/ | - https://www.learningmarkets.com/determining-expectancy-in-your-trading/ | ||||||
| * http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ | - http://www.lonestocktrader.com/make-money-trading-positive-expectancy/ | ||||||
| * https://www.babypips.com/trading/trade-expectancy-matter | - https://www.babypips.com/trading/trade-expectancy-matter | ||||||
|  |  | ||||||
|   | |||||||
| @@ -315,7 +315,6 @@ Before installing FreqTrade on a Raspberry Pi running the official Raspbian Imag | |||||||
|  |  | ||||||
| The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. | The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. | ||||||
| It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. | It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. | ||||||
| If you have installed it from (mini)conda, you can remove `numpy`, `scipy`, and `pandas` from `requirements.txt` before you install it with `pip`. |  | ||||||
|  |  | ||||||
| Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). | Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). | ||||||
|  |  | ||||||
| @@ -327,7 +326,7 @@ conda activate freqtrade | |||||||
| conda install scipy pandas numpy | conda install scipy pandas numpy | ||||||
|  |  | ||||||
| sudo apt install libffi-dev | sudo apt install libffi-dev | ||||||
| python3 -m pip install -r requirements.txt | python3 -m pip install -r requirements-pi.txt | ||||||
| python3 -m pip install -e . | python3 -m pip install -e . | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| """ FreqTrade bot """ | """ FreqTrade bot """ | ||||||
| __version__ = '0.18.2-dev' | __version__ = '0.18.5-dev' | ||||||
|  |  | ||||||
|  |  | ||||||
| class DependencyException(BaseException): | class DependencyException(BaseException): | ||||||
|   | |||||||
| @@ -247,6 +247,22 @@ class Arguments(object): | |||||||
|             dest='timerange', |             dest='timerange', | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         parser.add_argument( | ||||||
|  |             '--max_open_trades', | ||||||
|  |             help='Specify max_open_trades to use.', | ||||||
|  |             default=None, | ||||||
|  |             type=int, | ||||||
|  |             dest='max_open_trades', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         parser.add_argument( | ||||||
|  |             '--stake_amount', | ||||||
|  |             help='Specify stake_amount.', | ||||||
|  |             default=None, | ||||||
|  |             type=float, | ||||||
|  |             dest='stake_amount', | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def hyperopt_options(parser: argparse.ArgumentParser) -> None: |     def hyperopt_options(parser: argparse.ArgumentParser) -> None: | ||||||
|         """ |         """ | ||||||
| @@ -267,7 +283,6 @@ class Arguments(object): | |||||||
|             dest='position_stacking', |             dest='position_stacking', | ||||||
|             default=False |             default=False | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|             '--dmmp', '--disable-max-market-positions', |             '--dmmp', '--disable-max-market-positions', | ||||||
|             help='Disable applying `max_open_trades` during backtest ' |             help='Disable applying `max_open_trades` during backtest ' | ||||||
| @@ -293,6 +308,13 @@ class Arguments(object): | |||||||
|             nargs='+', |             nargs='+', | ||||||
|             dest='spaces', |             dest='spaces', | ||||||
|         ) |         ) | ||||||
|  |         parser.add_argument( | ||||||
|  |             '--print-all', | ||||||
|  |             help='Print all results, not only the best ones.', | ||||||
|  |             action='store_true', | ||||||
|  |             dest='print_all', | ||||||
|  |             default=False | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     def _build_subcommands(self) -> None: |     def _build_subcommands(self) -> None: | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -9,11 +9,11 @@ from argparse import Namespace | |||||||
| from logging.handlers import RotatingFileHandler | from logging.handlers import RotatingFileHandler | ||||||
| from typing import Any, Dict, List, Optional | from typing import Any, Dict, List, Optional | ||||||
|  |  | ||||||
| import ccxt |  | ||||||
| from jsonschema import Draft4Validator, validators | from jsonschema import Draft4Validator, validators | ||||||
| from jsonschema.exceptions import ValidationError, best_match | from jsonschema.exceptions import ValidationError, best_match | ||||||
|  |  | ||||||
| from freqtrade import OperationalException, constants | from freqtrade import OperationalException, constants | ||||||
|  | from freqtrade.exchange import is_exchange_supported, supported_exchanges | ||||||
| from freqtrade.misc import deep_merge_dicts | from freqtrade.misc import deep_merge_dicts | ||||||
| from freqtrade.state import RunMode | from freqtrade.state import RunMode | ||||||
|  |  | ||||||
| @@ -216,7 +216,7 @@ class Configuration(object): | |||||||
|             logger.info(f'Created data directory: {datadir}') |             logger.info(f'Created data directory: {datadir}') | ||||||
|         return datadir |         return datadir | ||||||
|  |  | ||||||
|     def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]: |     def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:  # noqa: C901 | ||||||
|         """ |         """ | ||||||
|         Extract information for sys.argv and load Backtesting configuration |         Extract information for sys.argv and load Backtesting configuration | ||||||
|         :return: configuration as dictionary |         :return: configuration as dictionary | ||||||
| @@ -239,14 +239,24 @@ class Configuration(object): | |||||||
|             config.update({'position_stacking': True}) |             config.update({'position_stacking': True}) | ||||||
|             logger.info('Parameter --enable-position-stacking detected ...') |             logger.info('Parameter --enable-position-stacking detected ...') | ||||||
|  |  | ||||||
|         # If --disable-max-market-positions is used we add it to the configuration |         # If --disable-max-market-positions or --max_open_trades is used we update configuration | ||||||
|         if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: |         if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: | ||||||
|             config.update({'use_max_market_positions': False}) |             config.update({'use_max_market_positions': False}) | ||||||
|             logger.info('Parameter --disable-max-market-positions detected ...') |             logger.info('Parameter --disable-max-market-positions detected ...') | ||||||
|             logger.info('max_open_trades set to unlimited ...') |             logger.info('max_open_trades set to unlimited ...') | ||||||
|  |         elif 'max_open_trades' in self.args and self.args.max_open_trades: | ||||||
|  |             config.update({'max_open_trades': self.args.max_open_trades}) | ||||||
|  |             logger.info('Parameter --max_open_trades detected, ' | ||||||
|  |                         'overriding max_open_trades to: %s ...', config.get('max_open_trades')) | ||||||
|         else: |         else: | ||||||
|             logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) |             logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) | ||||||
|  |  | ||||||
|  |         # If --stake_amount is used we update configuration | ||||||
|  |         if 'stake_amount' in self.args and self.args.stake_amount: | ||||||
|  |             config.update({'stake_amount': self.args.stake_amount}) | ||||||
|  |             logger.info('Parameter --stake_amount detected, overriding stake_amount to: %s ...', | ||||||
|  |                         config.get('stake_amount')) | ||||||
|  |  | ||||||
|         # If --timerange is used we add it to the configuration |         # If --timerange is used we add it to the configuration | ||||||
|         if 'timerange' in self.args and self.args.timerange: |         if 'timerange' in self.args and self.args.timerange: | ||||||
|             config.update({'timerange': self.args.timerange}) |             config.update({'timerange': self.args.timerange}) | ||||||
| @@ -331,6 +341,10 @@ class Configuration(object): | |||||||
|             config.update({'spaces': self.args.spaces}) |             config.update({'spaces': self.args.spaces}) | ||||||
|             logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) |             logger.info('Parameter -s/--spaces detected: %s', config.get('spaces')) | ||||||
|  |  | ||||||
|  |         if 'print_all' in self.args and self.args.print_all: | ||||||
|  |             config.update({'print_all': self.args.print_all}) | ||||||
|  |             logger.info('Parameter --print-all detected: %s', config.get('print_all')) | ||||||
|  |  | ||||||
|         return config |         return config | ||||||
|  |  | ||||||
|     def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: |     def _validate_config_schema(self, conf: Dict[str, Any]) -> Dict[str, Any]: | ||||||
| @@ -396,20 +410,16 @@ class Configuration(object): | |||||||
|         :return: True or raised an exception if the exchange if not supported |         :return: True or raised an exception if the exchange if not supported | ||||||
|         """ |         """ | ||||||
|         exchange = config.get('exchange', {}).get('name').lower() |         exchange = config.get('exchange', {}).get('name').lower() | ||||||
|         if exchange not in ccxt.exchanges: |         if not is_exchange_supported(exchange): | ||||||
|  |  | ||||||
|             exception_msg = f'Exchange "{exchange}" not supported.\n' \ |             exception_msg = f'Exchange "{exchange}" not supported.\n' \ | ||||||
|                             f'The following exchanges are supported: {", ".join(ccxt.exchanges)}' |                             f'The following exchanges are supported: ' \ | ||||||
|  |                             f'{", ".join(supported_exchanges())}' | ||||||
|  |  | ||||||
|             logger.critical(exception_msg) |             logger.critical(exception_msg) | ||||||
|             raise OperationalException( |             raise OperationalException( | ||||||
|                 exception_msg |                 exception_msg | ||||||
|             ) |             ) | ||||||
|         # Depreciation warning |  | ||||||
|         if 'ccxt_rate_limit' in config.get('exchange', {}): |  | ||||||
|             logger.warning("`ccxt_rate_limit` has been deprecated in favor of " |  | ||||||
|                            "`ccxt_config` and `ccxt_async_config` and will be removed " |  | ||||||
|                            "in a future version.") |  | ||||||
|  |  | ||||||
|         logger.debug('Exchange "%s" supported', exchange) |         logger.debug('Exchange "%s" supported', exchange) | ||||||
|         return True |         return True | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ | |||||||
| Functions to convert data from one format to another | Functions to convert data from one format to another | ||||||
| """ | """ | ||||||
| import logging | import logging | ||||||
|  |  | ||||||
| import pandas as pd | import pandas as pd | ||||||
| from pandas import DataFrame, to_datetime | from pandas import DataFrame, to_datetime | ||||||
| from freqtrade.misc import timeframe_to_minutes |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -58,6 +58,8 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, ticker_interval: str) -> Da | |||||||
|     using the previous close as price for "open", "high" "low" and "close", volume is set to 0 |     using the previous close as price for "open", "high" "low" and "close", volume is set to 0 | ||||||
|  |  | ||||||
|     """ |     """ | ||||||
|  |     from freqtrade.exchange import timeframe_to_minutes | ||||||
|  |  | ||||||
|     ohlc_dict = { |     ohlc_dict = { | ||||||
|         'open': 'first', |         'open': 'first', | ||||||
|         'high': 'max', |         'high': 'max', | ||||||
|   | |||||||
| @@ -37,23 +37,23 @@ class DataProvider(object): | |||||||
|     @property |     @property | ||||||
|     def available_pairs(self) -> List[Tuple[str, str]]: |     def available_pairs(self) -> List[Tuple[str, str]]: | ||||||
|         """ |         """ | ||||||
|         Return a list of tuples containing pair, tick_interval for which data is currently cached. |         Return a list of tuples containing pair, ticker_interval for which data is currently cached. | ||||||
|         Should be whitelist + open trades. |         Should be whitelist + open trades. | ||||||
|         """ |         """ | ||||||
|         return list(self._exchange._klines.keys()) |         return list(self._exchange._klines.keys()) | ||||||
|  |  | ||||||
|     def ohlcv(self, pair: str, tick_interval: str = None, copy: bool = True) -> DataFrame: |     def ohlcv(self, pair: str, ticker_interval: str = None, copy: bool = True) -> DataFrame: | ||||||
|         """ |         """ | ||||||
|         get ohlcv data for the given pair as DataFrame |         get ohlcv data for the given pair as DataFrame | ||||||
|         Please check `available_pairs` to verify which pairs are currently cached. |         Please check `available_pairs` to verify which pairs are currently cached. | ||||||
|         :param pair: pair to get the data for |         :param pair: pair to get the data for | ||||||
|         :param tick_interval: ticker_interval to get pair for |         :param ticker_interval: ticker_interval to get pair for | ||||||
|         :param copy: copy dataframe before returning. |         :param copy: copy dataframe before returning. | ||||||
|                      Use false only for RO operations (where the dataframe is not modified) |                      Use false only for RO operations (where the dataframe is not modified) | ||||||
|         """ |         """ | ||||||
|         if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): |         if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): | ||||||
|             if tick_interval: |             if ticker_interval: | ||||||
|                 pairtick = (pair, tick_interval) |                 pairtick = (pair, ticker_interval) | ||||||
|             else: |             else: | ||||||
|                 pairtick = (pair, self._config['ticker_interval']) |                 pairtick = (pair, self._config['ticker_interval']) | ||||||
|  |  | ||||||
| @@ -65,7 +65,7 @@ class DataProvider(object): | |||||||
|         """ |         """ | ||||||
|         get stored historic ohlcv data |         get stored historic ohlcv data | ||||||
|         :param pair: pair to get the data for |         :param pair: pair to get the data for | ||||||
|         :param tick_interval: ticker_interval to get pair for |         :param ticker_interval: ticker_interval to get pair for | ||||||
|         """ |         """ | ||||||
|         return load_pair_history(pair=pair, |         return load_pair_history(pair=pair, | ||||||
|                                  ticker_interval=ticker_interval, |                                  ticker_interval=ticker_interval, | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| """ | """ | ||||||
| Handle historic data (ohlcv). | Handle historic data (ohlcv). | ||||||
| includes: |  | ||||||
|  | Includes: | ||||||
| * load data for a pair (or a list of pairs) from disk | * load data for a pair (or a list of pairs) from disk | ||||||
| * download data from exchange and store to disk | * download data from exchange and store to disk | ||||||
| """ | """ | ||||||
|  |  | ||||||
| import logging | import logging | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from typing import Optional, List, Dict, Tuple, Any | from typing import Optional, List, Dict, Tuple, Any | ||||||
| @@ -15,8 +15,7 @@ from pandas import DataFrame | |||||||
| from freqtrade import misc, OperationalException | from freqtrade import misc, OperationalException | ||||||
| from freqtrade.arguments import TimeRange | from freqtrade.arguments import TimeRange | ||||||
| from freqtrade.data.converter import parse_ticker_dataframe | from freqtrade.data.converter import parse_ticker_dataframe | ||||||
| from freqtrade.exchange import Exchange | from freqtrade.exchange import Exchange, timeframe_to_minutes | ||||||
| from freqtrade.misc import timeframe_to_minutes |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -101,7 +100,7 @@ def load_pair_history(pair: str, | |||||||
|         download_pair_history(datadir=datadir, |         download_pair_history(datadir=datadir, | ||||||
|                               exchange=exchange, |                               exchange=exchange, | ||||||
|                               pair=pair, |                               pair=pair, | ||||||
|                               tick_interval=ticker_interval, |                               ticker_interval=ticker_interval, | ||||||
|                               timerange=timerange) |                               timerange=timerange) | ||||||
|  |  | ||||||
|     pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) |     pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) | ||||||
| @@ -151,7 +150,7 @@ def make_testdata_path(datadir: Optional[Path]) -> Path: | |||||||
|     return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() |     return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve() | ||||||
|  |  | ||||||
|  |  | ||||||
| def load_cached_data_for_updating(filename: Path, tick_interval: str, | def load_cached_data_for_updating(filename: Path, ticker_interval: str, | ||||||
|                                   timerange: Optional[TimeRange]) -> Tuple[List[Any], |                                   timerange: Optional[TimeRange]) -> Tuple[List[Any], | ||||||
|                                                                            Optional[int]]: |                                                                            Optional[int]]: | ||||||
|     """ |     """ | ||||||
| @@ -165,7 +164,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, | |||||||
|         if timerange.starttype == 'date': |         if timerange.starttype == 'date': | ||||||
|             since_ms = timerange.startts * 1000 |             since_ms = timerange.startts * 1000 | ||||||
|         elif timerange.stoptype == 'line': |         elif timerange.stoptype == 'line': | ||||||
|             num_minutes = timerange.stopts * timeframe_to_minutes(tick_interval) |             num_minutes = timerange.stopts * timeframe_to_minutes(ticker_interval) | ||||||
|             since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 |             since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000 | ||||||
|  |  | ||||||
|     # read the cached file |     # read the cached file | ||||||
| @@ -192,7 +191,7 @@ def load_cached_data_for_updating(filename: Path, tick_interval: str, | |||||||
| def download_pair_history(datadir: Optional[Path], | def download_pair_history(datadir: Optional[Path], | ||||||
|                           exchange: Exchange, |                           exchange: Exchange, | ||||||
|                           pair: str, |                           pair: str, | ||||||
|                           tick_interval: str = '5m', |                           ticker_interval: str = '5m', | ||||||
|                           timerange: Optional[TimeRange] = None) -> bool: |                           timerange: Optional[TimeRange] = None) -> bool: | ||||||
|     """ |     """ | ||||||
|     Download the latest ticker intervals from the exchange for the pair passed in parameters |     Download the latest ticker intervals from the exchange for the pair passed in parameters | ||||||
| @@ -202,7 +201,7 @@ def download_pair_history(datadir: Optional[Path], | |||||||
|  |  | ||||||
|     Based on @Rybolov work: https://github.com/rybolov/freqtrade-data |     Based on @Rybolov work: https://github.com/rybolov/freqtrade-data | ||||||
|     :param pair: pair to download |     :param pair: pair to download | ||||||
|     :param tick_interval: ticker interval |     :param ticker_interval: ticker interval | ||||||
|     :param timerange: range of time to download |     :param timerange: range of time to download | ||||||
|     :return: bool with success state |     :return: bool with success state | ||||||
|  |  | ||||||
| @@ -210,17 +209,17 @@ def download_pair_history(datadir: Optional[Path], | |||||||
|     try: |     try: | ||||||
|         path = make_testdata_path(datadir) |         path = make_testdata_path(datadir) | ||||||
|         filepair = pair.replace("/", "_") |         filepair = pair.replace("/", "_") | ||||||
|         filename = path.joinpath(f'{filepair}-{tick_interval}.json') |         filename = path.joinpath(f'{filepair}-{ticker_interval}.json') | ||||||
|  |  | ||||||
|         logger.info('Download the pair: "%s", Interval: %s', pair, tick_interval) |         logger.info('Download the pair: "%s", Interval: %s', pair, ticker_interval) | ||||||
|  |  | ||||||
|         data, since_ms = load_cached_data_for_updating(filename, tick_interval, timerange) |         data, since_ms = load_cached_data_for_updating(filename, ticker_interval, timerange) | ||||||
|  |  | ||||||
|         logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') |         logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') | ||||||
|         logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') |         logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') | ||||||
|  |  | ||||||
|         # Default since_ms to 30 days if nothing is given |         # Default since_ms to 30 days if nothing is given | ||||||
|         new_data = exchange.get_history(pair=pair, tick_interval=tick_interval, |         new_data = exchange.get_history(pair=pair, ticker_interval=ticker_interval, | ||||||
|                                         since_ms=since_ms if since_ms |                                         since_ms=since_ms if since_ms | ||||||
|                                         else |                                         else | ||||||
|                                         int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) |                                         int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000) | ||||||
| @@ -233,5 +232,5 @@ def download_pair_history(datadir: Optional[Path], | |||||||
|         return True |         return True | ||||||
|     except BaseException: |     except BaseException: | ||||||
|         logger.info('Failed to download the pair: "%s", Interval: %s', |         logger.info('Failed to download the pair: "%s", Interval: %s', | ||||||
|                     pair, tick_interval) |                     pair, ticker_interval) | ||||||
|         return False |         return False | ||||||
|   | |||||||
| @@ -1,3 +1,8 @@ | |||||||
| from freqtrade.exchange.exchange import Exchange  # noqa: F401 | from freqtrade.exchange.exchange import Exchange  # noqa: F401 | ||||||
|  | from freqtrade.exchange.exchange import (is_exchange_supported,  # noqa: F401 | ||||||
|  |                                          supported_exchanges) | ||||||
|  | from freqtrade.exchange.exchange import (timeframe_to_seconds,  # noqa: F401 | ||||||
|  |                                          timeframe_to_minutes, | ||||||
|  |                                          timeframe_to_msecs) | ||||||
| from freqtrade.exchange.kraken import Kraken  # noqa: F401 | from freqtrade.exchange.kraken import Kraken  # noqa: F401 | ||||||
| from freqtrade.exchange.binance import Binance  # noqa: F401 | from freqtrade.exchange.binance import Binance  # noqa: F401 | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| # pragma pylint: disable=W0603 | # pragma pylint: disable=W0603 | ||||||
| """ Cryptocurrency Exchanges support """ | """ | ||||||
|  | Cryptocurrency Exchanges support | ||||||
|  | """ | ||||||
| import logging | import logging | ||||||
| import inspect | import inspect | ||||||
| from random import randint | from random import randint | ||||||
| @@ -16,7 +18,6 @@ from pandas import DataFrame | |||||||
| from freqtrade import (constants, DependencyException, OperationalException, | from freqtrade import (constants, DependencyException, OperationalException, | ||||||
|                        TemporaryError, InvalidOrderException) |                        TemporaryError, InvalidOrderException) | ||||||
| from freqtrade.data.converter import parse_ticker_dataframe | from freqtrade.data.converter import parse_ticker_dataframe | ||||||
| from freqtrade.misc import timeframe_to_seconds, timeframe_to_msecs |  | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -138,7 +139,7 @@ class Exchange(object): | |||||||
|         # Find matching class for the given exchange name |         # Find matching class for the given exchange name | ||||||
|         name = exchange_config['name'] |         name = exchange_config['name'] | ||||||
|  |  | ||||||
|         if name not in ccxt_module.exchanges: |         if not is_exchange_supported(name, ccxt_module): | ||||||
|             raise OperationalException(f'Exchange {name} is not supported') |             raise OperationalException(f'Exchange {name} is not supported') | ||||||
|  |  | ||||||
|         ex_config = { |         ex_config = { | ||||||
| @@ -146,7 +147,6 @@ class Exchange(object): | |||||||
|             'secret': exchange_config.get('secret'), |             'secret': exchange_config.get('secret'), | ||||||
|             'password': exchange_config.get('password'), |             'password': exchange_config.get('password'), | ||||||
|             'uid': exchange_config.get('uid', ''), |             'uid': exchange_config.get('uid', ''), | ||||||
|             'enableRateLimit': exchange_config.get('ccxt_rate_limit', True) |  | ||||||
|         } |         } | ||||||
|         if ccxt_kwargs: |         if ccxt_kwargs: | ||||||
|             logger.info('Applying additional ccxt config: %s', ccxt_kwargs) |             logger.info('Applying additional ccxt config: %s', ccxt_kwargs) | ||||||
| @@ -490,26 +490,26 @@ class Exchange(object): | |||||||
|             logger.info("returning cached ticker-data for %s", pair) |             logger.info("returning cached ticker-data for %s", pair) | ||||||
|             return self._cached_ticker[pair] |             return self._cached_ticker[pair] | ||||||
|  |  | ||||||
|     def get_history(self, pair: str, tick_interval: str, |     def get_history(self, pair: str, ticker_interval: str, | ||||||
|                     since_ms: int) -> List: |                     since_ms: int) -> List: | ||||||
|         """ |         """ | ||||||
|         Gets candle history using asyncio and returns the list of candles. |         Gets candle history using asyncio and returns the list of candles. | ||||||
|         Handles all async doing. |         Handles all async doing. | ||||||
|         """ |         """ | ||||||
|         return asyncio.get_event_loop().run_until_complete( |         return asyncio.get_event_loop().run_until_complete( | ||||||
|             self._async_get_history(pair=pair, tick_interval=tick_interval, |             self._async_get_history(pair=pair, ticker_interval=ticker_interval, | ||||||
|                                     since_ms=since_ms)) |                                     since_ms=since_ms)) | ||||||
|  |  | ||||||
|     async def _async_get_history(self, pair: str, |     async def _async_get_history(self, pair: str, | ||||||
|                                  tick_interval: str, |                                  ticker_interval: str, | ||||||
|                                  since_ms: int) -> List: |                                  since_ms: int) -> List: | ||||||
|         # Assume exchange returns 500 candles |         # Assume exchange returns 500 candles | ||||||
|         _LIMIT = 500 |         _LIMIT = 500 | ||||||
|  |  | ||||||
|         one_call = timeframe_to_msecs(tick_interval) * _LIMIT |         one_call = timeframe_to_msecs(ticker_interval) * _LIMIT | ||||||
|         logger.debug("one_call: %s msecs", one_call) |         logger.debug("one_call: %s msecs", one_call) | ||||||
|         input_coroutines = [self._async_get_candle_history( |         input_coroutines = [self._async_get_candle_history( | ||||||
|             pair, tick_interval, since) for since in |             pair, ticker_interval, since) for since in | ||||||
|             range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] |             range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] | ||||||
|  |  | ||||||
|         tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) |         tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) | ||||||
| @@ -549,14 +549,14 @@ class Exchange(object): | |||||||
|                 logger.warning("Async code raised an exception: %s", res.__class__.__name__) |                 logger.warning("Async code raised an exception: %s", res.__class__.__name__) | ||||||
|                 continue |                 continue | ||||||
|             pair = res[0] |             pair = res[0] | ||||||
|             tick_interval = res[1] |             ticker_interval = res[1] | ||||||
|             ticks = res[2] |             ticks = res[2] | ||||||
|             # keeping last candle time as last refreshed time of the pair |             # keeping last candle time as last refreshed time of the pair | ||||||
|             if ticks: |             if ticks: | ||||||
|                 self._pairs_last_refresh_time[(pair, tick_interval)] = ticks[-1][0] // 1000 |                 self._pairs_last_refresh_time[(pair, ticker_interval)] = ticks[-1][0] // 1000 | ||||||
|             # keeping parsed dataframe in cache |             # keeping parsed dataframe in cache | ||||||
|             self._klines[(pair, tick_interval)] = parse_ticker_dataframe( |             self._klines[(pair, ticker_interval)] = parse_ticker_dataframe( | ||||||
|                 ticks, tick_interval, fill_missing=True) |                 ticks, ticker_interval, fill_missing=True) | ||||||
|         return tickers |         return tickers | ||||||
|  |  | ||||||
|     def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: |     def _now_is_time_to_refresh(self, pair: str, ticker_interval: str) -> bool: | ||||||
| @@ -567,17 +567,17 @@ class Exchange(object): | |||||||
|                      + interval_in_sec) >= arrow.utcnow().timestamp) |                      + interval_in_sec) >= arrow.utcnow().timestamp) | ||||||
|  |  | ||||||
|     @retrier_async |     @retrier_async | ||||||
|     async def _async_get_candle_history(self, pair: str, tick_interval: str, |     async def _async_get_candle_history(self, pair: str, ticker_interval: str, | ||||||
|                                         since_ms: Optional[int] = None) -> Tuple[str, str, List]: |                                         since_ms: Optional[int] = None) -> Tuple[str, str, List]: | ||||||
|         """ |         """ | ||||||
|         Asyncronously gets candle histories using fetch_ohlcv |         Asyncronously gets candle histories using fetch_ohlcv | ||||||
|         returns tuple: (pair, tick_interval, ohlcv_list) |         returns tuple: (pair, ticker_interval, ohlcv_list) | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             # fetch ohlcv asynchronously |             # fetch ohlcv asynchronously | ||||||
|             logger.debug("fetching %s, %s since %s ...", pair, tick_interval, since_ms) |             logger.debug("fetching %s, %s since %s ...", pair, ticker_interval, since_ms) | ||||||
|  |  | ||||||
|             data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval, |             data = await self._api_async.fetch_ohlcv(pair, timeframe=ticker_interval, | ||||||
|                                                      since=since_ms) |                                                      since=since_ms) | ||||||
|  |  | ||||||
|             # Because some exchange sort Tickers ASC and other DESC. |             # Because some exchange sort Tickers ASC and other DESC. | ||||||
| @@ -589,9 +589,9 @@ class Exchange(object): | |||||||
|                     data = sorted(data, key=lambda x: x[0]) |                     data = sorted(data, key=lambda x: x[0]) | ||||||
|             except IndexError: |             except IndexError: | ||||||
|                 logger.exception("Error loading %s. Result was %s.", pair, data) |                 logger.exception("Error loading %s. Result was %s.", pair, data) | ||||||
|                 return pair, tick_interval, [] |                 return pair, ticker_interval, [] | ||||||
|             logger.debug("done fetching %s, %s ...", pair, tick_interval) |             logger.debug("done fetching %s, %s ...", pair, ticker_interval) | ||||||
|             return pair, tick_interval, data |             return pair, ticker_interval, data | ||||||
|  |  | ||||||
|         except ccxt.NotSupported as e: |         except ccxt.NotSupported as e: | ||||||
|             raise OperationalException( |             raise OperationalException( | ||||||
| @@ -690,3 +690,34 @@ class Exchange(object): | |||||||
|                 f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') |                 f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') | ||||||
|         except ccxt.BaseError as e: |         except ccxt.BaseError as e: | ||||||
|             raise OperationalException(e) |             raise OperationalException(e) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def is_exchange_supported(exchange: str, ccxt_module=None) -> bool: | ||||||
|  |     return exchange in supported_exchanges(ccxt_module) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def supported_exchanges(ccxt_module=None) -> List[str]: | ||||||
|  |     return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def timeframe_to_seconds(ticker_interval: str) -> int: | ||||||
|  |     """ | ||||||
|  |     Translates the timeframe interval value written in the human readable | ||||||
|  |     form ('1m', '5m', '1h', '1d', '1w', etc.) to the number | ||||||
|  |     of seconds for one timeframe interval. | ||||||
|  |     """ | ||||||
|  |     return ccxt.Exchange.parse_timeframe(ticker_interval) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def timeframe_to_minutes(ticker_interval: str) -> int: | ||||||
|  |     """ | ||||||
|  |     Same as above, but returns minutes. | ||||||
|  |     """ | ||||||
|  |     return ccxt.Exchange.parse_timeframe(ticker_interval) // 60 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def timeframe_to_msecs(ticker_interval: str) -> int: | ||||||
|  |     """ | ||||||
|  |     Same as above, but returns milliseconds. | ||||||
|  |     """ | ||||||
|  |     return ccxt.Exchange.parse_timeframe(ticker_interval) * 1000 | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ from freqtrade import (DependencyException, OperationalException, InvalidOrderEx | |||||||
| from freqtrade.data.converter import order_book_to_dataframe | from freqtrade.data.converter import order_book_to_dataframe | ||||||
| from freqtrade.data.dataprovider import DataProvider | from freqtrade.data.dataprovider import DataProvider | ||||||
| from freqtrade.edge import Edge | from freqtrade.edge import Edge | ||||||
| from freqtrade.misc import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.persistence import Trade | from freqtrade.persistence import Trade | ||||||
| from freqtrade.rpc import RPCManager, RPCMessageType | from freqtrade.rpc import RPCManager, RPCMessageType | ||||||
| from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver | from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver | ||||||
| @@ -460,7 +460,7 @@ class FreqtradeBot(object): | |||||||
|     def get_real_amount(self, trade: Trade, order: Dict) -> float: |     def get_real_amount(self, trade: Trade, order: Dict) -> float: | ||||||
|         """ |         """ | ||||||
|         Get real amount for the trade |         Get real amount for the trade | ||||||
|         Necessary for self.exchanges which charge fees in base currency (e.g. binance) |         Necessary for exchanges which charge fees in base currency (e.g. binance) | ||||||
|         """ |         """ | ||||||
|         order_amount = order['amount'] |         order_amount = order['amount'] | ||||||
|         # Only run for closed orders |         # Only run for closed orders | ||||||
| @@ -522,6 +522,10 @@ class FreqtradeBot(object): | |||||||
|  |  | ||||||
|             trade.update(order) |             trade.update(order) | ||||||
|  |  | ||||||
|  |             # Updating wallets when order is closed | ||||||
|  |             if not trade.is_open: | ||||||
|  |                 self.wallets.update() | ||||||
|  |  | ||||||
|     def get_sell_rate(self, pair: str, refresh: bool) -> float: |     def get_sell_rate(self, pair: str, refresh: bool) -> float: | ||||||
|         """ |         """ | ||||||
|         Get sell rate - either using get-ticker bid or first bid based on orderbook |         Get sell rate - either using get-ticker bid or first bid based on orderbook | ||||||
|   | |||||||
| @@ -1,18 +1,17 @@ | |||||||
| """ | """ | ||||||
| Various tool function for Freqtrade and scripts | Various tool function for Freqtrade and scripts | ||||||
| """ | """ | ||||||
|  |  | ||||||
| import gzip | import gzip | ||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from typing import Dict | from typing import Dict | ||||||
|  |  | ||||||
| from ccxt import Exchange |  | ||||||
| import numpy as np | import numpy as np | ||||||
| from pandas import DataFrame | from pandas import DataFrame | ||||||
| import rapidjson | import rapidjson | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -132,26 +131,3 @@ def deep_merge_dicts(source, destination): | |||||||
|             destination[key] = value |             destination[key] = value | ||||||
|  |  | ||||||
|     return destination |     return destination | ||||||
|  |  | ||||||
|  |  | ||||||
| def timeframe_to_seconds(ticker_interval: str) -> int: |  | ||||||
|     """ |  | ||||||
|     Translates the timeframe interval value written in the human readable |  | ||||||
|     form ('1m', '5m', '1h', '1d', '1w', etc.) to the number |  | ||||||
|     of seconds for one timeframe interval. |  | ||||||
|     """ |  | ||||||
|     return Exchange.parse_timeframe(ticker_interval) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def timeframe_to_minutes(ticker_interval: str) -> int: |  | ||||||
|     """ |  | ||||||
|     Same as above, but returns minutes. |  | ||||||
|     """ |  | ||||||
|     return Exchange.parse_timeframe(ticker_interval) // 60 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def timeframe_to_msecs(ticker_interval: str) -> int: |  | ||||||
|     """ |  | ||||||
|     Same as above, but returns milliseconds. |  | ||||||
|     """ |  | ||||||
|     return Exchange.parse_timeframe(ticker_interval) * 1000 |  | ||||||
|   | |||||||
| @@ -19,12 +19,14 @@ from freqtrade.arguments import Arguments | |||||||
| from freqtrade.configuration import Configuration | from freqtrade.configuration import Configuration | ||||||
| from freqtrade.data import history | from freqtrade.data import history | ||||||
| from freqtrade.data.dataprovider import DataProvider | from freqtrade.data.dataprovider import DataProvider | ||||||
| from freqtrade.misc import file_dump_json, timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
|  | from freqtrade.misc import file_dump_json | ||||||
| from freqtrade.persistence import Trade | from freqtrade.persistence import Trade | ||||||
| from freqtrade.resolvers import ExchangeResolver, StrategyResolver | from freqtrade.resolvers import ExchangeResolver, StrategyResolver | ||||||
| from freqtrade.state import RunMode | from freqtrade.state import RunMode | ||||||
| from freqtrade.strategy.interface import SellType, IStrategy | from freqtrade.strategy.interface import SellType, IStrategy | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -115,7 +115,7 @@ class Hyperopt(Backtesting): | |||||||
|         """ |         """ | ||||||
|         Log results if it is better than any previous evaluation |         Log results if it is better than any previous evaluation | ||||||
|         """ |         """ | ||||||
|         if results['loss'] < self.current_best_loss: |         if self.config.get('print_all', False) or results['loss'] < self.current_best_loss: | ||||||
|             current = results['current_tries'] |             current = results['current_tries'] | ||||||
|             total = results['total_tries'] |             total = results['total_tries'] | ||||||
|             res = results['result'] |             res = results['result'] | ||||||
|   | |||||||
| @@ -157,12 +157,15 @@ class StrategyResolver(IResolver): | |||||||
|                         getfullargspec(strategy.populate_indicators).args) |                         getfullargspec(strategy.populate_indicators).args) | ||||||
|                     strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) |                     strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) | ||||||
|                     strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) |                     strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) | ||||||
|  |                     try: | ||||||
|                     return import_strategy(strategy, config=config) |                         return import_strategy(strategy, config=config) | ||||||
|  |                     except TypeError as e: | ||||||
|  |                         logger.warning( | ||||||
|  |                             f"Impossible to load strategy '{strategy}' from {_path}. Error: {e}") | ||||||
|             except FileNotFoundError: |             except FileNotFoundError: | ||||||
|                 logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) |                 logger.warning('Path "%s" does not exist', _path.relative_to(Path.cwd())) | ||||||
|  |  | ||||||
|         raise ImportError( |         raise ImportError( | ||||||
|             "Impossible to load Strategy '{}'. This class does not exist" |             f"Impossible to load Strategy '{strategy_name}'. This class does not exist" | ||||||
|             " or contains Python code errors".format(strategy_name) |             " or contains Python code errors" | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -20,6 +20,9 @@ logger = logging.getLogger(__name__) | |||||||
| logger.debug('Included module rpc.telegram ...') | logger.debug('Included module rpc.telegram ...') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | MAX_TELEGRAM_MESSAGE_LENGTH = 4096 | ||||||
|  |  | ||||||
|  |  | ||||||
| def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: | def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: | ||||||
|     """ |     """ | ||||||
|     Decorator to check if the message comes from the correct chat_id |     Decorator to check if the message comes from the correct chat_id | ||||||
| @@ -266,7 +269,8 @@ class Telegram(RPC): | |||||||
|                                  headers=[ |                                  headers=[ | ||||||
|                                      'Day', |                                      'Day', | ||||||
|                                      f'Profit {stake_cur}', |                                      f'Profit {stake_cur}', | ||||||
|                                      f'Profit {fiat_disp_cur}' |                                      f'Profit {fiat_disp_cur}', | ||||||
|  |                                      f'Trades' | ||||||
|                                  ], |                                  ], | ||||||
|                                  tablefmt='simple') |                                  tablefmt='simple') | ||||||
|             message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' |             message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' | ||||||
| @@ -327,13 +331,20 @@ class Telegram(RPC): | |||||||
|             output = '' |             output = '' | ||||||
|             for currency in result['currencies']: |             for currency in result['currencies']: | ||||||
|                 if currency['est_btc'] > 0.0001: |                 if currency['est_btc'] > 0.0001: | ||||||
|                     output += "*{currency}:*\n" \ |                     curr_output = "*{currency}:*\n" \ | ||||||
|                             "\t`Available: {available: .8f}`\n" \ |                             "\t`Available: {available: .8f}`\n" \ | ||||||
|                             "\t`Balance: {balance: .8f}`\n" \ |                             "\t`Balance: {balance: .8f}`\n" \ | ||||||
|                             "\t`Pending: {pending: .8f}`\n" \ |                             "\t`Pending: {pending: .8f}`\n" \ | ||||||
|                             "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) |                             "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) | ||||||
|                 else: |                 else: | ||||||
|                     output += "*{currency}:* not showing <1$ amount \n".format(**currency) |                     curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) | ||||||
|  |  | ||||||
|  |                 # Handle overflowing messsage length | ||||||
|  |                 if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: | ||||||
|  |                     self._send_msg(output, bot=bot) | ||||||
|  |                     output = curr_output | ||||||
|  |                 else: | ||||||
|  |                     output += curr_output | ||||||
|  |  | ||||||
|             output += "\n*Estimated Value*:\n" \ |             output += "\n*Estimated Value*:\n" \ | ||||||
|                       "\t`BTC: {total: .8f}`\n" \ |                       "\t`BTC: {total: .8f}`\n" \ | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ from freqtrade.strategy.interface import IStrategy | |||||||
| # Import Default-Strategy to have hyperopt correctly resolve | # Import Default-Strategy to have hyperopt correctly resolve | ||||||
| from freqtrade.strategy.default_strategy import DefaultStrategy  # noqa: F401 | from freqtrade.strategy.default_strategy import DefaultStrategy  # noqa: F401 | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -16,7 +17,6 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: | |||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     # Copy all attributes from base class and class |     # Copy all attributes from base class and class | ||||||
|  |  | ||||||
|     comb = {**strategy.__class__.__dict__, **strategy.__dict__} |     comb = {**strategy.__class__.__dict__, **strategy.__dict__} | ||||||
|  |  | ||||||
|     # Delete '_abc_impl' from dict as deepcopy fails on 3.7 with |     # Delete '_abc_impl' from dict as deepcopy fails on 3.7 with | ||||||
| @@ -26,6 +26,7 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: | |||||||
|         del comb['_abc_impl'] |         del comb['_abc_impl'] | ||||||
|  |  | ||||||
|     attr = deepcopy(comb) |     attr = deepcopy(comb) | ||||||
|  |  | ||||||
|     # Adjust module name |     # Adjust module name | ||||||
|     attr['__module__'] = 'freqtrade.strategy' |     attr['__module__'] = 'freqtrade.strategy' | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,10 +13,11 @@ import arrow | |||||||
| from pandas import DataFrame | from pandas import DataFrame | ||||||
|  |  | ||||||
| from freqtrade.data.dataprovider import DataProvider | from freqtrade.data.dataprovider import DataProvider | ||||||
| from freqtrade.misc import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.persistence import Trade | from freqtrade.persistence import Trade | ||||||
| from freqtrade.wallets import Wallets | from freqtrade.wallets import Wallets | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,31 +9,31 @@ from freqtrade.tests.conftest import get_patched_exchange | |||||||
|  |  | ||||||
| def test_ohlcv(mocker, default_conf, ticker_history): | def test_ohlcv(mocker, default_conf, ticker_history): | ||||||
|     default_conf["runmode"] = RunMode.DRY_RUN |     default_conf["runmode"] = RunMode.DRY_RUN | ||||||
|     tick_interval = default_conf["ticker_interval"] |     ticker_interval = default_conf["ticker_interval"] | ||||||
|     exchange = get_patched_exchange(mocker, default_conf) |     exchange = get_patched_exchange(mocker, default_conf) | ||||||
|     exchange._klines[("XRP/BTC", tick_interval)] = ticker_history |     exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history | ||||||
|     exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history |     exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history | ||||||
|     dp = DataProvider(default_conf, exchange) |     dp = DataProvider(default_conf, exchange) | ||||||
|     assert dp.runmode == RunMode.DRY_RUN |     assert dp.runmode == RunMode.DRY_RUN | ||||||
|     assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", tick_interval)) |     assert ticker_history.equals(dp.ohlcv("UNITTEST/BTC", ticker_interval)) | ||||||
|     assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) |     assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) | ||||||
|     assert dp.ohlcv("UNITTEST/BTC", tick_interval) is not ticker_history |     assert dp.ohlcv("UNITTEST/BTC", ticker_interval) is not ticker_history | ||||||
|     assert dp.ohlcv("UNITTEST/BTC", tick_interval, copy=False) is ticker_history |     assert dp.ohlcv("UNITTEST/BTC", ticker_interval, copy=False) is ticker_history | ||||||
|     assert not dp.ohlcv("UNITTEST/BTC", tick_interval).empty |     assert not dp.ohlcv("UNITTEST/BTC", ticker_interval).empty | ||||||
|     assert dp.ohlcv("NONESENSE/AAA", tick_interval).empty |     assert dp.ohlcv("NONESENSE/AAA", ticker_interval).empty | ||||||
|  |  | ||||||
|     # Test with and without parameter |     # Test with and without parameter | ||||||
|     assert dp.ohlcv("UNITTEST/BTC", tick_interval).equals(dp.ohlcv("UNITTEST/BTC")) |     assert dp.ohlcv("UNITTEST/BTC", ticker_interval).equals(dp.ohlcv("UNITTEST/BTC")) | ||||||
|  |  | ||||||
|     default_conf["runmode"] = RunMode.LIVE |     default_conf["runmode"] = RunMode.LIVE | ||||||
|     dp = DataProvider(default_conf, exchange) |     dp = DataProvider(default_conf, exchange) | ||||||
|     assert dp.runmode == RunMode.LIVE |     assert dp.runmode == RunMode.LIVE | ||||||
|     assert isinstance(dp.ohlcv("UNITTEST/BTC", tick_interval), DataFrame) |     assert isinstance(dp.ohlcv("UNITTEST/BTC", ticker_interval), DataFrame) | ||||||
|  |  | ||||||
|     default_conf["runmode"] = RunMode.BACKTEST |     default_conf["runmode"] = RunMode.BACKTEST | ||||||
|     dp = DataProvider(default_conf, exchange) |     dp = DataProvider(default_conf, exchange) | ||||||
|     assert dp.runmode == RunMode.BACKTEST |     assert dp.runmode == RunMode.BACKTEST | ||||||
|     assert dp.ohlcv("UNITTEST/BTC", tick_interval).empty |     assert dp.ohlcv("UNITTEST/BTC", ticker_interval).empty | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_historic_ohlcv(mocker, default_conf, ticker_history): | def test_historic_ohlcv(mocker, default_conf, ticker_history): | ||||||
| @@ -54,15 +54,15 @@ def test_historic_ohlcv(mocker, default_conf, ticker_history): | |||||||
| def test_available_pairs(mocker, default_conf, ticker_history): | def test_available_pairs(mocker, default_conf, ticker_history): | ||||||
|     exchange = get_patched_exchange(mocker, default_conf) |     exchange = get_patched_exchange(mocker, default_conf) | ||||||
|  |  | ||||||
|     tick_interval = default_conf["ticker_interval"] |     ticker_interval = default_conf["ticker_interval"] | ||||||
|     exchange._klines[("XRP/BTC", tick_interval)] = ticker_history |     exchange._klines[("XRP/BTC", ticker_interval)] = ticker_history | ||||||
|     exchange._klines[("UNITTEST/BTC", tick_interval)] = ticker_history |     exchange._klines[("UNITTEST/BTC", ticker_interval)] = ticker_history | ||||||
|     dp = DataProvider(default_conf, exchange) |     dp = DataProvider(default_conf, exchange) | ||||||
|  |  | ||||||
|     assert len(dp.available_pairs) == 2 |     assert len(dp.available_pairs) == 2 | ||||||
|     assert dp.available_pairs == [ |     assert dp.available_pairs == [ | ||||||
|         ("XRP/BTC", tick_interval), |         ("XRP/BTC", ticker_interval), | ||||||
|         ("UNITTEST/BTC", tick_interval), |         ("UNITTEST/BTC", ticker_interval), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -71,10 +71,10 @@ def test_refresh(mocker, default_conf, ticker_history): | |||||||
|     mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) |     mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) | ||||||
|  |  | ||||||
|     exchange = get_patched_exchange(mocker, default_conf, id="binance") |     exchange = get_patched_exchange(mocker, default_conf, id="binance") | ||||||
|     tick_interval = default_conf["ticker_interval"] |     ticker_interval = default_conf["ticker_interval"] | ||||||
|     pairs = [("XRP/BTC", tick_interval), ("UNITTEST/BTC", tick_interval)] |     pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)] | ||||||
|  |  | ||||||
|     pairs_non_trad = [("ETH/USDT", tick_interval), ("BTC/TUSD", "1h")] |     pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] | ||||||
|  |  | ||||||
|     dp = DataProvider(default_conf, exchange) |     dp = DataProvider(default_conf, exchange) | ||||||
|     dp.refresh(pairs) |     dp.refresh(pairs) | ||||||
|   | |||||||
| @@ -242,10 +242,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non | |||||||
|  |  | ||||||
|     assert download_pair_history(datadir=None, exchange=exchange, |     assert download_pair_history(datadir=None, exchange=exchange, | ||||||
|                                  pair='MEME/BTC', |                                  pair='MEME/BTC', | ||||||
|                                  tick_interval='1m') |                                  ticker_interval='1m') | ||||||
|     assert download_pair_history(datadir=None, exchange=exchange, |     assert download_pair_history(datadir=None, exchange=exchange, | ||||||
|                                  pair='CFI/BTC', |                                  pair='CFI/BTC', | ||||||
|                                  tick_interval='1m') |                                  ticker_interval='1m') | ||||||
|     assert not exchange._pairs_last_refresh_time |     assert not exchange._pairs_last_refresh_time | ||||||
|     assert os.path.isfile(file1_1) is True |     assert os.path.isfile(file1_1) is True | ||||||
|     assert os.path.isfile(file2_1) is True |     assert os.path.isfile(file2_1) is True | ||||||
| @@ -259,10 +259,10 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf) -> Non | |||||||
|  |  | ||||||
|     assert download_pair_history(datadir=None, exchange=exchange, |     assert download_pair_history(datadir=None, exchange=exchange, | ||||||
|                                  pair='MEME/BTC', |                                  pair='MEME/BTC', | ||||||
|                                  tick_interval='5m') |                                  ticker_interval='5m') | ||||||
|     assert download_pair_history(datadir=None, exchange=exchange, |     assert download_pair_history(datadir=None, exchange=exchange, | ||||||
|                                  pair='CFI/BTC', |                                  pair='CFI/BTC', | ||||||
|                                  tick_interval='5m') |                                  ticker_interval='5m') | ||||||
|     assert not exchange._pairs_last_refresh_time |     assert not exchange._pairs_last_refresh_time | ||||||
|     assert os.path.isfile(file1_5) is True |     assert os.path.isfile(file1_5) is True | ||||||
|     assert os.path.isfile(file2_5) is True |     assert os.path.isfile(file2_5) is True | ||||||
| @@ -280,8 +280,8 @@ def test_download_pair_history2(mocker, default_conf) -> None: | |||||||
|     json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) |     json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) | ||||||
|     mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) |     mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick) | ||||||
|     exchange = get_patched_exchange(mocker, default_conf) |     exchange = get_patched_exchange(mocker, default_conf) | ||||||
|     download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') |     download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='1m') | ||||||
|     download_pair_history(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') |     download_pair_history(None, exchange, pair="UNITTEST/BTC", ticker_interval='3m') | ||||||
|     assert json_dump_mock.call_count == 2 |     assert json_dump_mock.call_count == 2 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -298,7 +298,7 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, def | |||||||
|  |  | ||||||
|     assert not download_pair_history(datadir=None, exchange=exchange, |     assert not download_pair_history(datadir=None, exchange=exchange, | ||||||
|                                      pair='MEME/BTC', |                                      pair='MEME/BTC', | ||||||
|                                      tick_interval='1m') |                                      ticker_interval='1m') | ||||||
|     # clean files freshly downloaded |     # clean files freshly downloaded | ||||||
|     _clean_test_file(file1_1) |     _clean_test_file(file1_1) | ||||||
|     _clean_test_file(file1_5) |     _clean_test_file(file1_5) | ||||||
|   | |||||||
| @@ -941,8 +941,8 @@ def test_get_history(default_conf, mocker, caplog, exchange_name): | |||||||
|     ] |     ] | ||||||
|     pair = 'ETH/BTC' |     pair = 'ETH/BTC' | ||||||
|  |  | ||||||
|     async def mock_candle_hist(pair, tick_interval, since_ms): |     async def mock_candle_hist(pair, ticker_interval, since_ms): | ||||||
|         return pair, tick_interval, tick |         return pair, ticker_interval, tick | ||||||
|  |  | ||||||
|     exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) |     exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) | ||||||
|     # one_call calculation * 1.8 should do 2 calls |     # one_call calculation * 1.8 should do 2 calls | ||||||
| @@ -1038,7 +1038,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ | |||||||
|     # exchange = Exchange(default_conf) |     # exchange = Exchange(default_conf) | ||||||
|     await async_ccxt_exception(mocker, default_conf, MagicMock(), |     await async_ccxt_exception(mocker, default_conf, MagicMock(), | ||||||
|                                "_async_get_candle_history", "fetch_ohlcv", |                                "_async_get_candle_history", "fetch_ohlcv", | ||||||
|                                pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) |                                pair='ABCD/BTC', ticker_interval=default_conf['ticker_interval']) | ||||||
|  |  | ||||||
|     api_mock = MagicMock() |     api_mock = MagicMock() | ||||||
|     with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): |     with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'): | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ from typing import NamedTuple, List | |||||||
| import arrow | import arrow | ||||||
| from pandas import DataFrame | from pandas import DataFrame | ||||||
|  |  | ||||||
| from freqtrade.misc import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.strategy.interface import SellType | from freqtrade.strategy.interface import SellType | ||||||
|  |  | ||||||
| ticker_start_time = arrow.get(2018, 10, 3) | ticker_start_time = arrow.get(2018, 10, 3) | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| from freqtrade import optimize | from freqtrade import optimize | ||||||
| from freqtrade.arguments import TimeRange | from freqtrade.arguments import TimeRange | ||||||
| from freqtrade.data import history | from freqtrade.data import history | ||||||
| from freqtrade.misc import timeframe_to_minutes | from freqtrade.exchange import timeframe_to_minutes | ||||||
| from freqtrade.strategy.default_strategy import DefaultStrategy | from freqtrade.strategy.default_strategy import DefaultStrategy | ||||||
| from freqtrade.tests.conftest import log_has, patch_exchange | from freqtrade.tests.conftest import log_has, patch_exchange | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,8 @@ | |||||||
|  |  | ||||||
| import re | import re | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from random import randint | from random import choice, randint | ||||||
|  | from string import ascii_uppercase | ||||||
| from unittest.mock import MagicMock, PropertyMock | from unittest.mock import MagicMock, PropertyMock | ||||||
|  |  | ||||||
| import arrow | import arrow | ||||||
| @@ -20,7 +21,8 @@ from freqtrade.rpc import RPCMessageType | |||||||
| from freqtrade.rpc.telegram import Telegram, authorized_only | from freqtrade.rpc.telegram import Telegram, authorized_only | ||||||
| from freqtrade.state import State | from freqtrade.state import State | ||||||
| from freqtrade.strategy.interface import SellType | from freqtrade.strategy.interface import SellType | ||||||
| from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange) | from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, | ||||||
|  |                                       patch_exchange) | ||||||
| from freqtrade.tests.test_freqtradebot import patch_get_signal | from freqtrade.tests.test_freqtradebot import patch_get_signal | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -587,6 +589,45 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: | |||||||
|     assert 'all balances are zero' in result |     assert 'all balances are zero' in result | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: | ||||||
|  |     balances = [] | ||||||
|  |     for i in range(100): | ||||||
|  |         curr = choice(ascii_uppercase) + choice(ascii_uppercase) + choice(ascii_uppercase) | ||||||
|  |         balances.append({ | ||||||
|  |             'currency': curr, | ||||||
|  |             'available': 1.0, | ||||||
|  |             'pending': 0.5, | ||||||
|  |             'balance': i, | ||||||
|  |             'est_btc': 1 | ||||||
|  |         }) | ||||||
|  |     mocker.patch('freqtrade.rpc.rpc.RPC._rpc_balance', return_value={ | ||||||
|  |         'currencies': balances, | ||||||
|  |         'total': 100.0, | ||||||
|  |         'symbol': 100.0, | ||||||
|  |         'value': 1000.0, | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     msg_mock = MagicMock() | ||||||
|  |     mocker.patch.multiple( | ||||||
|  |         'freqtrade.rpc.telegram.Telegram', | ||||||
|  |         _init=MagicMock(), | ||||||
|  |         _send_msg=msg_mock | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     freqtradebot = get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |     patch_get_signal(freqtradebot, (True, False)) | ||||||
|  |  | ||||||
|  |     telegram = Telegram(freqtradebot) | ||||||
|  |  | ||||||
|  |     telegram._balance(bot=MagicMock(), update=update) | ||||||
|  |     assert msg_mock.call_count > 1 | ||||||
|  |     # Test if wrap happens around 4000 - | ||||||
|  |     # and each single currency-output is around 120 characters long so we need | ||||||
|  |     # an offset to avoid random test failures | ||||||
|  |     assert len(msg_mock.call_args_list[0][0][0]) < 4096 | ||||||
|  |     assert len(msg_mock.call_args_list[0][0][0]) > (4096 - 120) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_start_handle(default_conf, update, mocker) -> None: | def test_start_handle(default_conf, update, mocker) -> None: | ||||||
|     msg_mock = MagicMock() |     msg_mock = MagicMock() | ||||||
|     mocker.patch.multiple( |     mocker.patch.multiple( | ||||||
|   | |||||||
| @@ -1,17 +1,19 @@ | |||||||
| # pragma pylint: disable=missing-docstring, protected-access, C0103 | # pragma pylint: disable=missing-docstring, protected-access, C0103 | ||||||
| import logging | import logging | ||||||
|  | import warnings | ||||||
| from base64 import urlsafe_b64encode | from base64 import urlsafe_b64encode | ||||||
| from os import path | from os import path | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| import warnings | from unittest.mock import Mock | ||||||
|  |  | ||||||
| import pytest | import pytest | ||||||
| from pandas import DataFrame | from pandas import DataFrame | ||||||
|  |  | ||||||
|  | from freqtrade.resolvers import StrategyResolver | ||||||
| from freqtrade.strategy import import_strategy | from freqtrade.strategy import import_strategy | ||||||
| from freqtrade.strategy.default_strategy import DefaultStrategy | from freqtrade.strategy.default_strategy import DefaultStrategy | ||||||
| from freqtrade.strategy.interface import IStrategy | from freqtrade.strategy.interface import IStrategy | ||||||
| from freqtrade.resolvers import StrategyResolver | from freqtrade.tests.conftest import log_has_re | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_import_strategy(caplog): | def test_import_strategy(caplog): | ||||||
| @@ -94,6 +96,16 @@ def test_load_not_found_strategy(): | |||||||
|         strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) |         strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_load_staticmethod_importerror(mocker, caplog): | ||||||
|  |     mocker.patch("freqtrade.resolvers.strategy_resolver.import_strategy", Mock( | ||||||
|  |         side_effect=TypeError("can't pickle staticmethod objects"))) | ||||||
|  |     with pytest.raises(ImportError, | ||||||
|  |                        match=r"Impossible to load Strategy 'DefaultStrategy'." | ||||||
|  |                              r" This class does not exist or contains Python code errors"): | ||||||
|  |         StrategyResolver() | ||||||
|  |     assert log_has_re(r".*Error: can't pickle staticmethod objects", caplog.record_tuples) | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_strategy(result): | def test_strategy(result): | ||||||
|     config = {'strategy': 'DefaultStrategy'} |     config = {'strategy': 'DefaultStrategy'} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -494,15 +494,6 @@ def test_check_exchange(default_conf, caplog) -> None: | |||||||
|     ): |     ): | ||||||
|         configuration.check_exchange(default_conf) |         configuration.check_exchange(default_conf) | ||||||
|  |  | ||||||
|     # Test ccxt_rate_limit depreciation |  | ||||||
|     default_conf.get('exchange').update({'name': 'binance'}) |  | ||||||
|     default_conf['exchange']['ccxt_rate_limit'] = True |  | ||||||
|     configuration.check_exchange(default_conf) |  | ||||||
|     assert log_has("`ccxt_rate_limit` has been deprecated in favor of " |  | ||||||
|                    "`ccxt_config` and `ccxt_async_config` and will be removed " |  | ||||||
|                    "in a future version.", |  | ||||||
|                    caplog.record_tuples) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: | def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: | ||||||
|     mocker.patch('freqtrade.configuration.open', mocker.mock_open( |     mocker.patch('freqtrade.configuration.open', mocker.mock_open( | ||||||
|   | |||||||
| @@ -1407,7 +1407,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ | |||||||
|         amount=amount, |         amount=amount, | ||||||
|         exchange='binance', |         exchange='binance', | ||||||
|         open_rate=0.245441, |         open_rate=0.245441, | ||||||
|         open_order_id="123456" |         open_order_id="123456", | ||||||
|  |         is_open=True, | ||||||
|     ) |     ) | ||||||
|     freqtrade.update_trade_state(trade, limit_buy_order) |     freqtrade.update_trade_state(trade, limit_buy_order) | ||||||
|     assert trade.amount != amount |     assert trade.amount != amount | ||||||
| @@ -1432,6 +1433,35 @@ def test_update_trade_state_exception(mocker, default_conf, | |||||||
|     assert log_has('Could not update trade amount: ', caplog.record_tuples) |     assert log_has('Could not update trade amount: ', caplog.record_tuples) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): | ||||||
|  |     mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) | ||||||
|  |     # get_order should not be called!! | ||||||
|  |     mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) | ||||||
|  |     wallet_mock = MagicMock() | ||||||
|  |     mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) | ||||||
|  |  | ||||||
|  |     patch_exchange(mocker) | ||||||
|  |     Trade.session = MagicMock() | ||||||
|  |     amount = limit_sell_order["amount"] | ||||||
|  |     freqtrade = get_patched_freqtradebot(mocker, default_conf) | ||||||
|  |     wallet_mock.reset_mock() | ||||||
|  |     trade = Trade( | ||||||
|  |         pair='LTC/ETH', | ||||||
|  |         amount=amount, | ||||||
|  |         exchange='binance', | ||||||
|  |         open_rate=0.245441, | ||||||
|  |         fee_open=0.0025, | ||||||
|  |         fee_close=0.0025, | ||||||
|  |         open_order_id="123456", | ||||||
|  |         is_open=True, | ||||||
|  |     ) | ||||||
|  |     freqtrade.update_trade_state(trade, limit_sell_order) | ||||||
|  |     assert trade.amount == limit_sell_order['amount'] | ||||||
|  |     # Wallet needs to be updated after closing a limit-sell order to reenable buying | ||||||
|  |     assert wallet_mock.call_count == 1 | ||||||
|  |     assert not trade.is_open | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, | def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, | ||||||
|                       fee, markets, mocker) -> None: |                       fee, markets, mocker) -> None: | ||||||
|     patch_RPCManager(mocker) |     patch_RPCManager(mocker) | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ | |||||||
| flake8==3.7.7 | flake8==3.7.7 | ||||||
| flake8-type-annotations==0.1.0 | flake8-type-annotations==0.1.0 | ||||||
| flake8-tidy-imports==2.0.0 | flake8-tidy-imports==2.0.0 | ||||||
| pytest==4.4.0 | pytest==4.4.1 | ||||||
| pytest-mock==1.10.3 | pytest-mock==1.10.4 | ||||||
| pytest-asyncio==0.10.0 | pytest-asyncio==0.10.0 | ||||||
| pytest-cov==2.6.1 | pytest-cov==2.6.1 | ||||||
| coveralls==1.7.0 | coveralls==1.7.0 | ||||||
| mypy==0.700 | mypy==0.701 | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								requirements-pi.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								requirements-pi.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | ccxt==1.18.486 | ||||||
|  | SQLAlchemy==1.3.3 | ||||||
|  | python-telegram-bot==11.1.0 | ||||||
|  | arrow==0.13.1 | ||||||
|  | cachetools==3.1.0 | ||||||
|  | requests==2.21.0 | ||||||
|  | urllib3==1.25 | ||||||
|  | wrapt==1.11.1 | ||||||
|  | scikit-learn==0.20.3 | ||||||
|  | joblib==0.13.2 | ||||||
|  | jsonschema==3.0.1 | ||||||
|  | TA-Lib==0.4.17 | ||||||
|  | tabulate==0.8.3 | ||||||
|  | coinmarketcap==5.0.3 | ||||||
|  |  | ||||||
|  | # Required for hyperopt | ||||||
|  | scikit-optimize==0.5.2 | ||||||
|  |  | ||||||
|  | # find first, C search in arrays | ||||||
|  | py_find_1st==1.1.3 | ||||||
|  |  | ||||||
|  | #Load ticker files 30% faster | ||||||
|  | python-rapidjson==0.7.0 | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| # Include all requirements to run the bot. | # Include all requirements to run the bot. | ||||||
| -r requirements.txt | -r requirements.txt | ||||||
|  |  | ||||||
| plotly==3.7.1 | plotly==3.8.1 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| ccxt==1.18.435 | ccxt==1.18.486 | ||||||
| SQLAlchemy==1.3.2 | SQLAlchemy==1.3.3 | ||||||
| python-telegram-bot==11.1.0 | python-telegram-bot==11.1.0 | ||||||
| arrow==0.13.1 | arrow==0.13.1 | ||||||
| cachetools==3.1.0 | cachetools==3.1.0 | ||||||
| requests==2.21.0 | requests==2.21.0 | ||||||
| urllib3==1.24.1 | urllib3==1.25 | ||||||
| wrapt==1.11.1 | wrapt==1.11.1 | ||||||
| numpy==1.16.2 | numpy==1.16.3 | ||||||
| pandas==0.24.2 | pandas==0.24.2 | ||||||
| scikit-learn==0.20.3 | scikit-learn==0.20.3 | ||||||
| joblib==0.13.2 | joblib==0.13.2 | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| #!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||||
|  | """ | ||||||
| """This script generate json data""" | This script generates json data | ||||||
|  | """ | ||||||
| import json | import json | ||||||
| import sys | import sys | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| @@ -35,7 +36,7 @@ if args.config: | |||||||
|     config: Dict[str, Any] = {} |     config: Dict[str, Any] = {} | ||||||
|     # Now expecting a list of config filenames here, not a string |     # Now expecting a list of config filenames here, not a string | ||||||
|     for path in args.config: |     for path in args.config: | ||||||
|         print('Using config: %s ...', path) |         print(f"Using config: {path}...") | ||||||
|         # Merge config options, overwriting old values |         # Merge config options, overwriting old values | ||||||
|         config = deep_merge_dicts(configuration._load_config_file(path), config) |         config = deep_merge_dicts(configuration._load_config_file(path), config) | ||||||
|  |  | ||||||
| @@ -44,18 +45,20 @@ if args.config: | |||||||
|     config['exchange']['key'] = '' |     config['exchange']['key'] = '' | ||||||
|     config['exchange']['secret'] = '' |     config['exchange']['secret'] = '' | ||||||
| else: | else: | ||||||
|     config = {'stake_currency': '', |     config = { | ||||||
|               'dry_run': True, |         'stake_currency': '', | ||||||
|               'exchange': { |         'dry_run': True, | ||||||
|                   'name': args.exchange, |         'exchange': { | ||||||
|                   'key': '', |             'name': args.exchange, | ||||||
|                   'secret': '', |             'key': '', | ||||||
|                   'pair_whitelist': [], |             'secret': '', | ||||||
|                   'ccxt_async_config': { |             'pair_whitelist': [], | ||||||
|                       "enableRateLimit": False |             'ccxt_async_config': { | ||||||
|                   } |                 'enableRateLimit': True, | ||||||
|               } |                 'rateLimit': 200 | ||||||
|               } |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name']) | dl_path = Path(DEFAULT_DL_PATH).joinpath(config['exchange']['name']) | ||||||
| @@ -92,18 +95,18 @@ for pair in PAIRS: | |||||||
|         pairs_not_available.append(pair) |         pairs_not_available.append(pair) | ||||||
|         print(f"skipping pair {pair}") |         print(f"skipping pair {pair}") | ||||||
|         continue |         continue | ||||||
|     for tick_interval in timeframes: |     for ticker_interval in timeframes: | ||||||
|         pair_print = pair.replace('/', '_') |         pair_print = pair.replace('/', '_') | ||||||
|         filename = f'{pair_print}-{tick_interval}.json' |         filename = f'{pair_print}-{ticker_interval}.json' | ||||||
|         dl_file = dl_path.joinpath(filename) |         dl_file = dl_path.joinpath(filename) | ||||||
|         if args.erase and dl_file.exists(): |         if args.erase and dl_file.exists(): | ||||||
|             print(f'Deleting existing data for pair {pair}, interval {tick_interval}') |             print(f'Deleting existing data for pair {pair}, interval {ticker_interval}') | ||||||
|             dl_file.unlink() |             dl_file.unlink() | ||||||
|  |  | ||||||
|         print(f'downloading pair {pair}, interval {tick_interval}') |         print(f'downloading pair {pair}, interval {ticker_interval}') | ||||||
|         download_pair_history(datadir=dl_path, exchange=exchange, |         download_pair_history(datadir=dl_path, exchange=exchange, | ||||||
|                               pair=pair, |                               pair=pair, | ||||||
|                               tick_interval=tick_interval, |                               ticker_interval=ticker_interval, | ||||||
|                               timerange=timerange) |                               timerange=timerange) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,10 @@ | |||||||
|  | """ | ||||||
|  | This script was adapted from ccxt here: | ||||||
|  | https://github.com/ccxt/ccxt/blob/master/examples/py/arbitrage-pairs.py | ||||||
|  | """ | ||||||
| import os | import os | ||||||
| import sys | import sys | ||||||
|  | import traceback | ||||||
|  |  | ||||||
| root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||||
| sys.path.append(root + '/python') | sys.path.append(root + '/python') | ||||||
| @@ -49,6 +54,11 @@ def print_supported_exchanges(): | |||||||
|  |  | ||||||
| try: | try: | ||||||
|  |  | ||||||
|  |     if len(sys.argv) < 2: | ||||||
|  |         dump("Usage: python " + sys.argv[0], green('id')) | ||||||
|  |         print_supported_exchanges() | ||||||
|  |         sys.exit(1) | ||||||
|  |  | ||||||
|     id = sys.argv[1]  # get exchange id from command line arguments |     id = sys.argv[1]  # get exchange id from command line arguments | ||||||
|  |  | ||||||
|     # check if the exchange is supported by ccxt |     # check if the exchange is supported by ccxt | ||||||
| @@ -87,5 +97,7 @@ try: | |||||||
|  |  | ||||||
| except Exception as e: | except Exception as e: | ||||||
|     dump('[' + type(e).__name__ + ']', str(e)) |     dump('[' + type(e).__name__ + ']', str(e)) | ||||||
|  |     dump(traceback.format_exc()) | ||||||
|     dump("Usage: python " + sys.argv[0], green('id')) |     dump("Usage: python " + sys.argv[0], green('id')) | ||||||
|     print_supported_exchanges() |     print_supported_exchanges() | ||||||
|  |     sys.exit(1) | ||||||
|   | |||||||
| @@ -82,7 +82,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram | |||||||
|     return trades |     return trades | ||||||
|  |  | ||||||
|  |  | ||||||
| def generate_plot_file(fig, pair, tick_interval, is_last) -> None: | def generate_plot_file(fig, pair, ticker_interval, is_last) -> None: | ||||||
|     """ |     """ | ||||||
|     Generate a plot html file from pre populated fig plotly object |     Generate a plot html file from pre populated fig plotly object | ||||||
|     :return: None |     :return: None | ||||||
| @@ -90,7 +90,7 @@ def generate_plot_file(fig, pair, tick_interval, is_last) -> None: | |||||||
|     logger.info('Generate plot file for %s', pair) |     logger.info('Generate plot file for %s', pair) | ||||||
|  |  | ||||||
|     pair_name = pair.replace("/", "_") |     pair_name = pair.replace("/", "_") | ||||||
|     file_name = 'freqtrade-plot-' + pair_name + '-' + tick_interval + '.html' |     file_name = 'freqtrade-plot-' + pair_name + '-' + ticker_interval + '.html' | ||||||
|  |  | ||||||
|     Path("user_data/plots").mkdir(parents=True, exist_ok=True) |     Path("user_data/plots").mkdir(parents=True, exist_ok=True) | ||||||
|  |  | ||||||
| @@ -135,20 +135,20 @@ def get_tickers_data(strategy, exchange, pairs: List[str], args): | |||||||
|     :return: dictinnary of tickers. output format: {'pair': tickersdata} |     :return: dictinnary of tickers. output format: {'pair': tickersdata} | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     tick_interval = strategy.ticker_interval |     ticker_interval = strategy.ticker_interval | ||||||
|     timerange = Arguments.parse_timerange(args.timerange) |     timerange = Arguments.parse_timerange(args.timerange) | ||||||
|  |  | ||||||
|     tickers = {} |     tickers = {} | ||||||
|     if args.live: |     if args.live: | ||||||
|         logger.info('Downloading pairs.') |         logger.info('Downloading pairs.') | ||||||
|         exchange.refresh_latest_ohlcv([(pair, tick_interval) for pair in pairs]) |         exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs]) | ||||||
|         for pair in pairs: |         for pair in pairs: | ||||||
|             tickers[pair] = exchange.klines((pair, tick_interval)) |             tickers[pair] = exchange.klines((pair, ticker_interval)) | ||||||
|     else: |     else: | ||||||
|         tickers = history.load_data( |         tickers = history.load_data( | ||||||
|             datadir=Path(str(_CONF.get("datadir"))), |             datadir=Path(str(_CONF.get("datadir"))), | ||||||
|             pairs=pairs, |             pairs=pairs, | ||||||
|             ticker_interval=tick_interval, |             ticker_interval=ticker_interval, | ||||||
|             refresh_pairs=_CONF.get('refresh_pairs', False), |             refresh_pairs=_CONF.get('refresh_pairs', False), | ||||||
|             timerange=timerange, |             timerange=timerange, | ||||||
|             exchange=Exchange(_CONF) |             exchange=Exchange(_CONF) | ||||||
| @@ -399,7 +399,7 @@ def analyse_and_plot_pairs(args: Namespace): | |||||||
|     strategy, exchange, pairs = get_trading_env(args) |     strategy, exchange, pairs = get_trading_env(args) | ||||||
|     # Set timerange to use |     # Set timerange to use | ||||||
|     timerange = Arguments.parse_timerange(args.timerange) |     timerange = Arguments.parse_timerange(args.timerange) | ||||||
|     tick_interval = strategy.ticker_interval |     ticker_interval = strategy.ticker_interval | ||||||
|  |  | ||||||
|     tickers = get_tickers_data(strategy, exchange, pairs, args) |     tickers = get_tickers_data(strategy, exchange, pairs, args) | ||||||
|     pair_counter = 0 |     pair_counter = 0 | ||||||
| @@ -422,7 +422,7 @@ def analyse_and_plot_pairs(args: Namespace): | |||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         is_last = (False, True)[pair_counter == len(tickers)] |         is_last = (False, True)[pair_counter == len(tickers)] | ||||||
|         generate_plot_file(fig, pair, tick_interval, is_last) |         generate_plot_file(fig, pair, ticker_interval, is_last) | ||||||
|  |  | ||||||
|     logger.info('End of ploting process %s plots generated', pair_counter) |     logger.info('End of ploting process %s plots generated', pair_counter) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,10 +27,12 @@ from plotly.offline import plot | |||||||
| from freqtrade.arguments import Arguments | from freqtrade.arguments import Arguments | ||||||
| from freqtrade.configuration import Configuration | from freqtrade.configuration import Configuration | ||||||
| from freqtrade.data import history | from freqtrade.data import history | ||||||
| from freqtrade.misc import common_datearray, timeframe_to_seconds | from freqtrade.exchange import timeframe_to_seconds | ||||||
|  | from freqtrade.misc import common_datearray | ||||||
| from freqtrade.resolvers import StrategyResolver | from freqtrade.resolvers import StrategyResolver | ||||||
| from freqtrade.state import RunMode | from freqtrade.state import RunMode | ||||||
|  |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -76,7 +78,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     in helping out to find a good algorithm. |     in helping out to find a good algorithm. | ||||||
|     """ |     """ | ||||||
|  |  | ||||||
|     # We need to use the same pairs, same tick_interval |     # We need to use the same pairs, same ticker_interval | ||||||
|     # and same timeperiod as used in backtesting |     # and same timeperiod as used in backtesting | ||||||
|     # to match the tickerdata against the profits-results |     # to match the tickerdata against the profits-results | ||||||
|     timerange = Arguments.parse_timerange(args.timerange) |     timerange = Arguments.parse_timerange(args.timerange) | ||||||
| @@ -112,7 +114,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     else: |     else: | ||||||
|         filter_pairs = config['exchange']['pair_whitelist'] |         filter_pairs = config['exchange']['pair_whitelist'] | ||||||
|  |  | ||||||
|     tick_interval = strategy.ticker_interval |     ticker_interval = strategy.ticker_interval | ||||||
|     pairs = config['exchange']['pair_whitelist'] |     pairs = config['exchange']['pair_whitelist'] | ||||||
|  |  | ||||||
|     if filter_pairs: |     if filter_pairs: | ||||||
| @@ -122,7 +124,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     tickers = history.load_data( |     tickers = history.load_data( | ||||||
|         datadir=Path(str(config.get('datadir'))), |         datadir=Path(str(config.get('datadir'))), | ||||||
|         pairs=pairs, |         pairs=pairs, | ||||||
|         ticker_interval=tick_interval, |         ticker_interval=ticker_interval, | ||||||
|         refresh_pairs=False, |         refresh_pairs=False, | ||||||
|         timerange=timerange |         timerange=timerange | ||||||
|     ) |     ) | ||||||
| @@ -134,7 +136,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     dates = common_datearray(dataframes) |     dates = common_datearray(dataframes) | ||||||
|     min_date = int(min(dates).timestamp()) |     min_date = int(min(dates).timestamp()) | ||||||
|     max_date = int(max(dates).timestamp()) |     max_date = int(max(dates).timestamp()) | ||||||
|     num_iterations = define_index(min_date, max_date, tick_interval) + 1 |     num_iterations = define_index(min_date, max_date, ticker_interval) + 1 | ||||||
|  |  | ||||||
|     # Make an average close price of all the pairs that was involved. |     # Make an average close price of all the pairs that was involved. | ||||||
|     # this could be useful to gauge the overall market trend |     # this could be useful to gauge the overall market trend | ||||||
| @@ -154,7 +156,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     avgclose /= num |     avgclose /= num | ||||||
|  |  | ||||||
|     # make an profits-growth array |     # make an profits-growth array | ||||||
|     pg = make_profit_array(data, num_iterations, min_date, tick_interval, filter_pairs) |     pg = make_profit_array(data, num_iterations, min_date, ticker_interval, filter_pairs) | ||||||
|  |  | ||||||
|     # |     # | ||||||
|     # Plot the pairs average close prices, and total profit growth |     # Plot the pairs average close prices, and total profit growth | ||||||
| @@ -178,7 +180,7 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     fig.append_trace(profit, 2, 1) |     fig.append_trace(profit, 2, 1) | ||||||
|  |  | ||||||
|     for pair in pairs: |     for pair in pairs: | ||||||
|         pg = make_profit_array(data, num_iterations, min_date, tick_interval, [pair]) |         pg = make_profit_array(data, num_iterations, min_date, ticker_interval, [pair]) | ||||||
|         pair_profit = go.Scattergl( |         pair_profit = go.Scattergl( | ||||||
|             x=dates, |             x=dates, | ||||||
|             y=pg, |             y=pg, | ||||||
| @@ -189,11 +191,11 @@ def plot_profit(args: Namespace) -> None: | |||||||
|     plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html'))) |     plot(fig, filename=str(Path('user_data').joinpath('freqtrade-profit-plot.html'))) | ||||||
|  |  | ||||||
|  |  | ||||||
| def define_index(min_date: int, max_date: int, interval: str) -> int: | def define_index(min_date: int, max_date: int, ticker_interval: str) -> int: | ||||||
|     """ |     """ | ||||||
|     Return the index of a specific date |     Return the index of a specific date | ||||||
|     """ |     """ | ||||||
|     interval_seconds = timeframe_to_seconds(interval) |     interval_seconds = timeframe_to_seconds(ticker_interval) | ||||||
|     return int((max_date - min_date) / interval_seconds) |     return int((max_date - min_date) / interval_seconds) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user