Merge branch 'develop' of https://github.com/Bovhasselt/Tradingbotv1.1 into develop

This commit is contained in:
tijnsteenmetz 2021-02-26 12:58:30 +01:00
commit 929b42446b
52 changed files with 862 additions and 218 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.9.1-slim-buster as base
FROM python:3.9.2-slim-buster as base
# Setup env
ENV LANG C.UTF-8

View File

@ -22,12 +22,21 @@ expect.
We strongly recommend you to have coding and Python knowledge. Do not
hesitate to read the source code and understand the mechanism of this bot.
## Exchange marketplaces supported
## Supported Exchange marketplaces
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
- [X] [Kraken](https://kraken.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
- [X] [FTX](https://ftx.com)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
## Documentation

View File

@ -51,6 +51,8 @@ fi
docker images
docker push ${IMAGE_NAME}
docker push ${IMAGE_NAME}:$TAG_PLOT
docker push ${IMAGE_NAME}:$TAG
if [ $? -ne 0 ]; then
echo "failed pushing repo"
return 1

View File

@ -40,6 +40,11 @@ For the sample below, you then need to add the command line parameter `--hyperop
A sample of this can be found below, which is identical to the Default Hyperopt loss implementation. A full sample can be found in [userdata/hyperopts](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_loss.py).
``` python
from datetime import datetime
from typing import Dict
from pandas import DataFrame
from freqtrade.optimize.hyperopt import IHyperOptLoss
TARGET_TRADES = 600
@ -54,6 +59,7 @@ class SuperDuperHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
@ -81,6 +87,8 @@ Currently, the arguments are:
* `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the timerange used
* `min_date`: End date of the timerange used
* `config`: Config object used (Note: Not all strategy-related parameters will be updated here if they are part of a hyperopt space).
* `processed`: Dict of Dataframes with the pair as keys containing the data used for backtesting.
This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you.

View File

@ -40,7 +40,9 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
#### Stoploss Guard
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
`StoplossGuard` selects all trades within `lookback_period` in minutes (or in candles when using `lookback_period_candles`).
If `trade_limit` or more trades resulted in stoploss, trading will stop for `stop_duration` in minutes (or in candles when using `stop_duration_candles`).
This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time.
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.

View File

@ -35,6 +35,22 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python
- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.).
- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md).
## Supported exchange marketplaces
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#blacklists))
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com)
- [X] [Kraken](https://kraken.com/)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested
Exchanges confirmed working by the community:
- [X] [Bitvavo](https://bitvavo.com/)
## Requirements
### Hardware requirements

View File

@ -188,7 +188,7 @@ Sample configuration with inline comments explaining the process:
'senkou_a': {
'color': 'green', #optional
'fill_to': 'senkou_b',
'fill_label': 'Ichimoku Cloud' #optional,
'fill_label': 'Ichimoku Cloud', #optional
'fill_color': 'rgba(255,76,46,0.2)', #optional
},
# plot senkou_b, too. Not only the area to it.

View File

@ -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

View File

@ -315,11 +315,11 @@ class AwesomeStrategy(IStrategy):
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Check if the entry already exists
if not metadata["pair"] in self._cust_info:
if not metadata["pair"] in self.cust_info:
# Create empty entry for this pair
self._cust_info[metadata["pair"]] = {}
self.cust_info[metadata["pair"]] = {}
if "crosstime" in self.cust_info[metadata["pair"]:
if "crosstime" in self.cust_info[metadata["pair"]]:
self.cust_info[metadata["pair"]]["crosstime"] += 1
else:
self.cust_info[metadata["pair"]]["crosstime"] = 1
@ -444,14 +444,19 @@ It can also be used in specific callbacks to get the signal that caused the acti
``` python
# fetch current dataframe
if self.dp:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
timeframe=self.timeframe)
if self.dp.runmode.value in ('live', 'dry_run'):
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
timeframe=self.timeframe)
```
!!! Note "No data available"
Returns an empty dataframe if the requested pair was not cached.
This should not happen when using whitelisted pairs.
!!! Warning "Warning about backtesting"
This method will return an empty dataframe during backtesting.
### *orderbook(pair, maximum)*
``` python
@ -462,8 +467,8 @@ if self.dp:
dataframe['best_ask'] = ob['asks'][0][0]
```
!!! Warning
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used.
!!! Warning "Warning about backtesting"
The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used, as the method will return uptodate values.
### *ticker(pair)*

View File

@ -24,7 +24,7 @@ config["strategy"] = "SampleStrategy"
# Location of the data
data_location = Path(config['user_data_dir'], 'data', 'binance')
# Pair to analyze - Only use one pair here
pair = "BTC_USDT"
pair = "BTC/USDT"
```
@ -34,7 +34,9 @@ from freqtrade.data.history import load_pair_history
candles = load_pair_history(datadir=data_location,
timeframe=config["timeframe"],
pair=pair)
pair=pair,
data_format = "hdf5",
)
# Confirm success
print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}")

View File

