diff --git a/.gitignore b/.gitignore index f206fce66..4720ff5cb 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ user_data/* user_data/notebooks/* freqtrade-plot.html freqtrade-profit-plot.html +freqtrade/rpc/api_server/ui/* # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Dockerfile b/Dockerfile index 445f909b0..8d4f0ebe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ RUN pip install -e . --no-cache-dir \ - && mkdir /freqtrade/user_data/ + && mkdir /freqtrade/user_data/ \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/Dockerfile.armhf b/Dockerfile.armhf index b6f2e44e6..f938ec457 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -41,7 +41,9 @@ COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && freqtrade install-ui + ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] diff --git a/MANIFEST.in b/MANIFEST.in index 2f59bcc7a..adbcd2e30 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,3 +2,5 @@ include LICENSE include README.md recursive-include freqtrade *.py recursive-include freqtrade/templates/ *.j2 *.ipynb +include freqtrade/rpc/api_server/ui/fallback_file.html +include freqtrade/rpc/api_server/ui/favicon.ico diff --git a/README.md b/README.md index 51a3409ea..bb136d7f2 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io - [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Persistence**: Persistence is achieved through sqlite. -- [x] **Dry-run**: Run the bot without playing money. +- [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). diff --git a/config_binance.json.example b/config_binance.json.example index 83c9748d7..4fa615d6d 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -12,15 +12,15 @@ "sell": 30 }, "bid_strategy": { - "use_order_book": false, "ask_last_balance": 0.0, + "use_order_book": false, "order_book_top": 1, "check_depth_of_market": { "enabled": false, "bids_to_ask_delta": 1 } }, - "ask_strategy":{ + "ask_strategy": { "use_order_book": false, "order_book_min": 1, "order_book_max": 1, diff --git a/docker-compose.yml b/docker-compose.yml index 7094500b4..1f63059f0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,11 @@ services: container_name: freqtrade volumes: - "./user_data:/freqtrade/user_data" + # Expose api on port 8080 (localhost only) + # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation + # before enabling this. + # ports: + # - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade diff --git a/docs/configuration.md b/docs/configuration.md index c6c40319d..00d2830e4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -82,13 +82,14 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String -| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List -| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)).
**Datatype:** List +| `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List +| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) 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)
**Datatype:** Dict | `exchange.ccxt_async_config` | 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)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer | `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean +| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts diff --git a/docs/data-download.md b/docs/data-download.md index 4c7376933..04f444a8b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -264,7 +264,19 @@ If you are using Binance for example: ```bash mkdir -p user_data/data/binance -cp freqtrade/tests/testdata/pairs.json user_data/data/binance +cp tests/testdata/pairs.json user_data/data/binance +``` + +If you your configuration directory `user_data` was made by docker, you may get the following error: + +``` +cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied +``` + +You can fix the permissions of your user-data directory as follows: + +``` +sudo chown -R $UID:$GID user_data ``` The format of the `pairs.json` file is a simple json list. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 85bd72323..94b2fca39 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.2.7 +mkdocs-material==6.2.8 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 diff --git a/docs/rest-api.md b/docs/rest-api.md index 2c7142c61..e2b94f080 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -1,4 +1,19 @@ -# REST API Usage +# REST API & FreqUI + +## FreqUI + +Freqtrade provides a builtin webserver, which can serve [FreqUI](https://github.com/freqtrade/frequi), the freqtrade UI. + +By default, the UI is not included in the installation (except for docker images), and must be installed explicitly with `freqtrade install-ui`. +This same command can also be used to update freqUI, should there be a new release. + +Once the bot is started in trade / dry-run mode (with `freqtrade trade`) - the UI will be available under the configured port below (usually `http://127.0.0.1:8080`). + +!!! info "Alpha release" + FreqUI is still considered an alpha release - if you encounter bugs or inconsistencies please open a [FreqUI issue](https://github.com/freqtrade/frequi/issues/new/choose). + +!!! Note "developers" + Developers should not use this method, but instead use the method described in the [freqUI repository](https://github.com/freqtrade/frequi) to get the source-code of freqUI. ## Configuration @@ -23,9 +38,6 @@ Sample configuration: !!! Danger "Security warning" By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. -!!! Danger "Password selection" - Please make sure to select a very strong, unique password to protect your bot from unauthorized access. - You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: @@ -35,16 +47,22 @@ This should return the response: All other endpoints return sensitive info and require authentication and are therefore not available through a web browser. -To generate a secure password, either use a password manager, or use the below code snipped. +### Security + +To generate a secure password, best use a password manager, or use the below code. ``` python import secrets secrets.token_hex() ``` -!!! Hint +!!! Hint "JWT token" Use the same method to also generate a JWT secret key (`jwt_secret_key`). +!!! Danger "Password selection" + Please make sure to select a very strong, unique password to protect your bot from unauthorized access. + Also change `jwt_secret_key` to something random (no need to remember this, but it'll be used to encrypt your session, so it better be something unique!). + ### Configuration with docker If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker. @@ -57,28 +75,20 @@ If you run your bot using docker, you'll need to have the bot listen to incoming }, ``` -Add the following to your docker command: +Uncomment the following from your docker-compose file: -``` bash - -p 127.0.0.1:8080:8080 -``` - -A complete sample-command may then look as follows: - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -p 127.0.0.1:8080:8080 \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +```yml + ports: + - "127.0.0.1:8080:8080" ``` !!! Danger "Security warning" - By using `-p 8080:8080` the API is available to everyone connecting to the server under the correct port, so others may be able to control your bot. + By using `8080:8080` in the docker port mapping, the API will be available to everyone connecting to the server under the correct port, so others may be able to control your bot. -## Consuming the API + +## Rest API + +### Consuming the API You can consume the API by using the script `scripts/rest_client.py`. The client script only requires the `requests` module, so Freqtrade does not need to be installed on the system. @@ -89,7 +99,7 @@ python3 scripts/rest_client.py [optional parameters] By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be used, however you can specify a configuration file to override this behaviour. -### Minimalistic client config +#### Minimalistic client config ``` json { @@ -105,7 +115,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use python3 scripts/rest_client.py --config rest_config.json [optional parameters] ``` -## Available endpoints +### Available endpoints | Command | Description | |----------|-------------| @@ -264,12 +274,12 @@ whitelist ``` -## OpenAPI interface +### OpenAPI interface To enable the builtin openAPI interface (Swagger UI), specify `"enable_openapi": true` in the api_server configuration. This will enable the Swagger UI at the `/docs` endpoint. By default, that's running at http://localhost:8080/docs/ - but it'll depend on your settings. -## Advanced API usage using JWT tokens +### Advanced API usage using JWT tokens !!! Note The below should be done in an application (a Freqtrade REST API client, which fetches info via API), and is not intended to be used on a regular basis. @@ -294,9 +304,9 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` -## CORS +### CORS -All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +All web-based front-ends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 21c5d6812..784b99bed 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -10,8 +10,8 @@ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, - start_new_strategy) +from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, + start_new_hyperopt, start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a6c8a245f..c64c11a18 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -70,6 +70,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe"] +ARGS_INSTALL_UI = ["erase_ui_only"] + ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", @@ -167,8 +169,8 @@ class Arguments: from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, start_list_timeframes, start_new_config, start_new_hyperopt, start_new_strategy, start_plot_dataframe, start_plot_profit, @@ -355,6 +357,14 @@ class Arguments: test_pairlist_cmd.set_defaults(func=start_test_pairlist) self._build_args(optionlist=ARGS_TEST_PAIRLIST, parser=test_pairlist_cmd) + # Add install-ui subcommand + install_ui_cmd = subparsers.add_parser( + 'install-ui', + help='Install FreqUI', + ) + install_ui_cmd.set_defaults(func=start_install_ui) + self._build_args(optionlist=ARGS_INSTALL_UI, parser=install_ui_cmd) + # Add Plotting subcommand plot_dataframe_cmd = subparsers.add_parser( 'plot-dataframe', diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 668b4abf5..7dc85377d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -387,6 +387,12 @@ AVAILABLE_CLI_OPTIONS = { help='Clean all existing data for the selected exchange/pairs/timeframes.', action='store_true', ), + "erase_ui_only": Arg( + '--erase', + help="Clean UI folder, don't download new version.", + action='store_true', + default=False, + ), # Templating options "template": Arg( '--template', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index a0105e140..5ba3db9f9 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -1,7 +1,9 @@ import logging import sys from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Optional, Tuple + +import requests from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir @@ -137,3 +139,87 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") + + +def clean_ui_subdir(directory: Path): + if directory.is_dir(): + logger.info("Removing UI directory content.") + + for p in reversed(list(directory.glob('**/*'))): # iterate contents from leaves to root + if p.name in ('.gitkeep', 'fallback_file.html'): + continue + if p.is_file(): + p.unlink() + elif p.is_dir(): + p.rmdir() + + +def read_ui_version(dest_folder: Path) -> Optional[str]: + file = dest_folder / '.uiversion' + if not file.is_file(): + return None + + with file.open('r') as f: + return f.read() + + +def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): + from io import BytesIO + from zipfile import ZipFile + + logger.info(f"Downloading {dl_url}") + resp = requests.get(dl_url).content + dest_folder.mkdir(parents=True, exist_ok=True) + with ZipFile(BytesIO(resp)) as zf: + for fn in zf.filelist: + with zf.open(fn) as x: + destfile = dest_folder / fn.filename + if fn.is_dir(): + destfile.mkdir(exist_ok=True) + else: + destfile.write_bytes(x.read()) + with (dest_folder / '.uiversion').open('w') as f: + f.write(version) + + +def get_ui_download_url() -> Tuple[str, str]: + base_url = 'https://api.github.com/repos/freqtrade/frequi/' + # Get base UI Repo path + + resp = requests.get(f"{base_url}releases") + resp.raise_for_status() + r = resp.json() + + latest_version = r[0]['name'] + assets = r[0].get('assets', []) + dl_url = '' + if assets and len(assets) > 0: + dl_url = assets[0]['browser_download_url'] + + # URL not found - try assets url + if not dl_url: + assets = r[0]['assets_url'] + resp = requests.get(assets) + r = resp.json() + dl_url = r[0]['browser_download_url'] + + return dl_url, latest_version + + +def start_install_ui(args: Dict[str, Any]) -> None: + + dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' + # First make sure the assets are removed. + dl_url, latest_version = get_ui_download_url() + + curr_version = read_ui_version(dest_folder) + if curr_version == latest_version and not args.get('erase_ui_only'): + logger.info(f"UI already up-to-date, FreqUI Version {curr_version}.") + return + + clean_ui_subdir(dest_folder) + if args.get('erase_ui_only'): + logger.info("Erased UI directory content. Not downloading new version.") + else: + # Download a new version + download_and_install_ui(dest_folder, dl_url, latest_version) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 69301ca0e..802ddc2b1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -45,6 +45,16 @@ USERPATH_NOTEBOOKS = 'notebooks' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] + +# Define decimals per coin for outputs +# Only used for outputs. +DECIMAL_PER_COIN_FALLBACK = 3 # Should be low to avoid listing all possible FIAT's +DECIMALS_PER_COIN = { + 'BTC': 8, + 'ETH': 5, +} + + # Soure files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index e549a3701..2bdef1c89 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -159,7 +159,8 @@ class Edge: available_capital = (total_capital + capital_in_trade) * self._capital_ratio allowed_capital_at_risk = available_capital * self._allowed_risk max_position_size = abs(allowed_capital_at_risk / stoploss) - position_size = min(max_position_size, free_capital) + # Position size must be below available capital. + position_size = min(min(max_position_size, free_capital), available_capital) if pair in self._cached_pairs: logger.info( 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2656daab5..a6eb75d5b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -233,7 +233,7 @@ class FreqtradeBot(LoggingMixin): _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) return _whitelist - def get_free_open_trades(self): + def get_free_open_trades(self) -> int: """ Return the number of free open trades slots or 0 if max number of open trades reached @@ -246,7 +246,7 @@ class FreqtradeBot(LoggingMixin): Updates open orders based on order list kept in the database. Mainly updates the state of orders - but may also close trades """ - if self.config['dry_run']: + if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): # Updating open orders in dry-run does not make sense and will fail. return @@ -439,83 +439,6 @@ class FreqtradeBot(LoggingMixin): return used_rate - def get_trade_stake_amount(self, pair: str) -> float: - """ - Calculate stake amount for the trade - :return: float: Stake amount - :raise: DependencyException if the available stake amount is too low - """ - stake_amount: float - # Ensure wallets are uptodate. - self.wallets.update() - - if self.edge: - stake_amount = self.edge.stake_amount( - pair, - self.wallets.get_free(self.config['stake_currency']), - self.wallets.get_total(self.config['stake_currency']), - Trade.total_open_trades_stakes() - ) - else: - stake_amount = self.config['stake_amount'] - if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - stake_amount = self._calculate_unlimited_stake_amount() - - return self._check_available_stake_amount(stake_amount) - - def _get_available_stake_amount(self) -> float: - """ - Return the total currently available balance in stake currency, - respecting tradable_balance_ratio. - Calculated as - + free amount ) * tradable_balance_ratio - - """ - val_tied_up = Trade.total_open_trades_stakes() - - # Ensure % is used from the overall balance - # Otherwise we'd risk lowering stakes with each open trade. - # (tied up + current free) * ratio) - tied up - available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * - self.config['tradable_balance_ratio']) - val_tied_up - return available_amount - - def _calculate_unlimited_stake_amount(self) -> float: - """ - Calculate stake amount for "unlimited" stake amount - :return: 0 if max number of trades reached, else stake_amount to use. - """ - free_open_trades = self.get_free_open_trades() - if not free_open_trades: - return 0 - - available_amount = self._get_available_stake_amount() - - return available_amount / free_open_trades - - def _check_available_stake_amount(self, stake_amount: float) -> float: - """ - Check if stake amount can be fulfilled with the available balance - for the stake currency - :return: float: Stake amount - """ - available_amount = self._get_available_stake_amount() - - if self.config['amend_last_stake_amount']: - # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio - # Otherwise the remaining amount is too low to trade. - if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']): - stake_amount = min(stake_amount, available_amount) - else: - stake_amount = 0 - - if available_amount < stake_amount: - raise DependencyException( - f"Available balance ({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" - ) - - return stake_amount - def create_trade(self, pair: str) -> bool: """ Check the implemented trading strategy for buy signals. @@ -549,7 +472,8 @@ class FreqtradeBot(LoggingMixin): (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: - stake_amount = self.get_trade_stake_amount(pair) + stake_amount = self.wallets.get_trade_stake_amount(pair, self.get_free_open_trades(), + self.edge) if not stake_amount: logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") return False @@ -1146,7 +1070,9 @@ class FreqtradeBot(LoggingMixin): if not self.exchange.check_order_canceled_empty(order): try: # if trade is not partially completed, just delete the order - self.exchange.cancel_order(trade.open_order_id, trade.pair) + co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") return 'error cancelling order' @@ -1154,6 +1080,7 @@ class FreqtradeBot(LoggingMixin): else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info('Sell order %s for %s.', reason, trade) + trade.update_order(order) trade.close_rate = None trade.close_rate_requested = None diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 22e14b564..7bbc24056 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -11,10 +11,35 @@ from typing.io import IO import rapidjson +from freqtrade.constants import DECIMAL_PER_COIN_FALLBACK, DECIMALS_PER_COIN + logger = logging.getLogger(__name__) +def decimals_per_coin(coin: str): + """ + Helper method getting decimal amount for this coin + example usage: f".{decimals_per_coin('USD')}f" + :param coin: Which coin are we printing the price / value for + """ + return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) + + +def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: + """ + Get price value for this coin + :param value: Value to be printed + :param coin: Which coin are we printing the price / value for + :param show_coin_name: Return string in format: "222.22 USDT" or "222.22" + :return: Formatted / rounded value (with or without coin name) + """ + if show_coin_name: + return f"{value:.{decimals_per_coin(coin)}f} {coin}" + else: + return f"{value:.{decimals_per_coin(coin)}f}" + + def shorten_date(_date: str) -> str: """ Trim the date so it fits on small screens diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8edfbaf8d..118253e86 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -10,7 +10,7 @@ from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown -from freqtrade.misc import file_dump_json +from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value logger = logging.getLogger(__name__) @@ -38,11 +38,12 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def _get_line_floatfmt() -> List[str]: +def _get_line_floatfmt(stake_currency: str) -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) """ - return ['s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', 'd', 'd', 'd'] + return ['s', 'd', '.2f', '.2f', f'.{decimals_per_coin(stake_currency)}f', + '.2f', 'd', 'd', 'd', 'd'] def _get_line_header(first_column: str, stake_currency: str) -> List[str]: @@ -352,7 +353,7 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st """ headers = _get_line_header('Pair', stake_currency) - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) output = [[ t['key'], t['trades'], t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], t['duration_avg'], t['wins'], t['draws'], t['losses'] @@ -383,7 +384,9 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren output = [[ t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], - t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], + t['profit_mean_pct'], t['profit_sum_pct'], + round_coin_value(t['profit_total_abs'], stake_currency, False), + t['profit_total_pct'], ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") @@ -396,7 +399,7 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: :param all_results: Dict of containing results for all strategies :return: pretty printed table with tabulate as string """ - floatfmt = _get_line_floatfmt() + floatfmt = _get_line_floatfmt(stake_currency) headers = _get_line_header('Strategy', stake_currency) output = [[ diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 375709423..dff59819c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -171,6 +171,10 @@ class Order(_DECL_BASE): """ Get all non-closed orders - useful when trying to batch-update orders """ + if not isinstance(order, dict): + logger.warning(f"{order} is not a valid response object.") + return + filtered_orders = [o for o in orders if o.order_id == order.get('id')] if filtered_orders: oobj = filtered_orders[0] diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f45ba9b25..4325e537e 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -53,7 +53,7 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): data_format=config.get('dataformat_ohlcv', 'json'), ) - if startup_candles: + if startup_candles and data: min_date, max_date = get_timerange(data) logger.info(f"Loading data from {min_date} to {max_date}") timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), @@ -67,14 +67,16 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): if not filename.is_dir() and not filename.is_file(): logger.warning("Backtest file is missing skipping trades.") no_trades = True - - trades = load_trades( - config['trade_source'], - db_url=config.get('db_url'), - exportfilename=filename, - no_trades=no_trades, - strategy=config.get('strategy'), - ) + try: + trades = load_trades( + config['trade_source'], + db_url=config.get('db_url'), + exportfilename=filename, + no_trades=no_trades, + strategy=config.get('strategy'), + ) + except ValueError as e: + raise OperationalException(e) from e trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index a2082103b..3588f2196 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -167,7 +167,7 @@ def reload_config(rpc: RPC = Depends(get_rpc)): @router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) -def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc=Depends(get_rpc)): +def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Depends(get_rpc)): return rpc._rpc_analysed_dataframe(pair, timeframe, limit) diff --git a/freqtrade/rpc/api_server/ui/fallback_file.html b/freqtrade/rpc/api_server/ui/fallback_file.html new file mode 100644 index 000000000..7943530af --- /dev/null +++ b/freqtrade/rpc/api_server/ui/fallback_file.html @@ -0,0 +1,31 @@ + + + + + + Freqtrade UI + + + +
+

