Merge branch 'develop' into bt_add_maxdrawdown

This commit is contained in:
Matthias
2020-08-09 08:34:36 +02:00
63 changed files with 1535 additions and 310 deletions

View File

@@ -9,7 +9,8 @@ Note: Be careful with file-scoped imports in these subfiles.
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_download_data,
start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir,
start_new_hyperopt,
start_new_strategy)

View File

@@ -54,6 +54,8 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"]
@@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies",
"list-markets", "list-pairs", "list-strategies", "list-data",
"list-hyperopts", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "plot-profit", "show-trades"]
@@ -159,7 +161,7 @@ class Arguments:
self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_create_userdir, start_convert_data,
start_download_data,
start_download_data, start_list_data,
start_hyperopt_list, start_hyperopt_show,
start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies,
@@ -233,6 +235,15 @@ class Arguments:
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
# Add list-data subcommand
list_data_cmd = subparsers.add_parser(
'list-data',
help='List downloaded data.',
parents=[_common_parser],
)
list_data_cmd.set_defaults(func=start_list_data)
self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd)
# Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
parents=[_common_parser, _strategy_parser])

View File

@@ -1,5 +1,6 @@
import logging
import sys
from collections import defaultdict
from typing import Any, Dict, List
import arrow
@@ -11,6 +12,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv,
refresh_backtest_ohlcv_data,
refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode
@@ -88,3 +90,30 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
convert_trades_format(config,
convert_from=args['format_from'], convert_to=args['format_to'],
erase=args['erase'])
def start_list_data(args: Dict[str, Any]) -> None:
"""
List available backtest data
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.data.history.idatahandler import get_datahandler
from tabulate import tabulate
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
paircombs = dhc.ohlcv_get_available_data(config['datadir'])
if args['pairs']:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']]
print(f"Found {len(paircombs)} pair / timeframe combinations.")
groupedpair = defaultdict(list)
for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))):
groupedpair[pair].append(timeframe)
if groupedpair:
print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()],
headers=("Pair", "Timeframe"),
tablefmt='psql', stralign='right'))

View File

@@ -159,7 +159,9 @@ CONF_SCHEMA = {
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'}
'stoploss_on_exchange_interval': {'type': 'number'},
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
'maximum': 1.0}
},
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
},
@@ -342,4 +344,5 @@ CANCEL_REASON = {
}
# List of pairs with their timeframes
ListPairsWithTimeframes = List[Tuple[str, str]]
PairWithTimeframe = Tuple[str, str]
ListPairsWithTimeframes = List[PairWithTimeframe]

View File

@@ -5,16 +5,17 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data.
"""
import logging
from typing import Any, Dict, List, Optional
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from arrow import Arrow
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange
from freqtrade.state import RunMode
from freqtrade.constants import ListPairsWithTimeframes
logger = logging.getLogger(__name__)
@@ -25,6 +26,18 @@ class DataProvider:
self._config = config
self._exchange = exchange
self._pairlists = pairlists
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None:
"""
Store cached Dataframe.
Using private method as this should never be used by a user
(but the class is exposed via `self.dp` to the strategy)
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param dataframe: analyzed dataframe
"""
self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime)
def refresh(self,
pairlist: ListPairsWithTimeframes,
@@ -89,6 +102,20 @@ class DataProvider:
logger.warning(f"No data found for ({pair}, {timeframe}).")
return data
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
"""
:param pair: pair to get the data for
:param timeframe: timeframe to get data for
:return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe
combination.
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
"""
if (pair, timeframe) in self.__cached_pairs:
return self.__cached_pairs[(pair, timeframe)]
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
def market(self, pair: str) -> Optional[Dict[str, Any]]:
"""
Return market data for the pair

View File

@@ -13,6 +13,7 @@ from typing import List, Optional, Type
from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.converter import (clean_ohlcv_dataframe,
trades_remove_duplicates, trim_dataframe)
from freqtrade.exchange import timeframe_to_seconds
@@ -28,6 +29,14 @@ class IDataHandler(ABC):
def __init__(self, datadir: Path) -> None:
self._datadir = datadir
@abstractclassmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:return: List of Tuples of (pair, timeframe)
"""
@abstractclassmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
"""

View File

@@ -8,7 +8,8 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
ListPairsWithTimeframes)
from freqtrade.data.converter import trades_dict_to_list
from .idatahandler import IDataHandler, TradeList
@@ -21,6 +22,18 @@ class JsonDataHandler(IDataHandler):
_use_zip = False
_columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:return: List of Tuples of (pair, timeframe)
"""
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name)
for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
if match and len(match.groups()) > 1]
@classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
"""

View File

@@ -81,7 +81,7 @@ class Binance(Exchange):
return order
except ccxt.InsufficientFunds as e:
raise ExchangeError(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:

View File

@@ -187,6 +187,11 @@ 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"""
@@ -253,8 +258,8 @@ class Exchange:
api.urls['api'] = api.urls['test']
logger.info("Enabled Sandbox API on %s", name)
else:
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
"Please check your config.json")
logger.warning(
f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
def _load_async_markets(self, reload: bool = False) -> None:
@@ -520,13 +525,13 @@ class Exchange:
except ccxt.InsufficientFunds as e:
raise ExchangeError(
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise ExchangeError(
f'Could not create {ordertype} {side} order on market {pair}.'
f'Tried to {side} amount {amount} at rate {rate}.'
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e

View File

@@ -89,7 +89,7 @@ class Kraken(Exchange):
return order
except ccxt.InsufficientFunds as e:
raise ExchangeError(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:

View File

@@ -153,6 +153,10 @@ class FreqtradeBot:
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
self.strategy.informative_pairs())
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
self.strategy.analyze(self.active_pair_whitelist)
with self._sell_lock:
# Check and handle any timed out open orders
self.check_handle_timedout()
@@ -440,9 +444,8 @@ class FreqtradeBot:
return False
# running get_signal on historical data fetched
(buy, sell) = self.strategy.get_signal(
pair, self.strategy.timeframe,
self.dataprovider.ohlcv(pair, self.strategy.timeframe))
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe)
(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)
@@ -515,6 +518,12 @@ class FreqtradeBot:
amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy']
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force):
logger.info(f"User requested abortion of buying {pair}")
return False
order = self.exchange.buy(pair=pair, ordertype=order_type,
amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force)
@@ -589,6 +598,7 @@ class FreqtradeBot:
Sends rpc notification when a buy occured.
"""
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@@ -612,6 +622,7 @@ class FreqtradeBot:
current_rate = self.get_buy_rate(trade.pair, False)
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
@@ -717,9 +728,10 @@ class FreqtradeBot:
if (config_ask_strategy.get('use_sell_signal', True) or
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
(buy, sell) = self.strategy.get_signal(
trade.pair, self.strategy.timeframe,
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe)
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
if config_ask_strategy.get('use_order_book', False):
order_book_min = config_ask_strategy.get('order_book_min', 1)
@@ -815,10 +827,8 @@ class FreqtradeBot:
return False
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
if (not stoploss_order):
if not stoploss_order:
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
@@ -1097,12 +1107,20 @@ class FreqtradeBot:
order_type = self.strategy.order_types.get("emergencysell", "market")
amount = self._safe_sell_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force,
sell_reason=sell_reason.value):
logger.info(f"User requested abortion of selling {trade.pair}")
return False
# Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair),
ordertype=order_type,
amount=amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell']
time_in_force=time_in_force
)
trade.open_order_id = order['id']
@@ -1133,6 +1151,7 @@ class FreqtradeBot:
msg = {
'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
'pair': trade.pair,
'gain': gain,
@@ -1175,6 +1194,7 @@ class FreqtradeBot:
msg = {
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
'pair': trade.pair,
'gain': gain,

View File

@@ -103,7 +103,7 @@ class Backtesting:
if len(self.pairlists.whitelist) == 0:
raise OperationalException("No pair in whitelist.")
if config.get('fee'):
if config.get('fee', None) is not None:
self.fee = config['fee']
else:
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])

View File

@@ -5,6 +5,7 @@ import logging
import arrow
from typing import Any, Dict
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural
from freqtrade.pairlist.IPairList import IPairList
@@ -23,6 +24,13 @@ class AgeFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed must be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit:
raise OperationalException("AgeFilter requires min_days_listed must not exceed "
"exchange max request size "
f"({exchange.ohlcv_candle_limit})")
self._enabled = self._min_days_listed >= 1
@property
@@ -69,7 +77,7 @@ class AgeFilter(IPairList):
return True
else:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because age is less than "
f"because age {len(daily_candles)} is less than "
f"{self._min_days_listed} "
f"{plural(self._min_days_listed, 'day')}")
return False

View File

@@ -18,7 +18,11 @@ class PriceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
self._enabled = self._low_price_ratio != 0
self._min_price = pairlistconfig.get('min_price', 0)
self._max_price = pairlistconfig.get('max_price', 0)
self._enabled = ((self._low_price_ratio != 0) or
(self._min_price != 0) or
(self._max_price != 0))
@property
def needstickers(self) -> bool:
@@ -33,7 +37,18 @@ class PriceFilter(IPairList):
"""
Short whitelist method description - used for startup-messages
"""
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
active_price_filters = []
if self._low_price_ratio != 0:
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
if self._min_price != 0:
active_price_filters.append(f"below {self._min_price:.8f}")
if self._max_price != 0:
active_price_filters.append(f"above {self._max_price:.8f}")
if len(active_price_filters):
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
return f"{self.name} - No price filters configured."
def _validate_pair(self, ticker) -> bool:
"""
@@ -41,15 +56,33 @@ class PriceFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed
"""
if ticker['last'] is None:
if ticker['last'] is None or ticker['last'] == 0:
self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).")
return False
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
changeperc = compare / ticker['last']
if changeperc > self._low_price_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%")
return False
# Perform low_price_ratio check.
if self._low_price_ratio != 0:
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
changeperc = compare / ticker['last']
if changeperc > self._low_price_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%")
return False
# Perform min_price check.
if self._min_price != 0:
if ticker['last'] < self._min_price:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because last price < {self._min_price:.8f}")
return False
# Perform max_price check.
if self._max_price != 0:
if ticker['last'] > self._max_price:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because last price > {self._max_price:.8f}")
return False
return True

View File

@@ -11,11 +11,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
extract_trades_of_period,
load_trades)
from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import StrategyResolver
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__)
@@ -472,6 +474,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
"""
strategy = StrategyResolver.load_strategy(config)
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
plot_elements = init_plotscript(config)
trades = plot_elements['trades']
pair_counter = 0

View File

@@ -42,14 +42,14 @@ class HyperOptResolver(IResolver):
extra_dir=config.get('hyperopt_path'))
if not hasattr(hyperopt, 'populate_indicators'):
logger.warning("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.")
logger.info("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.")
if not hasattr(hyperopt, 'populate_buy_trend'):
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.")
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.")
if not hasattr(hyperopt, 'populate_sell_trend'):
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.")
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.")
return hyperopt

