Merge branch 'develop' into patch-1
This commit is contained in:
commit
af98e025d1
1
.gitignore
vendored
1
.gitignore
vendored
@ -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__/
|
||||
|
@ -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" ]
|
||||
|
@ -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" ]
|
||||
|
@ -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
|
||||
|
@ -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/).
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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.<br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||
| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.<br>**Keep it in secret, do not disclose publicly.** <br> **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.<br>**Keep it in secret, do not disclose publicly.** <br> **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)). <br> **Datatype:** List
|
||||
| `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#pairlists-and-pairlist-handlers)). <br> **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). <br> **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). <br> **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) <br> **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) <br> **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) <br> **Datatype:** Dict
|
||||
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **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. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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 <command> [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 <command> [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.
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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',
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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,'
|
||||
|
@ -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
|
||||
<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes>
|
||||
"""
|
||||
val_tied_up = Trade.total_open_trades_stakes()
|
||||
|
||||
# Ensure <tradable_balance_ratio>% 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
|
||||
|
@ -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
|
||||
|
@ -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 <Strategyname: DataFrame> 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 = [[
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
31
freqtrade/rpc/api_server/ui/fallback_file.html
Normal file
31
freqtrade/rpc/api_server/ui/fallback_file.html
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Freqtrade UI</title>
|
||||
<style>
|
||||
body {
|
||||
background-color: #3c3c3c;
|
||||
color: #dedede;
|
||||
text-align: center;
|
||||
}
|
||||
.main-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
h1 {
|
||||
margin-top: 10rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-container">
|
||||
<h1>Freqtrade UI not installed.</h1>
|
||||
<p>Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.</p>
|
||||
<p>You can then refresh this page and you should see the UI.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
BIN
freqtrade/rpc/api_server/ui/favicon.ico
Normal file
BIN
freqtrade/rpc/api_server/ui/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 124 KiB |
0
freqtrade/rpc/api_server/ui/installed/.gitkeep
Normal file
0
freqtrade/rpc/api_server/ui/installed/.gitkeep
Normal file
31
freqtrade/rpc/api_server/web_ui.py
Normal file
31
freqtrade/rpc/api_server/web_ui.py
Normal file
@ -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))
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
(<open_trade stakes> + free amount ) * tradable_balance_ratio - <open_trade stakes>
|
||||
"""
|
||||
val_tied_up = Trade.total_open_trades_stakes()
|
||||
|
||||
# Ensure <tradable_balance_ratio>% 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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
@ -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):
|
||||
|
@ -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 (?)
|
||||
|
||||
|
@ -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():
|
||||
|
@ -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:
|
||||
|
@ -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")
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user