Freqtrade UI not installed.

+

Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.

+

You can then refresh this page and you should see the UI.

+
+ + diff --git a/freqtrade/rpc/api_server/ui/favicon.ico b/freqtrade/rpc/api_server/ui/favicon.ico new file mode 100644 index 000000000..78c7e43b1 Binary files /dev/null and b/freqtrade/rpc/api_server/ui/favicon.ico differ diff --git a/freqtrade/rpc/api_server/ui/installed/.gitkeep b/freqtrade/rpc/api_server/ui/installed/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py new file mode 100644 index 000000000..6d7e77953 --- /dev/null +++ b/freqtrade/rpc/api_server/web_ui.py @@ -0,0 +1,31 @@ +from pathlib import Path + +from fastapi import APIRouter +from fastapi.exceptions import HTTPException +from starlette.responses import FileResponse + + +router_ui = APIRouter() + + +@router_ui.get('/favicon.ico', include_in_schema=False) +async def favicon(): + return FileResponse(Path(__file__).parent / 'ui/favicon.ico') + + +@router_ui.get('/{rest_of_path:path}', include_in_schema=False) +async def index_html(rest_of_path: str): + """ + Emulate path fallback to index.html. + """ + if rest_of_path.startswith('api') or rest_of_path.startswith('.'): + raise HTTPException(status_code=404, detail="Not Found") + uibase = Path(__file__).parent / 'ui/installed/' + if (uibase / rest_of_path).is_file(): + return FileResponse(str(uibase / rest_of_path)) + + index_file = uibase / 'index.html' + if not index_file.is_file(): + return FileResponse(str(uibase.parent / 'fallback_file.html')) + # Fall back to index.html, as indicated by vue router docs + return FileResponse(str(index_file)) diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py index 9c0779274..f3eaa1ebc 100644 --- a/freqtrade/rpc/api_server/webserver.py +++ b/freqtrade/rpc/api_server/webserver.py @@ -57,12 +57,16 @@ class ApiServer(RPCHandler): from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login from freqtrade.rpc.api_server.api_v1 import router as api_v1 from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public + from freqtrade.rpc.api_server.web_ui import router_ui + app.include_router(api_v1_public, prefix="/api/v1") app.include_router(api_v1, prefix="/api/v1", dependencies=[Depends(http_basic_or_jwt_token)], ) app.include_router(router_login, prefix="/api/v1", tags=["auth"]) + # UI Router MUST be last! + app.include_router(router_ui, prefix='') app.add_middleware( CORSMiddleware, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 491d1cde6..7549c38be 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,7 +9,7 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow -from numpy import NAN, int64, mean +from numpy import NAN, inf, int64, mean from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange @@ -451,7 +451,7 @@ class RPC: pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency) rate = tickers.get(pair, {}).get('bid', None) if rate: - if pair.startswith(stake_currency): + if pair.startswith(stake_currency) and not pair.endswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total except (ExchangeError): @@ -590,7 +590,8 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade.get_trade_stake_amount(pair) + stakeamount = self._freqtrade.wallets.get_trade_stake_amount( + pair, self._freqtrade.get_free_open_trades()) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): @@ -746,6 +747,7 @@ class RPC: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) res = { @@ -774,7 +776,8 @@ class RPC: }) return res - def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: + def _rpc_analysed_dataframe(self, pair: str, timeframe: str, + limit: Optional[int]) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( pair, timeframe) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 80d7be60f..87cd7b43d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -18,6 +18,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.exceptions import OperationalException +from freqtrade.misc import round_coin_value from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType @@ -205,14 +206,14 @@ class Telegram(RPCHandler): else: msg['stake_amount_fiat'] = 0 - message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{limit:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg) + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") if msg.get('fiat_currency', None): - message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg) + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" message += ")`" elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: @@ -352,6 +353,7 @@ class Telegram(RPCHandler): try: statlist, head = self._rpc._rpc_status_table( self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + message = tabulate(statlist, headers=head, tablefmt='simple') if(update.callback_query): query = update.callback_query @@ -384,7 +386,7 @@ class Telegram(RPCHandler): ) stats_tab = tabulate( [[day['date'], - f"{day['abs_profit']:.8f} {stats['stake_currency']}", + f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}", f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ @@ -438,18 +440,18 @@ class Telegram(RPCHandler): # Message to display if stats['closed_trade_count'] > 0: markdown_msg = ("*ROI:* Closed trades\n" - f"∙ `{profit_closed_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} " f"({profit_closed_percent_mean:.2f}%) " f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n") + f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n") else: markdown_msg = "`No closed trade` \n" markdown_msg += (f"*ROI:* All trades\n" - f"∙ `{profit_all_coin:.8f} {stake_cur} " + f"∙ `{round_coin_value(profit_all_coin, stake_cur)} " f"({profit_all_percent_mean:.2f}%) " f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n" - f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" + f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" f"*Latest Trade opened:* `{latest_trade_date}\n`" @@ -521,15 +523,17 @@ class Telegram(RPCHandler): "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) - for currency in result['currencies']: - if currency['est_stake'] > 0.0001: - curr_output = ("*{currency}:*\n" - "\t`Available: {free: .8f}`\n" - "\t`Balance: {balance: .8f}`\n" - "\t`Pending: {used: .8f}`\n" - "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency) + for curr in result['currencies']: + if curr['est_stake'] > 0.0001: + curr_output = ( + f"*{curr['currency']}:*\n" + f"\t`Available: {curr['free']:.8f}`\n" + f"\t`Balance: {curr['balance']:.8f}`\n" + f"\t`Pending: {curr['used']:.8f}`\n" + f"\t`Est. {curr['stake']}: " + f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n") else: - curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) + curr_output = f"*{curr['currency']}:* not showing <1$ amount \n" # Handle overflowing messsage length if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: @@ -539,8 +543,9 @@ class Telegram(RPCHandler): output += curr_output output += ("\n*Estimated Value*:\n" - "\t`{stake}: {total: .8f}`\n" - "\t`{symbol}: {value: .2f}`\n").format(**result) + f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['symbol']}: " + f"{round_coin_value(result['value'], result['symbol'], False)}`\n") if(update.callback_query): query = update.callback_query self._update_msg(chat_id=query.message.chat_id, message_id=query.message.message_id, msg=output, callback_path="update_balance", reload_able=True) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 4a1b43e36..dd6b773e1 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -5,7 +5,7 @@ import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- # Add your lib to import here diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index b3f9fef07..db1ba48b8 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -17,7 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib class SampleStrategy(IStrategy): """ This is a sample strategy to inspire you. - More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md + More information in https://www.freqtrade.io/en/latest/strategy-customization/ You can: :return: a Dataframe with all mandatory indicators for the strategies diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 3680dd416..d7dcfd487 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple import arrow +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -118,3 +120,79 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets + + def _get_available_stake_amount(self) -> float: + """ + Return the total currently available balance in stake currency, + respecting tradable_balance_ratio. + Calculated as + ( + free amount ) * tradable_balance_ratio - + """ + val_tied_up = Trade.total_open_trades_stakes() + + # Ensure % is used from the overall balance + # Otherwise we'd risk lowering stakes with each open trade. + # (tied up + current free) * ratio) - tied up + available_amount = ((val_tied_up + self.get_free(self._config['stake_currency'])) * + self._config['tradable_balance_ratio']) - val_tied_up + return available_amount + + def _calculate_unlimited_stake_amount(self, free_open_trades: int) -> float: + """ + Calculate stake amount for "unlimited" stake amount + :return: 0 if max number of trades reached, else stake_amount to use. + """ + if not free_open_trades: + return 0 + + available_amount = self._get_available_stake_amount() + + return available_amount / free_open_trades + + def _check_available_stake_amount(self, stake_amount: float) -> float: + """ + Check if stake amount can be fulfilled with the available balance + for the stake currency + :return: float: Stake amount + """ + available_amount = self._get_available_stake_amount() + + if self._config['amend_last_stake_amount']: + # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio + # Otherwise the remaining amount is too low to trade. + if available_amount > (stake_amount * self._config['last_stake_amount_min_ratio']): + stake_amount = min(stake_amount, available_amount) + else: + stake_amount = 0 + + if available_amount < stake_amount: + raise DependencyException( + f"Available balance ({available_amount} {self._config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self._config['stake_currency']})" + ) + + return stake_amount + + def get_trade_stake_amount(self, pair: str, free_open_trades: int, edge=None) -> float: + """ + Calculate stake amount for the trade + :return: float: Stake amount + :raise: DependencyException if the available stake amount is too low + """ + stake_amount: float + # Ensure wallets are uptodate. + self.update() + + if edge: + stake_amount = edge.stake_amount( + pair, + self.get_free(self._config['stake_currency']), + self.get_total(self._config['stake_currency']), + Trade.total_open_trades_stakes() + ) + else: + stake_amount = self._config['stake_amount'] + if stake_amount == UNLIMITED_STAKE_AMOUNT: + stake_amount = self._calculate_unlimited_stake_amount(free_open_trades) + + return self._check_available_stake_amount(stake_amount) diff --git a/mkdocs.yml b/mkdocs.yml index 47ab8ec48..18fccc333 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -14,7 +14,7 @@ nav: - Control the bot: - Telegram: telegram-usage.md - Web Hook: webhook-config.md - - REST API: rest-api.md + - REST API & FreqUI: rest-api.md - Data Downloading: data-download.md - Backtesting: backtesting.md - Hyperopt: hyperopt.md diff --git a/requirements.txt b/requirements.txt index e247d4b00..5d03a9c2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,12 @@ -numpy==1.20.0 +numpy==1.20.1 pandas==1.2.1 -ccxt==1.41.35 +ccxt==1.41.70 +# Pin cryptography for now due to rust build errors with piwheels +cryptography==3.3.2 aiohttp==3.7.3 -SQLAlchemy==1.3.22 -python-telegram-bot==13.1 +SQLAlchemy==1.3.23 +python-telegram-bot==13.2 arrow==0.17.0 cachetools==4.2.1 requests==2.25.1 @@ -19,7 +21,7 @@ tables==3.6.1 blosc==1.10.2 # find first, C search in arrays -py_find_1st==1.1.4 +py_find_1st==1.1.5 # Load ticker files 30% faster python-rapidjson==1.0 @@ -31,6 +33,7 @@ sdnotify==0.3.2 fastapi==0.63.0 uvicorn==0.13.3 pyjwt==2.0.1 +aiofiles==0.6.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index d655174b3..c81909025 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1,16 +1,20 @@ import re +from io import BytesIO from pathlib import Path from unittest.mock import MagicMock, PropertyMock +from zipfile import ZipFile import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_list, start_hyperopt_show, start_install_ui, + start_list_data, start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, start_list_timeframes, + start_new_hyperopt, start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) +from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, + get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -546,7 +550,7 @@ def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): start_new_hyperopt(get_args(args)) -def test_start_new_hyperopt_no_arg(mocker, caplog): +def test_start_new_hyperopt_no_arg(mocker): args = [ "new-hyperopt", ] @@ -555,6 +559,107 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): start_new_hyperopt(get_args(args)) +def test_start_install_ui(mocker): + clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') + get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url', + return_value=('https://example.com/whatever', '0.0.1')) + download_mock = mocker.patch('freqtrade.commands.deploy_commands.download_and_install_ui') + mocker.patch('freqtrade.commands.deploy_commands.read_ui_version', return_value=None) + args = [ + "install-ui", + ] + start_install_ui(get_args(args)) + assert clean_mock.call_count == 1 + assert get_url_mock.call_count == 1 + assert download_mock.call_count == 1 + + clean_mock.reset_mock() + get_url_mock.reset_mock() + download_mock.reset_mock() + + args = [ + "install-ui", + "--erase", + ] + start_install_ui(get_args(args)) + assert clean_mock.call_count == 1 + assert get_url_mock.call_count == 1 + assert download_mock.call_count == 0 + + +def test_clean_ui_subdir(mocker, tmpdir, caplog): + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, True]) + mocker.patch("freqtrade.commands.deploy_commands.Path.is_file", + side_effect=[False, True]) + rd_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.rmdir") + ul_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.unlink") + + mocker.patch("freqtrade.commands.deploy_commands.Path.glob", + return_value=[Path('test1'), Path('test2'), Path('.gitkeep')]) + folder = Path(tmpdir) / "uitests" + clean_ui_subdir(folder) + assert log_has("Removing UI directory content.", caplog) + assert rd_mock.call_count == 1 + assert ul_mock.call_count == 1 + + +def test_download_and_install_ui(mocker, tmpdir): + # Create zipfile + requests_mock = MagicMock() + file_like_object = BytesIO() + with ZipFile(file_like_object, mode='w') as zipfile: + for file in ('test1.txt', 'hello/', 'test2.txt'): + zipfile.writestr(file, file) + file_like_object.seek(0) + requests_mock.content = file_like_object.read() + + mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=requests_mock) + + mocker.patch("freqtrade.commands.deploy_commands.Path.is_dir", + side_effect=[True, False]) + wb_mock = mocker.patch("freqtrade.commands.deploy_commands.Path.write_bytes") + + folder = Path(tmpdir) / "uitests_dl" + folder.mkdir(exist_ok=True) + + assert read_ui_version(folder) is None + + download_and_install_ui(folder, 'http://whatever.xxx/download/file.zip', '22') + + assert wb_mock.call_count == 2 + + assert read_ui_version(folder) == '22' + + +def test_get_ui_download_url(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{'assets_url': 'http://whatever.json', 'name': '0.0.1'}], + [{'browser_download_url': 'http://download.zip'}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x, last_version = get_ui_download_url() + assert get_mock.call_count == 2 + assert last_version == '0.0.1' + assert x == 'http://download.zip' + + +def test_get_ui_download_url_direct(mocker): + response = MagicMock() + response.json = MagicMock( + side_effect=[[{ + 'assets_url': 'http://whatever.json', + 'name': '0.0.1', + 'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) + get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", + return_value=response) + x, last_version = get_ui_download_url() + assert get_mock.call_count == 1 + assert last_version == '0.0.1' + assert x == 'http://download11.zip' + + def test_download_data_keyboardInterrupt(mocker, caplog, markets): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index f25dad35b..c30bce6a4 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -209,7 +209,7 @@ def test_nonexisting_stoploss(mocker, edge_conf): assert edge.stoploss('N/O') == -0.1 -def test_stake_amount(mocker, edge_conf): +def test_edge_stake_amount(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( @@ -217,20 +217,33 @@ def test_stake_amount(mocker, edge_conf): 'E/F': PairInfo(-0.02, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - free = 100 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 31.25 + assert edge._capital_ratio == 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 31.25 - free = 20 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 20 + assert edge.stake_amount('E/F', free_capital=20, total_capital=100, + capital_in_trade=25) == 20 - free = 0 - total = 100 - in_trade = 25 - assert edge.stake_amount('E/F', free, total, in_trade) == 0 + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=25) == 0 + + # Test with increased allowed_risk + # Result should be no more than allowed capital + edge._allowed_risk = 0.4 + edge._capital_ratio = 0.5 + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=25) == 62.5 + + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 50 + + edge._capital_ratio = 1 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=100, total_capital=100, + capital_in_trade=0) == 100 + # Full capital is available + assert edge.stake_amount('E/F', free_capital=0, total_capital=100, + capital_in_trade=0) == 0 def test_nonexisting_stake_amount(mocker, edge_conf): diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 0c8b7bdcf..8e1d074aa 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -18,7 +18,7 @@ EXCHANGES = { 'bittrex': { 'pair': 'BTC/USDT', 'hasQuoteVolume': False, - 'timeframe': '5m', + 'timeframe': '1h', }, 'binance': { 'pair': 'BTC/USDT', @@ -120,7 +120,9 @@ class TestCCXTExchange(): ohlcv = exchange.refresh_latest_ohlcv([pair_tf]) assert isinstance(ohlcv, dict) assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf)) - assert len(exchange.klines(pair_tf)) > 200 + # assert len(exchange.klines(pair_tf)) > 200 + # Assume 90% uptime ... + assert len(exchange.klines(pair_tf)) > exchange._ohlcv_candle_limit * 0.90 # TODO: tests fetch_trades (?) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cd24e113e..f35a84725 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2072,9 +2072,9 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap def test_cancel_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) @@ -2091,9 +2091,9 @@ def test_cancel_order(default_conf, mocker, exchange_name): def test_cancel_stoploss_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = False api_mock = MagicMock() - api_mock.cancel_order = MagicMock(return_value=123) + api_mock.cancel_order = MagicMock(return_value={'id': '123'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 + assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == {'id': '123'} with pytest.raises(InvalidOrderException): api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) @@ -2462,6 +2462,9 @@ def test_timeframe_to_prev_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_prev_date("5m") < date + # Does not round + time = datetime(2019, 8, 12, 13, 20, 0, tzinfo=timezone.utc) + assert timeframe_to_prev_date('5m', time) == time def test_timeframe_to_next_date(): diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5f811e2e5..c8d4338af 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -341,12 +341,14 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') + sbs = mocker.patch('freqtrade.optimize.backtesting.store_backtest_stats') mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' default_conf['datadir'] = testdatadir - default_conf['export'] = None + default_conf['export'] = 'trades' + default_conf['exportfilename'] = 'export.txt' default_conf['timerange'] = '-1510694220' backtesting = Backtesting(default_conf) @@ -361,6 +363,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: assert log_has(line, caplog) assert backtesting.strategy.dp._pairlists is not None assert backtesting.strategy.bot_loop_start.call_count == 1 + assert sbs.call_count == 1 def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 71f1b3172..263950d83 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -83,11 +83,26 @@ def assert_response(response, expected_code=200, needs_cors=True): def test_api_not_found(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/invalid_url") + rc = client_get(client, f"{BASE_URI}/invalid_url") assert_response(rc, 404) assert rc.json() == {"detail": "Not Found"} +def test_api_ui_fallback(botclient): + ftbot, client = botclient + + rc = client_get(client, "/favicon.ico") + assert rc.status_code == 200 + + rc = client_get(client, "/fallback_file.html") + assert rc.status_code == 200 + assert '`freqtrade install-ui`' in rc.text + + # Forwarded to fallback_html or index.html (depending if it's installed or not) + rc = client_get(client, "/something") + assert rc.status_code == 200 + + def test_api_auth(): with pytest.raises(ValueError): create_token({'identity': {'u': 'Freqtrade'}}, 'secret1234', token_type="NotATokenType") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1c34b6b26..f065bb4c5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -519,7 +519,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*EUR:*' in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12.00000000' in result + assert 'BTC: 12.00000000' in result assert '*XRP:* not showing <1$ amount' in result @@ -1205,7 +1205,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.001000 BTC, 12.345 USD)`' + '*Total:* `(0.00100000 BTC, 12.345 USD)`' freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1389,7 +1389,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.001000 BTC)`') + '*Total:* `(0.00100000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index bebbc1508..94c3e24f6 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -743,18 +743,18 @@ def test_set_loggers_journald_importerror(mocker, import_fails): logger.handlers = orig_handlers -def test_set_logfile(default_conf, mocker): +def test_set_logfile(default_conf, mocker, tmpdir): patched_configuration_load_config_file(mocker, default_conf) - + f = Path(tmpdir / "test_file.log") + assert not f.is_file() arglist = [ - 'trade', '--logfile', 'test_file.log', + 'trade', '--logfile', str(f), ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) validated_conf = configuration.load_config() - assert validated_conf['logfile'] == "test_file.log" - f = Path("test_file.log") + assert validated_conf['logfile'] == str(f) assert f.is_file() try: f.unlink() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index abb91d66b..3bd2f5607 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -158,7 +158,8 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount( + 'ETH/BTC', freqtrade.get_free_open_trades()) assert result == default_conf['stake_amount'] @@ -194,12 +195,14 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b if expected[i] is not None: limit_buy_order_open['id'] = str(i) - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) assert pytest.approx(result) == expected[i] freqtrade.execute_buy('ETH/BTC', result) else: with pytest.raises(DependencyException): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', + freqtrade.get_free_open_trades()) def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @@ -210,7 +213,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.get_trade_stake_amount('ETH/BTC') + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) @pytest.mark.parametrize("balance_ratio,result1", [ @@ -239,25 +242,25 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.get_trade_stake_amount('ETH/BTC') + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/BTC', result) - result = freqtrade.get_trade_stake_amount('LTC/BTC') + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.get_trade_stake_amount('XRP/BTC') + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 freqtrade = FreqtradeBot(conf) - result = freqtrade.get_trade_stake_amount('NEO/BTC') + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) assert result == 0 @@ -283,8 +286,10 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) - assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 - assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 + assert freqtrade.wallets.get_trade_stake_amount( + 'NEO/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.20 + assert freqtrade.wallets.get_trade_stake_amount( + 'LTC/BTC', freqtrade.get_free_open_trades(), freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -500,7 +505,8 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0 + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades(), + freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, @@ -2603,7 +2609,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch( - 'freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + 'freqtrade.exchange.Exchange.cancel_order_with_result', side_effect=InvalidOrderException()) freqtrade = FreqtradeBot(default_conf) diff --git a/tests/test_integration.py b/tests/test_integration.py index 9695977ac..8e3bd251a 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -178,7 +178,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 - assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) == result1 rpc._rpc_forcebuy('TKN/BTC', None) @@ -199,7 +200,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc # One trade sold assert len(trades) == 4 # stake-amount should now be reduced, since one trade was sold at a loss. - assert freqtrade.get_trade_stake_amount('XRP/BTC') < result1 + assert freqtrade.wallets.get_trade_stake_amount( + 'XRP/BTC', freqtrade.get_free_open_trades()) < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 diff --git a/tests/test_misc.py b/tests/test_misc.py index 429da135a..e6ba70aee 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,9 +6,31 @@ from unittest.mock import MagicMock import pytest -from freqtrade.misc import (file_dump_json, file_load_json, format_ms_time, pair_to_filename, - plural, render_template, render_template_with_fallback, - safe_value_fallback, safe_value_fallback2, shorten_date) +from freqtrade.misc import (decimals_per_coin, file_dump_json, file_load_json, format_ms_time, + pair_to_filename, plural, render_template, + render_template_with_fallback, round_coin_value, safe_value_fallback, + safe_value_fallback2, shorten_date) + + +def test_decimals_per_coin(): + assert decimals_per_coin('USDT') == 3 + assert decimals_per_coin('EUR') == 3 + assert decimals_per_coin('BTC') == 8 + assert decimals_per_coin('ETH') == 5 + + +def test_round_coin_value(): + assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' + assert round_coin_value(222.2, 'USDT') == '222.200 USDT' + assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' + assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' + assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' + + assert round_coin_value(222.222222, 'USDT', False) == '222.222' + assert round_coin_value(222.2, 'USDT', False) == '222.200' + assert round_coin_value(222.12745, 'EUR', False) == '222.127' + assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' + assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' def test_shorten_date() -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 9921f541b..d0d29f142 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1074,7 +1074,7 @@ def test_get_best_pair(fee): @pytest.mark.usefixtures("init_persistence") -def test_update_order_from_ccxt(): +def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') assert isinstance(o, Order) @@ -1120,6 +1120,14 @@ def test_update_order_from_ccxt(): with pytest.raises(DependencyException, match=r"Order-id's don't match"): o.update_from_ccxt_object(ccxt_order) + message = "aaaa is not a valid response object." + assert not log_has(message, caplog) + Order.update_orders([o], 'aaaa') + assert log_has(message, caplog) + + # Call regular update - shouldn't fail. + Order.update_orders([o], {'id': '1234'}) + @pytest.mark.usefixtures("init_persistence") def test_select_order(fee):