@ -12,7 +12,7 @@ from freqtrade.commands.data_commands import (start_convert_data, start_download
start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui,
start_new_hyperopt, start_new_strategy)
from freqtrade.commands.automation_commands import start_build_hyperopt
from freqtrade.commands.automation_commands import (start_build_hyperopt, start_custom_hyperopt, start_extract_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,

View File

@ -55,8 +55,13 @@ ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
# Automation
ARGS_BUILD_CUSTOM_HYPEROPT = ["buy_indicators", "sell_indicators", "hyperopt"]
ARGS_EXTRACT_STRATEGY = ["strategy", "extract_name"]
ARGS_BUILD_BUILD_HYPEROPT = ["strategy", "hyperopt"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
@ -175,7 +180,7 @@ class Arguments:
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_build_hyperopt,
start_build_hyperopt, start_custom_hyperopt, start_extract_strategy,
start_new_strategy, start_plot_dataframe, start_plot_profit,
start_show_trades, start_test_pairlist, start_trading)
@ -210,12 +215,24 @@ class Arguments:
build_hyperopt_cmd.set_defaults(func=start_new_hyperopt)
self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd)
# add build-hyperopt subcommand
build_custom_hyperopt_cmd = subparsers.add_parser('build-hyperopt',
# add custom-hyperopt subcommand
build_custom_hyperopt_cmd = subparsers.add_parser('custom-hyperopt',
help="Build a custom hyperopt")
build_custom_hyperopt_cmd.set_defaults(func=start_build_hyperopt)
build_custom_hyperopt_cmd.set_defaults(func=start_custom_hyperopt)
self._build_args(optionlist=ARGS_BUILD_CUSTOM_HYPEROPT, parser=build_custom_hyperopt_cmd)
# add extract-strategy subcommand
extract_strategy_cmd = subparsers.add_parser('extract-strategy',
help="Extract data dictionaries for custom-hyperopt from strategy")
extract_strategy_cmd.set_defaults(func=start_extract_strategy)
self._build_args(optionlist=ARGS_EXTRACT_STRATEGY, parser=extract_strategy_cmd)
# add build-hyperopt subcommand
build_extracted_hyperopt_cmd = subparsers.add_parser('build-hyperopt',
help="Create a hyperopt for a strategy")
build_extracted_hyperopt_cmd.set_defaults(func=start_build_hyperopt)
self._build_args(optionlist=ARGS_BUILD_BUILD_HYPEROPT, parser=build_extracted_hyperopt_cmd)
# add new-strategy subcommand
build_strategy_cmd = subparsers.add_parser('new-strategy',
help="Create new strategy")

View File

@ -1,9 +1,10 @@
import ast
import logging
from pathlib import Path
from typing import Any, Dict
from typing import Any, Dict, List
from freqtrade.constants import USERPATH_HYPEROPTS
from freqtrade.constants import (USERPATH_HYPEROPTS,
USERPATH_STRATEGIES)
from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode
from freqtrade.configuration import setup_utils_configuration
@ -11,20 +12,139 @@ from freqtrade.misc import render_template
logger = logging.getLogger(__name__)
'''
TODO
-make the code below more dynamic with a large list of indicators and aims
-buy_space integer values variation based on aim(later deep learning)
-add --mode , see notes
-when making the strategy reading tool, make sure that the populate indicators gets copied to here
'''
POSSIBLE_GUARDS = ["rsi", "mfi", "fastd"]
POSSIBLE_TRIGGERS = ["bb_lowerband", "bb_upperband"]
POSSIBLE_VALUES = {"above": ">", "below": "<"}
# ---------------------------------------------------extract-strategy------------------------------------------------------
def get_indicator_info(file: List, indicators: Dict) -> None:
"""
Get all necessary information to build a custom hyperopt space using
the file and a dictionary filled with the indicators and their corropsonding line numbers.
"""
info_list = []
for indicator in indicators:
indicator_info = []
# find the corrosponding aim
for position, line in enumerate(file):
if position == indicators[indicator]:
# use split twice to remove the context around the indicator
back_of_line = line.split(f"(dataframe['{indicator}'] ", 1)[1]
aim = back_of_line.split()[0]
# add the indicator and aim to the info
indicator_info.append(indicator)
indicator_info.append(aim)
# check if first character after aim is a d in which case the indicator is a trigger
if back_of_line.split()[1][0] == "d":
indicator_info.append("trigger")
# add the second indicator of the guard to the info list
back_of_line = back_of_line.split("dataframe['")[1]
second_indicator = back_of_line.split("'])")[0]
indicator_info.append(second_indicator)
# elif indicator[0:3] == "CDL":
# indicator_info.append("guard")
# else it is a regular guard
else:
indicator_info.append("guard")
value = back_of_line.split()[1]
value = value[:-1]
value = float(value)
indicator_info.append(value)
info_list.append(indicator_info)
return info_list
def build_hyperopt_buyelements(buy_indicators: Dict[str, str]):
def extract_lists(strategypath: Path) -> None:
"""
Get the indicators, their aims and the stoploss and format them into lists
"""
# store the file in a list for reference
stored_file = []
with open(strategypath) as file:
for line in file:
stored_file.append(line)
# find the start and end of buy trend
for position, line in enumerate(stored_file):
if "populate_buy_trend(" in line:
start_buy_number = position
elif "populate_sell_trend(" in line:
end_buy_number = position
# list the numbers between the start and end of buy trend
buy_lines = []
for i in range(start_buy_number, end_buy_number):
buy_lines.append(i)
# populate the indicators dictionaries with indicators attached to the line they are on
buyindicators = {}
sellindicators = {}
for position, line in enumerate(stored_file):
# check the lines in buy trend for indicator and add them
if position in buy_lines and "(dataframe['" in line:
# use split twice to remove the context around the indicator
back_of_line = line.split("(dataframe['", 1)[1]
buyindicator = back_of_line.split("'] ", 1)[0]
buyindicators[buyindicator] = position
# check the lines in sell trend for indicator and add them
elif position > end_buy_number and "(dataframe['" in line:
# use split twice to remove the context around the indicator
back_of_line = line.split("(dataframe['", 1)[1]
sellindicator = back_of_line.split("'] ", 1)[0]
sellindicators[sellindicator] = position
# build the final lists
buy_info_list = get_indicator_info(stored_file, buyindicators)
sell_info_list = get_indicator_info(stored_file, sellindicators)
# put the final lists into a tuple
final_lists = (buy_info_list, sell_info_list)
return final_lists
def start_extract_strategy(args: Dict) -> None:
"""
Check if the right subcommands where passed and start extracting the strategy data
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
# check if all required options are filled in
if not 'strategy' in args or not args['strategy']:
raise OperationalException("`extract-strategy` requires --strategy to be set.")
else:
# if the name is not specified use (strategy)_extract
if not 'extract_name' in args or not args['extract_name']:
args['extract_name'] = args['strategy'] + "_extract"
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['extract_name'] + '.txt')
if new_path.exists():
raise OperationalException(f"`{new_path}` already exists. "
"Please choose another name.")
# the path of the chosen strategy
strategy_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
# extract the buy and sell indicators as dicts
extracted_lists = str(extract_lists(strategy_path))
# save the dicts in a file
logger.info(f"Writing custom hyperopt to `{new_path}`.")
new_path.write_text(extracted_lists)
# --------------------------------------------------custom-hyperopt------------------------------------------------------
def custom_hyperopt_buyelements(buy_indicators: List):
"""
Build the arguments with the placefillers for the buygenerator
"""
@ -32,39 +152,44 @@ def build_hyperopt_buyelements(buy_indicators: Dict[str, str]):
buy_triggers = ""
buy_space = ""
for indicator in buy_indicators:
# Error handling
if not indicator in POSSIBLE_GUARDS and not indicator in POSSIBLE_TRIGGERS:
raise OperationalException(
f"`{indicator}` is not part of the available indicators. The current options are {POSSIBLE_GUARDS + POSSIBLE_TRIGGERS}.")
elif not buy_indicators[indicator] in POSSIBLE_VALUES:
raise OperationalException(
f"`{buy_indicators[indicator]}` is not part of the available indicator options. The current options are {POSSIBLE_VALUES}.")
for indicator_info in buy_indicators:
indicator = indicator_info[0]
aim = indicator_info[1]
usage = indicator_info[2]
# If the indicator is a guard
elif indicator in POSSIBLE_GUARDS:
# get the symbol corrosponding to the value
aim = POSSIBLE_VALUES[buy_indicators[indicator]]
if usage == "guard":
value = indicator_info[3]
if value >= -1.0 and value <= 1.0:
lower_bound = value - 0.3
upper_bound = value + 0.3
else:
lower_bound = value - 30.0
upper_bound = value + 30.0
# add the guard to its argument
buy_guards += f"if '{indicator}-enabled' in params and params['{indicator}-enabled']: conditions.append(dataframe['{indicator}'] {aim} params['{indicator}-value'])"
buy_guards += f"if params.get('{indicator}-enabled'):\n conditions.append(dataframe['{indicator}'] {aim} params['{indicator}-value'])\n"
# add the space to its argument
buy_space += f"Integer(10, 90, name='{indicator}-value'), Categorical([True, False], name='{indicator}-enabled'),"
# If the indicator is a trigger
elif indicator in POSSIBLE_TRIGGERS:
# get the symbol corrosponding to the value
aim = POSSIBLE_VALUES[buy_indicators[indicator]]
buy_space += f"Integer({lower_bound}, {upper_bound}, name='{indicator}-value'),\nCategorical([True, False], name='{indicator}-enabled'),\n"
# If the indicator is a trigger
elif usage == "trigger":
secondindicator = indicator_info[3]
# add the trigger to its argument
buy_triggers += f"if params['trigger'] == '{indicator}': conditions.append(dataframe['{indicator}'] {aim} dataframe['close'])"
buy_triggers += f"if params['trigger'] == '{indicator}':\n conditions.append(dataframe['{indicator}'] {aim} dataframe['{secondindicator}'])\n"
# Final line of indicator space makes all triggers
buy_space += "Categorical(["
# adding all triggers to the list
for indicator in buy_indicators:
if indicator in POSSIBLE_TRIGGERS:
for indicator_info in buy_indicators:
indicator = indicator_info[0]
usage = indicator_info[2]
if usage == "trigger":
buy_space += f"'{indicator}', "
# Deleting the last ", "
@ -74,7 +199,7 @@ def build_hyperopt_buyelements(buy_indicators: Dict[str, str]):
return {"buy_guards": buy_guards, "buy_triggers": buy_triggers, "buy_space": buy_space}
def build_hyperopt_sellelements(sell_indicators: Dict[str, str]):
def custom_hyperopt_sellelements(sell_indicators: Dict[str, str]):
"""
Build the arguments with the placefillers for the sellgenerator
"""
@ -82,44 +207,50 @@ def build_hyperopt_sellelements(sell_indicators: Dict[str, str]):
sell_triggers = ""
sell_space = ""
for indicator in sell_indicators:
# Error handling
if not indicator in POSSIBLE_GUARDS and not indicator in POSSIBLE_TRIGGERS:
raise OperationalException(
f"`{indicator}` is not part of the available indicators. The current options are {POSSIBLE_GUARDS + POSSIBLE_TRIGGERS}.")
elif not sell_indicators[indicator] in POSSIBLE_VALUES:
raise OperationalException(
f"`{sell_indicators[indicator]}` is not part of the available indicator options. The current options are {POSSIBLE_VALUES}.")
# If indicator is a guard
elif indicator in POSSIBLE_GUARDS:
# get the symbol corrosponding to the value
aim = POSSIBLE_VALUES[sell_indicators[indicator]]
for indicator_info in sell_indicators:
indicator = indicator_info[0]
aim = indicator_info[1]
usage = indicator_info[2]
# If the indicator is a guard
if usage == "guard":
value = indicator_info[3]
if value >= -1 and value <= 1:
lower_bound = value - 0.3
upper_bound = value + 0.3
else:
lower_bound = value - 30
upper_bound = value + 30
# add the guard to its argument
sell_guards += f"if '{indicator}-enabled' in params and params['sell-{indicator}-enabled']: conditions.append(dataframe['{indicator}'] {aim} params['sell-{indicator}-value'])"
sell_guards += f"if params.get('sell-{indicator}-enabled'):\n conditions.append(dataframe['{indicator}'] {aim} params['sell-{indicator}-value'])\n"
# add the space to its argument
sell_space += f"Integer(10, 90, name='sell-{indicator}-value'), Categorical([True, False], name='sell-{indicator}-enabled'),"
sell_space += f"Integer({lower_bound}, {upper_bound}, name='sell-{indicator}-value'),\nCategorical([True, False], name='sell-{indicator}-enabled'),\n"
# If the indicator is a trigger
elif indicator in POSSIBLE_TRIGGERS:
# get the symbol corrosponding to the value
aim = POSSIBLE_VALUES[sell_indicators[indicator]]
elif usage == "trigger":
secondindicator = indicator_info[3]
# add the trigger to its argument
sell_triggers += f"if params['sell-trigger'] == 'sell-{indicator}': conditions.append(dataframe['{indicator}'] {aim} dataframe['close'])"
sell_triggers += f"if params['sell-trigger'] == 'sell-{indicator}':\n conditions.append(dataframe['{indicator}'] {aim} dataframe['{secondindicator}'])\n"
# Final line of indicator space makes all triggers
sell_space += "Categorical(["
# Adding all triggers to the list
for indicator in sell_indicators:
if indicator in POSSIBLE_TRIGGERS:
# adding all triggers to the list
for indicator_info in sell_indicators:
indicator = indicator_info[0]
usage = indicator_info[2]
if usage == "trigger":
sell_space += f"'sell-{indicator}', "
# Deleting the last ", "
sell_space = sell_space[:-2]
sell_space += "], name='trigger')"
sell_space += "], name='sell-trigger')"
return {"sell_guards": sell_guards, "sell_triggers": sell_triggers, "sell_space": sell_space}
@ -130,11 +261,11 @@ def deploy_custom_hyperopt(hyperopt_name: str, hyperopt_path: Path, buy_indicato
"""
# Build the arguments for the buy and sell generators
buy_args = build_hyperopt_buyelements(buy_indicators)
sell_args = build_hyperopt_sellelements(sell_indicators)
buy_args = custom_hyperopt_buyelements(buy_indicators)
sell_args = custom_hyperopt_sellelements(sell_indicators)
# Build the final template
strategy_text = render_template(templatefile='base_hyperopt.py.j2',
strategy_text = render_template(templatefile='base_custom_hyperopt.py.j2',
arguments={"hyperopt": hyperopt_name,
"buy_guards": buy_args["buy_guards"],
"buy_triggers": buy_args["buy_triggers"],
@ -148,19 +279,20 @@ def deploy_custom_hyperopt(hyperopt_name: str, hyperopt_path: Path, buy_indicato
hyperopt_path.write_text(strategy_text)
def start_build_hyperopt(args: Dict[str, Any]) -> None:
def start_custom_hyperopt(args: Dict[str, Any]) -> None:
"""
Check if the right subcommands where passed and start building the hyperopt
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
# check what the name of the hyperopt should be
if not 'hyperopt' in args or not args['hyperopt']:
raise OperationalException("`build-hyperopt` requires --hyperopt to be set.")
raise OperationalException("`custom-hyperopt` requires --hyperopt to be set.")
elif not 'buy_indicators' in args or not args['buy_indicators']:
raise OperationalException("`build-hyperopt` requires --buy-indicators to be set.")
raise OperationalException("`custom-hyperopt` requires --buy-indicators to be set.")
elif not 'sell_indicators' in args or not args['sell_indicators']:
raise OperationalException("`build-hyperopt` requires --sell-indicators to be set.")
raise OperationalException("`custom-hyperopt` requires --sell-indicators to be set.")
else:
if args['hyperopt'] == 'DefaultHyperopt':
raise OperationalException("DefaultHyperopt is not allowed as name.")
@ -175,3 +307,40 @@ def start_build_hyperopt(args: Dict[str, Any]) -> None:
deploy_custom_hyperopt(args['hyperopt'], new_path,
buy_indicators, sell_indicators)
# --------------------------------------------------build-hyperopt------------------------------------------------------
def start_build_hyperopt(args: Dict[str, Any]) -> None:
"""
Check if the right subcommands where passed and start building the hyperopt
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
# strategy and hyperopt need to be defined
if not 'strategy' in args or not args['strategy']:
raise OperationalException("`build-hyperopt` requires --strategy to be set.")
if not 'hyperopt' in args or not args['hyperopt']:
args['hyperopt'] = args['strategy'] + "opt"
else:
if args['hyperopt'] == 'DefaultHyperopt':
raise OperationalException("DefaultHyperopt is not allowed as name.")
# the path of the chosen strategy
strategy_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py')
# the path where the hyperopt should be written
new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py')
if new_path.exists():
raise OperationalException(f"`{new_path}` already exists. "
"Please choose another Hyperopt Name.")
# extract the buy and sell indicators as dicts
extracted_lists = extract_lists(strategy_path)
buy_indicators = extracted_lists[0]
sell_indicators = extracted_lists[1]
# use the dicts to write the hyperopt
deploy_custom_hyperopt(args['hyperopt'], new_path,
buy_indicators, sell_indicators)

View File

@ -279,14 +279,21 @@ AVAILABLE_CLI_OPTIONS = {
"buy_indicators": Arg(
'-b', '--buy-indicators',
help='Specify the buy indicators the hyperopt should build. '
'Example: --buy-indicators `{"rsi":"above","bb_lowerband":"below"}`',
metavar='DICT',
'Example: --buy-indicators `[["rsi","<","trigger",30.0],["bb_lowerband",">","guard","close"]]`'
'Check the documentation for specific requirements for the lists.',
metavar='LIST',
),
"sell_indicators": Arg(
'-s', '--sell-indicators',
help='Specify the sell indicators the hyperopt should build. '
'Example: --sell-indicators `{"rsi":"above","bb_lowerband":"below"}`',
metavar='DICT',
'Example: --sell-indicators [["rsi",">","trigger",70.0],["bb_lowerband","<","guard","close"]]'
'Check the documentation for specific requirements for the lists.',
metavar='LIST',
),
"extract_name": Arg(
'--extract-name',
help='Specify the name of the file to which the data should be extracted. ',
metavar='FILENAME',
),
# List exchanges
"print_one_column": Arg(

View File

@ -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,
@ -382,4 +392,4 @@ PairWithTimeframe = Tuple[str, str]
ListPairsWithTimeframes = List[PairWithTimeframe]
# Type for trades list
TradeList = List[List]
TradeList = List[List]

View File

@ -383,3 +383,21 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date'
high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col]
low_date = profit_results.loc[idxmin, date_col]
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date
def calculate_csum(trades: pd.DataFrame) -> Tuple[float, float]:
"""
Calculate min/max cumsum of trades, to show if the wallet/stake amount ratio is sane
:param trades: DataFrame containing trades (requires columns close_date and profit_percent)
:return: Tuple (float, float) with cumsum of profit_abs
:raise: ValueError if trade-dataframe was found empty.
"""
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
csum_df = pd.DataFrame()
csum_df['sum'] = trades['profit_abs'].cumsum()
csum_min = csum_df['sum'].min()
csum_max = csum_df['sum'].max()
return csum_min, csum_max

View File

@ -104,6 +104,7 @@ class Edge:
exchange=self.exchange,
timeframe=self.strategy.timeframe,
timerange=self._timerange,
data_format=self.config.get('dataformat_ohlcv', 'json'),
)
data = load_data(
@ -159,7 +160,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,'

View File

@ -19,5 +19,11 @@ class Bittrex(Exchange):
"""
_ft_has: Dict = {
"ohlcv_candle_limit_per_timeframe": {
'1m': 1440,
'5m': 288,
'1h': 744,
'1d': 365,
},
"l2_limit_range": [1, 25, 500],
}

View File

@ -3,6 +3,7 @@
Cryptocurrency Exchanges support
"""
import asyncio
import http
import inspect
import logging
from copy import deepcopy
@ -34,6 +35,12 @@ CcxtModuleType = Any
logger = logging.getLogger(__name__)
# Workaround for adding samesite support to pre 3.8 python
# Only applies to python3.7, and only on certain exchanges (kraken)
# Replicates the fix from starlette (which is actually causing this problem)
http.cookies.Morsel._reserved["samesite"] = "SameSite" # type: ignore
class Exchange:
_config: Dict = {}
@ -94,7 +101,6 @@ class Exchange:
logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has)
# Assign this directly for easy access
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
self._trades_pagination = self._ft_has['trades_pagination']
@ -130,7 +136,8 @@ class Exchange:
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0))
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
config.get('timeframe', ''))
# Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get(
@ -191,11 +198,6 @@ class Exchange:
def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys())
@property
def ohlcv_candle_limit(self) -> int:
"""exchange ohlcv candle limit"""
return int(self._ohlcv_candle_limit)
@property
def markets(self) -> Dict:
"""exchange ccxt markets"""
@ -209,6 +211,17 @@ class Exchange:
"""exchange ccxt precisionMode"""
return self._api.precisionMode
def ohlcv_candle_limit(self, timeframe: str) -> int:
"""
Exchange ohlcv candle limit
Uses ohlcv_candle_limit_per_timeframe if the exchange has different limts
per timeframe (e.g. bittrex), otherwise falls back to ohlcv_candle_limit
:param timeframe: Timeframe to check
:return: Candle limit as integer
"""
return int(self._ft_has.get('ohlcv_candle_limit_per_timeframe', {}).get(
timeframe, self._ft_has.get('ohlcv_candle_limit')))
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict[str, Any]:
"""
@ -421,15 +434,16 @@ class Exchange:
raise OperationalException(
f'Time in force policies are not supported for {self.name} yet.')
def validate_required_startup_candles(self, startup_candles: int) -> None:
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
"""
Checks if required startup_candles is more than ohlcv_candle_limit.
Checks if required startup_candles is more than ohlcv_candle_limit().
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
"""
if startup_candles + 5 > self._ft_has['ohlcv_candle_limit']:
candle_limit = self.ohlcv_candle_limit(timeframe)
if startup_candles + 5 > candle_limit:
raise OperationalException(
f"This strategy requires {startup_candles} candles to start. "
f"{self.name} only provides {self._ft_has['ohlcv_candle_limit']}.")
f"{self.name} only provides {candle_limit} for {timeframe}.")
def exchange_has(self, endpoint: str) -> bool:
"""
@ -714,7 +728,7 @@ class Exchange:
"""
Get candle history using asyncio and returns the list of candles.
Handles all async work for this.
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
:param pair: Pair to download
:param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from
@ -744,7 +758,7 @@ class Exchange:
Download historic ohlcv
"""
one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
logger.debug(
"one_call: %s msecs (%s)",
one_call,
@ -846,7 +860,7 @@ class Exchange:
data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe,
since=since_ms,
limit=self._ohlcv_candle_limit)
limit=self.ohlcv_candle_limit(timeframe))
# Some exchanges sort OHLCV in ASC order and others in DESC.
# Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last)
@ -1019,7 +1033,7 @@ class Exchange:
"""
Get trade history data using asyncio.
Handles all async work and returns the list of candles.
Async over one pair, assuming we get `self._ohlcv_candle_limit` candles per call.
Async over one pair, assuming we get `self.ohlcv_candle_limit()` candles per call.
:param pair: Pair to download
:param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.

View File

@ -179,6 +179,7 @@ class FreqtradeBot(LoggingMixin):
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
# while selling is in process, since telegram messages arrive in an different thread.
with self._sell_lock:
trades = Trade.get_open_trades()
# First process current opened trades (positions)
self.exit_positions(trades)
@ -1183,6 +1184,7 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj)
trade.open_order_id = order['id']
trade.sell_order_status = ''
trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value
# In case of market sell orders the order can be closed immediately

View File

@ -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

View File

@ -546,10 +546,11 @@ class Hyperopt:
)
return self._get_results_dict(backtesting_results, min_date, max_date,
params_dict, params_details)
params_dict, params_details,
processed=processed)
def _get_results_dict(self, backtesting_results, min_date, max_date,
params_dict, params_details):
params_dict, params_details, processed: Dict[str, DataFrame]):
results_metrics = self._calculate_results_metrics(backtesting_results)
results_explanation = self._format_results_explanation_string(results_metrics)
@ -563,7 +564,8 @@ class Hyperopt:
loss: float = MAX_LOSS
if trade_count >= self.config['hyperopt_min_trades']:
loss = self.calculate_loss(results=backtesting_results, trade_count=trade_count,
min_date=min_date.datetime, max_date=max_date.datetime)
min_date=min_date.datetime, max_date=max_date.datetime,
config=self.config, processed=processed)
return {
'loss': loss,
'params_dict': params_dict,

View File

@ -5,6 +5,7 @@ This module defines the interface for the loss-function for hyperopt
from abc import ABC, abstractmethod
from datetime import datetime
from typing import Dict
from pandas import DataFrame
@ -19,7 +20,9 @@ class IHyperOptLoss(ABC):
@staticmethod
@abstractmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime, *args, **kwargs) -> float:
min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
"""