View File

@@ -18,6 +18,7 @@ from werkzeug.serving import make_server
from freqtrade.__init__ import __version__
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
logger = logging.getLogger(__name__)
@@ -56,7 +57,7 @@ def require_login(func: Callable[[Any, Any], Any]):
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
def rpc_catch_errors(func: Callable[[Any], Any]):
def rpc_catch_errors(func: Callable[..., Any]):
def func_wrapper(obj, *args, **kwargs):
@@ -106,6 +107,9 @@ class ApiServer(RPC):
# Register application handling
self.register_rest_rpc_urls()
if self._config.get('fiat_display_currency', None):
self._fiat_converter = CryptoToFiatConverter()
thread = threading.Thread(target=self.run, daemon=True)
thread.start()
@@ -197,6 +201,8 @@ class ApiServer(RPC):
view_func=self._ping, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
view_func=self._trades, methods=['GET'])
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
view_func=self._trades_delete, methods=['DELETE'])
# Combined actions and infos
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
methods=['GET', 'POST'])
@@ -421,6 +427,19 @@ class ApiServer(RPC):
results = self._rpc_trade_history(limit)
return self.rest_dump(results)
@require_login
@rpc_catch_errors
def _trades_delete(self, tradeid):
"""
Handler for DELETE /trades/<tradeid> endpoint.
Removes the trade from the database (tries to cancel open orders first!)
get:
param:
tradeid: Numeric trade-id assigned to the trade.
"""
result = self._rpc_delete(tradeid)
return self.rest_dump(result)
@require_login
@rpc_catch_errors
def _whitelist(self):