View File

@ -9,8 +9,9 @@ from pandas import DataFrame
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.data.btanalysis import (calculate_csum, calculate_market_change,
calculate_max_drawdown)
from freqtrade.misc import decimals_per_coin, file_dump_json, round_coin_value
logger = logging.getLogger(__name__)
@ -38,11 +39,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]:
@ -323,6 +325,13 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'drawdown_end': drawdown_end,
'drawdown_end_ts': drawdown_end.timestamp() * 1000,
})
csum_min, csum_max = calculate_csum(results)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
})
except ValueError:
strat_stats.update({
'max_drawdown': 0.0,
@ -330,6 +339,8 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame],
'drawdown_start_ts': 0,
'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc),
'drawdown_end_ts': 0,
'csum_min': 0,
'csum_max': 0
})
strategy_results = generate_strategy_metrics(all_results=all_results)
@ -352,7 +363,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 +394,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 +409,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 = [[
@ -436,6 +449,12 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('', ''), # Empty line to improve readability
('Abs Profit Min', round_coin_value(strat_results['csum_min'],
strat_results['stake_currency'])),
('Abs Profit Max', round_coin_value(strat_results['csum_max'],
strat_results['stake_currency'])),
('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)),
('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)),

View File

@ -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]

View File

@ -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,

View File

@ -30,10 +30,10 @@ class AgeFilter(IPairList):
if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed to be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit:
if self._min_days_listed > exchange.ohlcv_candle_limit('1d'):
raise OperationalException("AgeFilter requires min_days_listed to not exceed "
"exchange max request size "
f"({exchange.ohlcv_candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property
def needstickers(self) -> bool:

View File

@ -32,10 +32,10 @@ class RangeStabilityFilter(IPairList):
if self._days < 1:
raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
if self._days > exchange.ohlcv_candle_limit:
if self._days > exchange.ohlcv_candle_limit('1d'):
raise OperationalException("RangeStabilityFilter requires lookback_days to not "
"exceed exchange max request size "
f"({exchange.ohlcv_candle_limit})")
f"({exchange.ohlcv_candle_limit('1d')})")
@property
def needstickers(self) -> bool:

View File

@ -58,13 +58,13 @@ class StoplossGuard(IProtection):
SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit < 0)]
if len(trades) > self._trade_limit:
self.log_once(f"Trading stopped due to {self._trade_limit} "
f"stoplosses within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason()
if len(trades) < self._trade_limit:
return False, None, None
return False, None, None
self.log_once(f"Trading stopped due to {self._trade_limit} "
f"stoplosses within {self._lookback_period} minutes.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration)
return True, until, self._reason()
def global_stop(self, date_now: datetime) -> ProtectionReturn:
"""

View File

@ -113,7 +113,7 @@ class Daily(BaseModel):
class ShowConfig(BaseModel):
dry_run: str
dry_run: bool
stake_currency: str
stake_amount: Union[float, str]
max_open_trades: int

View File

@ -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)

View File

@ -2,6 +2,7 @@ import logging
from ipaddress import IPv4Address
from typing import Any, Dict
import rapidjson
import uvicorn
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
@ -14,6 +15,17 @@ from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__)
class FTJSONResponse(JSONResponse):
media_type = "application/json"
def render(self, content: Any) -> bytes:
"""
Use rapidjson for responses
Handles NaN and Inf / -Inf in a javascript way by default.
"""
return rapidjson.dumps(content).encode("utf-8")
class ApiServer(RPCHandler):
_rpc: RPC
@ -32,6 +44,7 @@ class ApiServer(RPCHandler):
self.app = FastAPI(title="Freqtrade API",
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
redoc_url=None,
default_response_class=FTJSONResponse,
)
self.configure_app(self.app, self._config)

View File

@ -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
@ -747,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 = {
@ -775,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)

View File

@ -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
@ -189,14 +190,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:
@ -365,7 +366,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=[
@ -415,18 +416,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`"
@ -494,15 +495,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:
@ -512,8 +515,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")
self._send_msg(output)
except RPCException as e:
self._send_msg(str(e))

View File

@ -0,0 +1,164 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# --- Do not remove these libs ---
from functools import reduce
from typing import Any, Callable, Dict, List
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer, Real # noqa
from freqtrade.optimize.hyperopt_interface import IHyperOpt
# --------------------------------
# Add your lib to import here
import talib.abstract as ta # noqa
import freqtrade.vendor.qtpylib.indicators as qtpylib
class {{ hyperopt }}(IHyperOpt):
"""
This is a Hyperopt template to get you started.
More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/
You should:
- Add any lib you need to build your hyperopt.
You must keep:
- The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator.
The methods roi_space, generate_roi_table and stoploss_space are not required
and are provided by default.
However, you may override them if you need 'roi' and 'stoploss' spaces that
differ from the defaults offered by Freqtrade.
Sample implementation of these methods will be copied to `user_data/hyperopts` when
creating the user-data directory using `freqtrade create-userdir --userdir user_data`,
or is available online under the following URL:
https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py.
"""
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
{{ buy_guards | indent(12) }}
# TRIGGERS
if 'trigger' in params:
{{ buy_triggers | indent(16) }}
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
{{ buy_space | indent(12) }}
]
@staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the sell strategy parameters to be used by Hyperopt.
"""
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Sell strategy Hyperopt will build and use.
"""
conditions = []
# GUARDS AND TRENDS
{{ sell_guards | indent(12) }}
# TRIGGERS
if 'sell-trigger' in params:
{{ sell_triggers | indent(16) }}
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'sell'] = 1
return dataframe
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
{{ sell_space | indent(12) }}
]
@ staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]:
"""
Generate the ROI table that will be used by Hyperopt
This implementation generates the default legacy Freqtrade ROI tables.
Change it if you need different number of steps in the generated
ROI tables or other structure of the ROI tables.
Please keep it aligned with parameters in the 'roi' optimization
hyperspace defined by the roi_space method.
"""
roi_table = {}
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2']
roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1']
roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0
return roi_table
@ staticmethod
def roi_space() -> List[Dimension]:
"""
Values to search for each ROI steps
Override it if you need some different ranges for the parameters in the
'roi' optimization hyperspace.
Please keep it aligned with the implementation of the
generate_roi_table method.
"""
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]
@ staticmethod
def stoploss_space() -> List[Dimension]:
"""
Stoploss Value to search
Override it if you need some different range for the parameter in the
'stoploss' optimization hyperspace.
"""
return [
Real(-0.35, -0.02, name='stoploss'),
]

View File

@ -55,7 +55,16 @@ class {{ hyperopt }}(IHyperOpt):
# TRIGGERS
if 'trigger' in params:
{{ buy_triggers | indent(16) }}
if params['trigger'] == 'bb_lower':
conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['close'], dataframe['sar']
))
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
@ -94,7 +103,16 @@ class {{ hyperopt }}(IHyperOpt):
# TRIGGERS
if 'sell-trigger' in params:
{{ sell_triggers | indent(16) }}
if params['sell-trigger'] == 'sell-bb_upper':
conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['sell-trigger'] == 'sell-macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
))
if params['sell-trigger'] == 'sell-sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['sar'], dataframe['close']
))
# Check that the candle had volume
conditions.append(dataframe['volume'] > 0)
@ -115,4 +133,4 @@ class {{ hyperopt }}(IHyperOpt):
"""
return [
{{ sell_space | indent(12) }}
]
]

View File

@ -1,5 +1,6 @@
from datetime import datetime
from math import exp
from typing import Dict
from pandas import DataFrame
@ -35,6 +36,7 @@ class SampleHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results

View File

@ -40,7 +40,7 @@
"# Location of the data\n",
"data_location = Path(config['user_data_dir'], 'data', 'binance')\n",
"# Pair to analyze - Only use one pair here\n",
"pair = \"BTC_USDT\""
"pair = \"BTC/USDT\""
]
},
{
@ -54,7 +54,9 @@
"\n",
"candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
" pair=pair)\n",
" pair=pair,\n",
" data_format = \"hdf5\",\n",
" )\n",
"\n",
"# Confirm success\n",
"print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n",

View File

@ -2,9 +2,9 @@
-r requirements.txt
# Required for hyperopt
scipy==1.6.0
scipy==1.6.1
scikit-learn==0.24.1
scikit-optimize==0.8.1
filelock==3.0.12
joblib==1.0.0
joblib==1.0.1
progressbar2==3.53.1

View File

@ -1,10 +1,12 @@
numpy==1.20.0
pandas==1.2.1
numpy==1.20.1
pandas==1.2.2
ccxt==1.41.62
ccxt==1.42.19
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.6
aiohttp==3.7.3
SQLAlchemy==1.3.22
python-telegram-bot==13.1
SQLAlchemy==1.3.23
python-telegram-bot==13.3
arrow==0.17.0
cachetools==4.2.1
requests==2.25.1
@ -12,14 +14,14 @@ urllib3==1.26.3
wrapt==1.12.1
jsonschema==3.2.0
TA-Lib==0.4.19
tabulate==0.8.7
tabulate==0.8.9
pycoingecko==1.4.0
jinja2==2.11.3
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
@ -29,7 +31,7 @@ sdnotify==0.3.2
# API Server
fastapi==0.63.0
uvicorn==0.13.3
uvicorn==0.13.4
pyjwt==2.0.1
aiofiles==0.6.0
@ -37,4 +39,4 @@ aiofiles==0.6.0
colorama==0.4.4
# Building config files interactively
questionary==1.9.0
prompt-toolkit==3.0.14
prompt-toolkit==3.0.16

View File

@ -19,7 +19,7 @@ if readme_file.is_file():
readme_long = (Path(__file__).parent / "README.md").read_text()
# Requirements used for submodules
api = ['flask', 'flask-jwt-extended', 'flask-cors']
api = ['fastapi', 'uvicorn', 'pyjwt', 'aiofiles']
plot = ['plotly>=4.0']
hyperopt = [
'scipy',

View File

@ -8,11 +8,12 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime
from freqtrade.configuration import TimeRange
from freqtrade.constants import LAST_BT_RESULT_FN
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_DATA_COLUMNS_OLD,
analyze_trade_parallelism, calculate_market_change,
calculate_max_drawdown, combine_dataframes_with_mean,
create_cum_profit, extract_trades_of_period,
get_latest_backtest_filename, get_latest_hyperopt_file,
load_backtest_data, load_trades, load_trades_from_db)
analyze_trade_parallelism, calculate_csum,
calculate_market_change, calculate_max_drawdown,
combine_dataframes_with_mean, create_cum_profit,
extract_trades_of_period, get_latest_backtest_filename,
get_latest_hyperopt_file, load_backtest_data, load_trades,
load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history
from tests.conftest import create_mock_trades
from tests.conftest_trades import MOCK_TRADE_COUNT
@ -284,6 +285,20 @@ def test_calculate_max_drawdown(testdatadir):
drawdown, h, low = calculate_max_drawdown(DataFrame())
def test_calculate_csum(testdatadir):
filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
csum_min, csum_max = calculate_csum(bt_data)
assert isinstance(csum_min, float)
assert isinstance(csum_max, float)
assert csum_min < 0.01
assert csum_max > 0.02
with pytest.raises(ValueError, match='Trade dataframe empty.'):
csum_min, csum_max = calculate_csum(DataFrame())
def test_calculate_max_drawdown2():
values = [0.011580, 0.010048, 0.011340, 0.012161, 0.010416, 0.010009, 0.020024,
-0.024662, -0.022350, 0.020496, -0.029859, -0.030511, 0.010041, 0.010872,

View File

@ -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):

View File

@ -5,10 +5,12 @@ However, these tests should give a good idea to determine if a new exchange is
suitable to run with freqtrade.
"""
from datetime import datetime, timedelta, timezone
from pathlib import Path
import pytest
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
from tests.conftest import get_default_conf
@ -122,7 +124,10 @@ class TestCCXTExchange():
assert len(ohlcv[pair_tf]) == len(exchange.klines(pair_tf))
# assert len(exchange.klines(pair_tf)) > 200
# Assume 90% uptime ...
assert len(exchange.klines(pair_tf)) > exchange._ohlcv_candle_limit * 0.90
assert len(exchange.klines(pair_tf)) > exchange.ohlcv_candle_limit(timeframe) * 0.90
# Check if last-timeframe is within the last 2 intervals
now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2))
assert exchange.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now)
# TODO: tests fetch_trades (?)

View File

@ -1417,7 +1417,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
ret = exchange.get_historic_ohlcv(pair, "5m", int((
arrow.utcnow().int_timestamp - since) * 1000))
@ -1473,7 +1473,7 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name):
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
# one_call calculation * 1.8 should do 2 calls
since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
since = 5 * 60 * exchange.ohlcv_candle_limit('5m') * 1.8
ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int((
arrow.utcnow().int_timestamp - since) * 1000))
@ -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"))
@ -2418,6 +2418,19 @@ def test_get_markets_error(default_conf, mocker):
ex.get_markets('LTC', 'USDT', True, False)
@pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_ohlcv_candle_limit(default_conf, mocker, exchange_name):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
timeframes = ('1m', '5m', '1h')
expected = exchange._ft_has['ohlcv_candle_limit']
for timeframe in timeframes:
if 'ohlcv_candle_limit_per_timeframe' in exchange._ft_has:
expected = exchange._ft_has['ohlcv_candle_limit_per_timeframe'][timeframe]
# This should only run for bittrex
assert exchange_name == 'bittrex'
assert exchange.ohlcv_candle_limit(timeframe) == expected
def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10
@ -2462,6 +2475,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():

View File

@ -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:

View File

@ -83,7 +83,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog):
"method": "StoplossGuard",
"lookback_period": 60,
"stop_duration": 40,
"trade_limit": 2
"trade_limit": 3
}]
freqtrade = get_patched_freqtradebot(mocker, default_conf)
message = r"Trading stopped due to .*"
@ -136,7 +136,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair
default_conf['protections'] = [{
"method": "StoplossGuard",
"lookback_period": 60,
"trade_limit": 1,
"trade_limit": 2,
"stop_duration": 60,
"only_per_pair": only_per_pair
}]

View File

@ -11,9 +11,11 @@ import uvicorn
from fastapi import FastAPI
from fastapi.exceptions import HTTPException
from fastapi.testclient import TestClient
from numpy import isnan
from requests.auth import _basic_auth_str
from freqtrade.__init__ import __version__
from freqtrade.exceptions import ExchangeError
from freqtrade.loggers import setup_logging, setup_logging_pre
from freqtrade.persistence import PairLocks, Trade
from freqtrade.rpc import RPC
@ -295,7 +297,7 @@ def test_api_run(default_conf, mocker, caplog):
"Please make sure that this is intentional!", caplog)
assert log_has_re("SECURITY WARNING - `jwt_secret_key` seems to be default.*", caplog)
# Test crashing flask
# Test crashing API server
caplog.clear()
mocker.patch('freqtrade.rpc.api_server.webserver.UvicornServer',
MagicMock(side_effect=Exception))
@ -789,6 +791,15 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'exchange': 'bittrex',
}]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc)
resp_values = rc.json()
assert len(resp_values) == 1
assert isnan(resp_values[0]['profit_abs'])
def test_api_version(botclient):
ftbot, client = botclient

View File

@ -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:

View File

@ -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()

View File

@ -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:

View File

@ -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):