View File

@@ -6,12 +6,14 @@ from abc import abstractmethod
from datetime import date, datetime, timedelta
from enum import Enum
from math import isnan
from typing import Any, Dict, List, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
from numpy import NAN, mean
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exceptions import (ExchangeError, InvalidOrderException,
PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@@ -103,6 +105,8 @@ class RPC:
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'ticker_interval': config['timeframe'], # DEPRECATED
'timeframe': config['timeframe'],
'timeframe_ms': timeframe_to_msecs(config['timeframe']),
'timeframe_min': timeframe_to_minutes(config['timeframe']),
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),
@@ -248,9 +252,10 @@ class RPC:
def _rpc_trade_history(self, limit: int) -> Dict:
""" Returns the X last trades """
if limit > 0:
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.id.desc()).limit(limit)
else:
trades = Trade.get_trades().order_by(Trade.id.desc()).all()
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
output = [trade.to_json() for trade in trades]
@@ -519,7 +524,7 @@ class RPC:
# check if valid pair
# check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
if trade:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
@@ -528,11 +533,51 @@ class RPC:
# execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price):
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
else:
return None
def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]:
"""
Handler for delete <id>.
Delete the given trade and close eventually existing open orders.
"""
with self._freqtrade._sell_lock:
c_count = 0
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
if not trade:
logger.warning('delete trade: Invalid argument received')
raise RPCException('invalid argument')
# Try cancelling regular order if that exists
if trade.open_order_id:
try:
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
c_count += 1
except (ExchangeError, InvalidOrderException):
pass
# cancel stoploss on exchange ...
if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
and trade.stoploss_order_id):
try:
self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
trade.pair)
c_count += 1
except (ExchangeError, InvalidOrderException):
pass
Trade.session.delete(trade)
Trade.session.flush()
self._freqtrade.wallets.update()
return {
'result': 'success',
'trade_id': trade_id,
'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
'cancel_order_count': c_count,
}
def _rpc_performance(self) -> List[Dict[str, Any]]:
"""
Handler for performance.

View File

@@ -5,6 +5,7 @@ This module manage Telegram communication
"""
import json
import logging
import arrow
from typing import Any, Callable, Dict
from tabulate import tabulate
@@ -92,6 +93,8 @@ class Telegram(RPC):
CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy),
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
@@ -496,6 +499,62 @@ class Telegram(RPC):
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /trades <n>
Returns last n recent trades.
:param bot: telegram bot
:param update: message update
:return: None
"""
stake_cur = self._config['stake_currency']
try:
nrecent = int(context.args[0])
except (TypeError, ValueError, IndexError):
nrecent = 10
try:
trades = self._rpc_trade_history(
nrecent
)
trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(),
trade['pair'],
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Open Date',
'Pair',
f'Profit ({stake_cur})',
],
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /delete <id>.
Delete the given trade
:param bot: telegram bot
:param update: message update
:return: None
"""
trade_id = context.args[0] if len(context.args) > 0 else None
try:
msg = self._rpc_delete(trade_id)
self._send_msg((
'`{result_msg}`\n'
'Please make sure to take care of this asset on the exchange manually.'
).format(**msg))
except RPCException as e:
self._send_msg(str(e))
@authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None:
"""
@@ -609,10 +668,12 @@ class Telegram(RPC):
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit:* `Lists cumulative profit from all finished trades`\n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n"
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
"*/count:* `Show number of trades running compared to allowed number of trades`"

View File

@@ -7,20 +7,19 @@ import warnings
from abc import ABC, abstractmethod
from datetime import datetime, timezone
from enum import Enum
from typing import Dict, NamedTuple, Optional, Tuple
from typing import Dict, List, NamedTuple, Optional, Tuple
import arrow
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import StrategyError
from freqtrade.exceptions import StrategyError, OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.persistence import Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
@@ -195,6 +194,63 @@ class IStrategy(ABC):
"""
return False
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
"""
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
return True
def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
@@ -208,6 +264,10 @@ class IStrategy(ABC):
"""
return []
###
# END - Intended to be overridden by strategy
###
def get_strategy_name(self) -> str:
"""
Returns strategy class name
@@ -277,6 +337,8 @@ class IStrategy(ABC):
# Defs that only make change on new candle data.
dataframe = self.analyze_ticker(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
if self.dp:
self.dp._set_cached_df(pair, self.timeframe, dataframe)
else:
logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0
@@ -288,13 +350,53 @@ class IStrategy(ABC):
return dataframe
def analyze_pair(self, pair: str) -> None:
"""
Fetch data for this pair from dataprovider and analyze.
Stores the dataframe into the dataprovider.
The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`.
:param pair: Pair to analyze.
"""
if not self.dp:
raise OperationalException("DataProvider not found.")
dataframe = self.dp.ohlcv(pair, self.timeframe)
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return
try:
df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = strategy_safe_wrapper(
self._analyze_ticker_internal, message=""
)(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date)
except StrategyError as error:
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
return
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return
def analyze(self, pairs: List[str]) -> None:
"""
Analyze all pairs using analyze_pair().
:param pairs: List of pairs to analyze
"""
for pair in pairs:
self.analyze_pair(pair)
@staticmethod
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
""" keep some data for dataframes """
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
""" make sure data is unmodified """
"""
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
"""
message = ""
if df_len != len(dataframe):
message = "length"
@@ -308,31 +410,17 @@ class IStrategy(ABC):
else:
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]:
def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]:
"""
Calculates current signal based several technical analysis indicators
Calculates current signal based based on the buy / sell columns of the dataframe.
Used by Bot to get the signal to buy or sell
:param pair: pair in format ANT/BTC
:param interval: Interval to use (in min)
:param dataframe: Dataframe to analyze
:param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return False, False
try:
df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = strategy_safe_wrapper(
self._analyze_ticker_internal, message=""
)(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date)
except StrategyError as error:
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
return False, False
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False
latest_date = dataframe['date'].max()
@@ -341,24 +429,18 @@ class IStrategy(ABC):
latest_date = arrow.get(latest_date)
# Check if dataframe is out of date
interval_minutes = timeframe_to_minutes(interval)
timeframe_minutes = timeframe_to_minutes(timeframe)
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))):
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - latest_date).seconds // 60
pair, (arrow.utcnow() - latest_date).seconds // 60
)
return False, False
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug(
'trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'],
pair,
str(buy),
str(sell)
)
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(buy), str(sell))
return buy, sell
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
@@ -504,7 +586,8 @@ class IStrategy(ABC):
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
"""
Creates a dataframe and populates indicators for given candle (OHLCV) data
Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advice_buy or advise_sell!
Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run.
Has positive effects on memory usage for whatever reason - also when

View File

@@ -5,7 +5,7 @@ from freqtrade.exceptions import StrategyError
logger = logging.getLogger(__name__)
def strategy_safe_wrapper(f, message: str = "", default_retval=None):
def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False):
"""
Wrapper around user-provided methods and functions.
Caches all exceptions and returns either the default_retval (if it's not None) or raises
@@ -20,7 +20,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None):
f"Strategy caused the following exception: {error}"
f"{f}"
)
if default_retval is None:
if default_retval is None and not supress_error:
raise StrategyError(str(error)) from error
return default_retval
except Exception as error:
@@ -28,7 +28,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None):
f"{message}"
f"Unexpected error {error} calling {f}"
)
if default_retval is None:
if default_retval is None and not supress_error:
raise StrategyError(str(error)) from error
return default_retval

View File

@@ -1,4 +1,65 @@
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote ressource for comparison)
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, this simply does nothing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
"""
return True
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
return True
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
"""
Check buy timeout function callback.