Merge branch 'develop' into hyperopt_colorama_init

This commit is contained in:
Matthias
2020-06-16 10:16:41 +02:00
98 changed files with 1442 additions and 643 deletions

View File

@@ -15,7 +15,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange",
"max_open_trades", "stake_amount", "fee"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
@@ -59,10 +59,10 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename",
"timerange", "ticker_interval", "no_trades"]
"timerange", "timeframe", "no_trades"]
ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
"trade_source", "ticker_interval"]
"trade_source", "timeframe"]
ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"]
@@ -318,7 +318,7 @@ class Arguments:
# Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser(
'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.',
help='Print available timeframes for the exchange.',
parents=[_common_parser],
)
list_timeframes_cmd.set_defaults(func=start_list_timeframes)

View File

@@ -75,8 +75,8 @@ def ask_user_config() -> Dict[str, Any]:
},
{
"type": "text",
"name": "ticker_interval",
"message": "Please insert your timeframe (ticker interval):",
"name": "timeframe",
"message": "Please insert your desired timeframe (e.g. 5m):",
"default": "5m",
},
{

View File

@@ -110,8 +110,8 @@ AVAILABLE_CLI_OPTIONS = {
action='store_true',
),
# Optimize common
"ticker_interval": Arg(
'-i', '--ticker-interval',
"timeframe": Arg(
'-i', '--timeframe', '--ticker-interval',
help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).',
),
"timerange": Arg(

View File

@@ -102,8 +102,8 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
Print ticker intervals (timeframes) available on Exchange
"""
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
# Do not use ticker_interval set in the config
config['ticker_interval'] = None
# Do not use timeframe set in the config
config['timeframe'] = None
# Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)

View File

@@ -25,7 +25,6 @@ def start_test_pairlist(args: Dict[str, Any]) -> None:
results = {}
for curr in quote_currencies:
config['stake_currency'] = curr
# Do not use ticker_interval set in the config
pairlists = PairListManager(exchange, config)
pairlists.refresh_pairlist()
results[curr] = pairlists.whitelist

View File

@@ -204,9 +204,9 @@ class Configuration:
def _process_optimize_options(self, config: Dict[str, Any]) -> None:
# This will override the strategy configuration
self._args_to_config(config, argname='ticker_interval',
logstring='Parameter -i/--ticker-interval detected ... '
'Using ticker_interval: {} ...')
self._args_to_config(config, argname='timeframe',
logstring='Parameter -i/--timeframe detected ... '
'Using timeframe: {} ...')
self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...')
@@ -242,8 +242,8 @@ class Configuration:
self._args_to_config(config, argname='strategy_list',
logstring='Using strategy list of {} strategies', logfun=len)
self._args_to_config(config, argname='ticker_interval',
logstring='Overriding ticker interval with Command line argument')
self._args_to_config(config, argname='timeframe',
logstring='Overriding timeframe with Command line argument')
self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...')

View File

@@ -60,10 +60,16 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
if (config.get('edge', {}).get('enabled', False)
and 'capital_available_percentage' in config.get('edge', {})):
logger.warning(
raise OperationalException(
"DEPRECATED: "
"Using 'edge.capital_available_percentage' has been deprecated in favor of "
"'tradable_balance_ratio'. Please migrate your configuration to "
"'tradable_balance_ratio' and remove 'capital_available_percentage' "
"from the edge configuration."
)
if 'ticker_interval' in config:
logger.warning(
"DEPRECATED: "
"Please use 'timeframe' instead of 'ticker_interval."
)
config['timeframe'] = config['ticker_interval']

View File

@@ -71,7 +71,7 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
'ticker_interval': {'type': 'string'},
'timeframe': {'type': 'string'},
'stake_currency': {'type': 'string'},
'stake_amount': {
'type': ['number', 'string'],
@@ -221,6 +221,7 @@ CONF_SCHEMA = {
},
'username': {'type': 'string'},
'password': {'type': 'string'},
'verbosity': {'type': 'string', 'enum': ['error', 'info']},
},
'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password']
},
@@ -286,7 +287,6 @@ CONF_SCHEMA = {
'process_throttle_secs': {'type': 'integer', 'minimum': 600},
'calculate_since_number_of_days': {'type': 'integer'},
'allowed_risk': {'type': 'number'},
'capital_available_percentage': {'type': 'number'},
'stoploss_range_min': {'type': 'number'},
'stoploss_range_max': {'type': 'number'},
'stoploss_range_step': {'type': 'number'},
@@ -303,6 +303,7 @@ CONF_SCHEMA = {
SCHEMA_TRADE_REQUIRED = [
'exchange',
'timeframe',
'max_open_trades',
'stake_currency',
'stake_amount',

View File

@@ -16,7 +16,7 @@ from freqtrade.persistence import Trade
logger = logging.getLogger(__name__)
# must align with columns in backtest.py
BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "duration",
BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "duration",
"open_rate", "close_rate", "open_at_end", "sell_reason"]
@@ -99,11 +99,11 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS)
persistence.init(db_url, clean_open_orders=False)
columns = ["pair", "open_time", "close_time", "profit", "profitperc",
columns = ["pair", "open_time", "close_time", "profit", "profit_percent",
"open_rate", "close_rate", "amount", "duration", "sell_reason",
"fee_open", "fee_close", "open_rate_requested", "close_rate_requested",
"stake_amount", "max_rate", "min_rate", "id", "exchange",
"stop_loss", "initial_stop_loss", "strategy", "ticker_interval"]
"stop_loss", "initial_stop_loss", "strategy", "timeframe"]
trades = pd.DataFrame([(t.pair,
t.open_date.replace(tzinfo=timezone.utc),
@@ -121,7 +121,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
t.min_rate,
t.id, t.exchange,
t.stop_loss, t.initial_stop_loss,
t.strategy, t.ticker_interval
t.strategy, t.timeframe
)
for t in Trade.get_trades().all()],
columns=columns)
@@ -190,7 +190,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
"""
Adds a column `col_name` with the cumulative profit for the given trades array.
:param df: DataFrame with date index
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
:param col_name: Column name that will be assigned the results
:param timeframe: Timeframe used during the operations
:return: Returns df with one additional column, col_name, containing the cumulative profit.
@@ -201,7 +201,8 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
from freqtrade.exchange import timeframe_to_minutes
timeframe_minutes = timeframe_to_minutes(timeframe)
# Resample to timeframe to make sure trades match candles
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time')[['profitperc']].sum()
_trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time'
)[['profit_percent']].sum()
df.loc[:, col_name] = _trades_sum.cumsum()
# Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0
@@ -211,13 +212,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
value_col: str = 'profitperc'
value_col: str = 'profit_percent'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
:param trades: DataFrame containing trades (requires columns close_time and profit_percent)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
:param value_col: Column in DataFrame to use for values (defaults to 'profitperc')
:param value_col: Column in DataFrame to use for values (defaults to 'profit_percent')
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
:raise: ValueError if trade-dataframe was found empty.
"""

View File

@@ -197,7 +197,7 @@ def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame:
df_new['date'] = df_new.index
# Drop 0 volume rows
df_new = df_new.dropna()
return df_new[DEFAULT_DATAFRAME_COLUMNS]
return df_new.loc[:, DEFAULT_DATAFRAME_COLUMNS]
def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
@@ -236,12 +236,12 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
from freqtrade.data.history.idatahandler import get_datahandler
src = get_datahandler(config['datadir'], convert_from)
trg = get_datahandler(config['datadir'], convert_to)
timeframes = config.get('timeframes', [config.get('ticker_interval')])
timeframes = config.get('timeframes', [config.get('timeframe')])
logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}")
if 'pairs' not in config:
config['pairs'] = []
# Check timeframes or fall back to ticker_interval.
# Check timeframes or fall back to timeframe.
for timeframe in timeframes:
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
timeframe))

View File

@@ -55,7 +55,7 @@ class DataProvider:
Use False only for read-only operations (where the dataframe is not modified)
"""
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, timeframe or self._config['ticker_interval']),
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
copy=copy)
else:
return DataFrame()
@@ -67,7 +67,7 @@ class DataProvider:
:param timeframe: timeframe to get data for
"""
return load_pair_history(pair=pair,
timeframe=timeframe or self._config['ticker_interval'],
timeframe=timeframe or self._config['timeframe'],
datadir=self._config['datadir']
)

View File

@@ -57,9 +57,7 @@ class Edge:
if self.config['stake_amount'] != UNLIMITED_STAKE_AMOUNT:
raise OperationalException('Edge works only with unlimited stake amount')
# Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future.
self._capital_percentage: float = self.edge_config.get(
'capital_available_percentage', self.config['tradable_balance_ratio'])
self._capital_ratio: float = self.config['tradable_balance_ratio']
self._allowed_risk: float = self.edge_config.get('allowed_risk')
self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14)
self._last_updated: int = 0 # Timestamp of pairs last updated time
@@ -100,14 +98,14 @@ class Edge:
datadir=self.config['datadir'],
pairs=pairs,
exchange=self.exchange,
timeframe=self.strategy.ticker_interval,
timeframe=self.strategy.timeframe,
timerange=self._timerange,
)
data = load_data(
datadir=self.config['datadir'],
pairs=pairs,
timeframe=self.strategy.ticker_interval,
timeframe=self.strategy.timeframe,
timerange=self._timerange,
startup_candles=self.strategy.startup_candle_count,
data_format=self.config.get('dataformat_ohlcv', 'json'),
@@ -157,7 +155,7 @@ class Edge:
def stake_amount(self, pair: str, free_capital: float,
total_capital: float, capital_in_trade: float) -> float:
stoploss = self.stoploss(pair)
available_capital = (total_capital + capital_in_trade) * self._capital_percentage
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)

View File

@@ -79,7 +79,7 @@ class Exchange:
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
logger.info(f"Using CCXT {ccxt.__version__}")
exchange_config = config['exchange']
# Deep merge ft_has with default ft_has options
@@ -115,7 +115,7 @@ class Exchange:
if validate:
# Check if timeframe is available
self.validate_timeframes(config.get('ticker_interval'))
self.validate_timeframes(config.get('timeframe'))
# Initial markets load
self._load_markets()
@@ -190,7 +190,7 @@ class Exchange:
def markets(self) -> Dict:
"""exchange ccxt markets"""
if not self._api.markets:
logger.warning("Markets were not loaded. Loading them now..")
logger.info("Markets were not loaded. Loading them now..")
self._load_markets()
return self._api.markets
@@ -275,8 +275,8 @@ class Exchange:
except ccxt.BaseError as e:
logger.warning('Unable to initialize markets. Reason: %s', e)
def _reload_markets(self) -> None:
"""Reload markets both sync and async, if refresh interval has passed"""
def reload_markets(self) -> None:
"""Reload markets both sync and async if refresh interval has passed """
# Check whether markets have to be reloaded
if (self._last_markets_refresh > 0) and (
self._last_markets_refresh + self.markets_refresh_interval
@@ -889,14 +889,19 @@ class Exchange:
Async wrapper handling downloading trades using either time or id based methods.
"""
logger.debug(f"_async_get_trade_history(), pair: {pair}, "
f"since: {since}, until: {until}, from_id: {from_id}")
if until is None:
until = ccxt.Exchange.milliseconds()
logger.debug(f"Exchange milliseconds: {until}")
if self._trades_pagination == 'time':
return await self._async_get_trade_history_time(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds())
pair=pair, since=since, until=until)
elif self._trades_pagination == 'id':
return await self._async_get_trade_history_id(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
pair=pair, since=since, until=until, from_id=from_id
)
else:
raise OperationalException(f"Exchange {self.name} does use neither time, "
@@ -947,6 +952,9 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
# Assign method to get_stoploss_order to allow easy overriding in other classes
cancel_stoploss_order = cancel_order
def is_cancel_order_result_suitable(self, corder) -> bool:
if not isinstance(corder, dict):
return False
@@ -999,6 +1007,9 @@ class Exchange:
except ccxt.BaseError as e:
raise OperationalException(e) from e
# Assign method to get_stoploss_order to allow easy overriding in other classes
get_stoploss_order = get_order
@retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
"""
@@ -1104,9 +1115,12 @@ class Exchange:
order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8)
elif fee_curr in self.get_pair_quote_currency(order['symbol']):
# Quote currency - divide by cost
return round(order['fee']['cost'] / order['cost'], 8)
return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None
else:
# If Fee currency is a different currency
if not order['cost']:
# If cost is None or 0.0 -> falsy, return None
return None
try:
comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency'])
tick = self.fetch_ticker(comb)

View File

@@ -2,7 +2,12 @@
import logging
from typing import Dict
import ccxt
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__)
@@ -10,5 +15,104 @@ logger = logging.getLogger(__name__)
class Ftx(Exchange):
_ft_has: Dict = {
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['type'] == 'stop' and stop_loss > float(order['price'])
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
"""
Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order.
Limit orders are defined by having orderPrice set, otherwise a market order is used.
"""
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
limit_rate = stop_price * limit_price_pct
ordertype = "stop"
stop_price = self.price_to_precision(pair, stop_price)
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
try:
params = self._params.copy()
if order_types.get('stoploss', 'market') == 'limit':
# set orderPrice to place limit order, otherwise it's a market order
params['orderPrice'] = limit_rate
amount = self.amount_to_precision(pair, amount)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, price=stop_price, params=params)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order
except ccxt.InsufficientFunds as e:
raise DependencyException(
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:
raise InvalidOrderException(
f'Could not 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.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def get_stoploss_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']:
try:
order = self._dry_run_open_orders[order_id]
return order
except KeyError as e:
# Gracefully handle errors with dry-run orders.
raise InvalidOrderException(
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
try:
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
order = [order for order in orders if order['id'] == order_id]
if len(order) == 1:
return order[0]
else:
raise InvalidOrderException(f"Could not get stoploss order for id {order_id}")
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier
def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']:
return {}
try:
return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@@ -139,8 +139,8 @@ class FreqtradeBot:
:return: True if one or more trades has been created or closed, False otherwise
"""
# Check whether markets have to be reloaded
self.exchange._reload_markets()
# Check whether markets have to be reloaded and reload them when it's needed
self.exchange.reload_markets()
# Query trades from persistence layer
trades = Trade.get_open_trades()
@@ -421,8 +421,8 @@ class FreqtradeBot:
# running get_signal on historical data fetched
(buy, sell) = self.strategy.get_signal(
pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(pair, self.strategy.ticker_interval))
pair, self.strategy.timeframe,
self.dataprovider.ohlcv(pair, self.strategy.timeframe))
if buy and not sell:
stake_amount = self.get_trade_stake_amount(pair)
@@ -547,7 +547,7 @@ class FreqtradeBot:
exchange=self.exchange.id,
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
ticker_interval=timeframe_to_minutes(self.config['ticker_interval'])
timeframe=timeframe_to_minutes(self.config['timeframe'])
)
# Update fees if order is closed
@@ -676,6 +676,8 @@ class FreqtradeBot:
raise PricingError from e
else:
rate = self.exchange.fetch_ticker(pair)[ask_strategy['price_side']]
if rate is None:
raise PricingError(f"Sell-Rate for {pair} was empty.")
self._sell_rate_cache[pair] = rate
return rate
@@ -696,15 +698,14 @@ 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.ticker_interval,
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
trade.pair, self.strategy.timeframe,
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
if config_ask_strategy.get('use_order_book', False):
# logger.debug('Order book %s',orderBook)
order_book_min = config_ask_strategy.get('order_book_min', 1)
order_book_max = config_ask_strategy.get('order_book_max', 1)
logger.info(f'Using order book between {order_book_min} and {order_book_max} '
f'for selling {trade.pair}...')
logger.debug(f'Using order book between {order_book_min} and {order_book_max} '
f'for selling {trade.pair}...')
order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
order_book_min=order_book_min,
@@ -719,6 +720,9 @@ class FreqtradeBot:
raise PricingError from e
logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
f"{sell_rate:0.8f}")
# Assign sell-rate to cache - otherwise sell-rate is never updated in the cache,
# resulting in outdated RPC messages
self._sell_rate_cache[trade.pair] = sell_rate
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True
@@ -769,18 +773,18 @@ class FreqtradeBot:
try:
# First we check if there is already a stoploss on exchange
stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
if trade.stoploss_order_id else None
except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception)
# We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed':
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, stoploss_order, sl_order=True)
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval']))
timeframe_to_next_date(self.config['timeframe']))
self._notify_sell(trade, "stoploss")
return True
@@ -802,7 +806,7 @@ class FreqtradeBot:
return False
# If stoploss order is canceled for some reason we add it
if stoploss_order and stoploss_order['status'] == 'canceled':
if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
rate=trade.stop_loss):
return False
@@ -835,7 +839,7 @@ class FreqtradeBot:
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
'in order to add another one ...', order['id'])
try:
self.exchange.cancel_order(order['id'], trade.pair)
self.exchange.cancel_stoploss_order(order['id'], trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}")
@@ -1063,7 +1067,7 @@ class FreqtradeBot:
# First cancelling stoploss on exchange ...
if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id:
try:
self.exchange.cancel_order(trade.stoploss_order_id, trade.pair)
self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair)
except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
@@ -1090,7 +1094,7 @@ class FreqtradeBot:
Trade.session.flush()
# Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']))
self._notify_sell(trade, order_type)

View File

@@ -11,7 +11,7 @@ from freqtrade.exceptions import OperationalException
logger = logging.getLogger(__name__)
def _set_loggers(verbosity: int = 0) -> None:
def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
"""
Set the logging level for third party libraries
:return: None
@@ -28,6 +28,10 @@ def _set_loggers(verbosity: int = 0) -> None:
)
logging.getLogger('telegram').setLevel(logging.INFO)
logging.getLogger('werkzeug').setLevel(
logging.ERROR if api_verbosity == 'error' else logging.INFO
)
def setup_logging(config: Dict[str, Any]) -> None:
"""
@@ -77,5 +81,5 @@ def setup_logging(config: Dict[str, Any]) -> None:
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=log_handlers
)
_set_loggers(verbosity)
_set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
logger.info('Verbosity set to %s', verbosity)

View File

@@ -18,7 +18,8 @@ from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.optimize.optimize_reports import (show_backtest_results,
from freqtrade.optimize.optimize_reports import (generate_backtest_stats,
show_backtest_results,
store_backtest_result)
from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.persistence import Trade
@@ -94,10 +95,10 @@ class Backtesting:
self.strategylist.append(StrategyResolver.load_strategy(self.config))
validate_config_consistency(self.config)
if "ticker_interval" not in self.config:
if "timeframe" not in self.config:
raise OperationalException("Timeframe (ticker interval) needs to be set in either "
"configuration or as cli argument `--ticker-interval 5m`")
self.timeframe = str(self.config.get('ticker_interval'))
"configuration or as cli argument `--timeframe 5m`")
self.timeframe = str(self.config.get('timeframe'))
self.timeframe_min = timeframe_to_minutes(self.timeframe)
# Get maximum required startup period
@@ -411,4 +412,5 @@ class Backtesting:
if self.config.get('export', False):
store_backtest_result(self.config['exportfilename'], all_results)
# Show backtest results
show_backtest_results(self.config, data, all_results)
stats = generate_backtest_stats(self.config, data, all_results)
show_backtest_results(self.config, stats)

View File

@@ -42,8 +42,8 @@ class DefaultHyperOptLoss(IHyperOptLoss):
* 0.25: Avoiding trade loss
* 1.0 to total profit, compared to the expected value (`EXPECTED_MAX_PROFIT`) defined above
"""
total_profit = results.profit_percent.sum()
trade_duration = results.trade_duration.mean()
total_profit = results['profit_percent'].sum()
trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)

View File

@@ -13,7 +13,7 @@ from collections import OrderedDict
from math import ceil
from operator import itemgetter
from pathlib import Path
from pprint import pprint
from pprint import pformat
from typing import Any, Dict, List, Optional
import progressbar
@@ -231,6 +231,9 @@ class Hyperopt:
if space in ['buy', 'sell']:
result_dict.setdefault('params', {}).update(space_params)
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
# OrderedDict is used to keep the numeric order of the items
@@ -245,11 +248,24 @@ class Hyperopt:
def _params_pretty_print(params, space: str, header: str) -> None:
if space in params:
space_params = Hyperopt._space_params(params, space, 5)
params_result = f"\n# {header}\n"
if space == 'stoploss':
print(header, space_params.get('stoploss'))
params_result += f"stoploss = {space_params.get('stoploss')}"
elif space == 'roi':
# TODO: get rid of OrderedDict when support for python 3.6 will be
# dropped (dicts keep the order as the language feature)
minimal_roi_result = rapidjson.dumps(
OrderedDict(
(str(k), v) for k, v in space_params.items()
),
default=str, indent=4, number_mode=rapidjson.NM_NATIVE)
params_result += f"minimal_roi = {minimal_roi_result}"
else:
print(header)
pprint(space_params, indent=4)
params_result += f"{space}_params = {pformat(space_params, indent=4)}"
params_result = params_result.replace("}", "\n}").replace("{", "{\n ")
params_result = params_result.replace("\n", "\n ")
print(params_result)
@staticmethod
def _space_params(params, space: str, r: int = None) -> Dict:

View File

@@ -31,13 +31,15 @@ class IHyperOpt(ABC):
Class attributes you can use:
ticker_interval -> int: value of the ticker interval to use for the strategy
"""
ticker_interval: str
ticker_interval: str # DEPRECATED
timeframe: str
def __init__(self, config: dict) -> None:
self.config = config
# Assign ticker_interval to be used in hyperopt
IHyperOpt.ticker_interval = str(config['ticker_interval'])
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
IHyperOpt.timeframe = str(config['timeframe'])
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
@@ -218,9 +220,10 @@ class IHyperOpt(ABC):
# Why do I still need such shamanic mantras in modern python?
def __getstate__(self):
state = self.__dict__.copy()
state['ticker_interval'] = self.ticker_interval
state['timeframe'] = self.timeframe
return state
def __setstate__(self, state):
self.__dict__.update(state)
IHyperOpt.ticker_interval = state['ticker_interval']
IHyperOpt.ticker_interval = state['timeframe']
IHyperOpt.timeframe = state['timeframe']

View File

@@ -14,7 +14,7 @@ class IHyperOptLoss(ABC):
Interface for freqtrade hyperopt Loss functions.
Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.)
"""
ticker_interval: str
timeframe: str
@staticmethod
@abstractmethod

View File

@@ -34,5 +34,5 @@ class OnlyProfitHyperOptLoss(IHyperOptLoss):
"""
Objective function, returns smaller number for better results.
"""
total_profit = results.profit_percent.sum()
total_profit = results['profit_percent'].sum()
return 1 - total_profit / EXPECTED_MAX_PROFIT

View File

@@ -18,10 +18,7 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
:param all_results: Dict of Dataframes, one results dataframe per strategy
"""
for strategy, results in all_results.items():
records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]
records = backtest_result_to_list(results)
if records:
filename = recordfilename
@@ -34,6 +31,18 @@ def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame
file_dump_json(filename, records)
def backtest_result_to_list(results: DataFrame) -> List[List]:
"""
Converts a list of Backtest-results to list
:param results: Dataframe containing results for one strategy
:return: List of Lists containing the trades
"""
return [[t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value]
for index, t in results.iterrows()]
def _get_line_floatfmt() -> List[str]:
"""
Generate floatformat (goes in line with _generate_result_line())
@@ -56,25 +65,25 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column:
"""
return {
'key': first_column,
'trades': len(result.index),
'profit_mean': result.profit_percent.mean(),
'profit_mean_pct': result.profit_percent.mean() * 100.0,
'profit_sum': result.profit_percent.sum(),
'profit_sum_pct': result.profit_percent.sum() * 100.0,
'profit_total_abs': result.profit_abs.sum(),
'profit_total_pct': result.profit_percent.sum() * 100.0 / max_open_trades,
'trades': len(result),
'profit_mean': result['profit_percent'].mean(),
'profit_mean_pct': result['profit_percent'].mean() * 100.0,
'profit_sum': result['profit_percent'].sum(),
'profit_sum_pct': result['profit_percent'].sum() * 100.0,
'profit_total_abs': result['profit_abs'].sum(),
'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades,
'duration_avg': str(timedelta(
minutes=round(result.trade_duration.mean()))
minutes=round(result['trade_duration'].mean()))
) if not result.empty else '0:00',
# 'duration_max': str(timedelta(
# minutes=round(result.trade_duration.max()))
# minutes=round(result['trade_duration'].max()))
# ) if not result.empty else '0:00',
# 'duration_min': str(timedelta(
# minutes=round(result.trade_duration.min()))
# minutes=round(result['trade_duration'].min()))
# ) if not result.empty else '0:00',
'wins': len(result[result.profit_abs > 0]),
'draws': len(result[result.profit_abs == 0]),
'losses': len(result[result.profit_abs < 0]),
'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]),
'losses': len(result[result['profit_abs'] < 0]),
}
@@ -93,8 +102,8 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
tabular_data = []
for pair in data:
result = results[results.pair == pair]
if skip_nan and result.profit_abs.isnull().all():
result = results[results['pair'] == pair]
if skip_nan and result['profit_abs'].isnull().all():
continue
tabular_data.append(_generate_result_line(result, max_open_trades, pair))
@@ -104,25 +113,6 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, max_open_t
return tabular_data
def generate_text_table(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
:param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string
"""
headers = _get_line_header('Pair', stake_currency)
floatfmt = _get_line_floatfmt()
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']
] for t in pair_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
"""
Generate small table outlining Backtest results
@@ -157,33 +147,6 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
return tabular_data
def generate_text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]],
stake_currency: str) -> str:
"""
Generate small table outlining Backtest results
:param sell_reason_stats: Sell reason metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
'Sell Reason',
'Sells',
'Wins',
'Draws',
'Losses',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
]
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_pct_total'],
] for t in sell_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
all_results: Dict) -> List[Dict]:
"""
@@ -200,26 +163,6 @@ def generate_strategy_metrics(stake_currency: str, max_open_trades: int,
return tabular_data
def generate_text_table_strategy(strategy_results, stake_currency: str) -> str:
"""
Generate summary table per strategy
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:return: pretty printed table with tabulate as string
"""
floatfmt = _get_line_floatfmt()
headers = _get_line_header('Strategy', 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']
] for t in strategy_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_edge_table(results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', 'd', 'd')
@@ -246,12 +189,20 @@ def generate_edge_table(results: dict) -> str:
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
all_results: Dict[str, DataFrame]):
def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
all_results: Dict[str, DataFrame]) -> Dict[str, Any]:
"""
:param config: Configuration object used for backtest
:param btdata: Backtest data
:param all_results: backtest result - dictionary with { Strategy: results}.
:return:
Dictionary containing results per strategy and a stratgy summary.
"""
stake_currency = config['stake_currency']
max_open_trades = config['max_open_trades']
result: Dict[str, Any] = {'strategy': {}}
for strategy, results in all_results.items():
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
max_open_trades=max_open_trades,
results=results, skip_nan=False)
@@ -261,21 +212,111 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
max_open_trades=max_open_trades,
results=results.loc[results['open_at_end']],
skip_nan=True)
strat_stats = {
'trades': backtest_result_to_list(results),
'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats,
'left_open_trades': left_open_results,
}
result['strategy'][strategy] = strat_stats
strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
max_open_trades=max_open_trades,
all_results=all_results)
result['strategy_comparison'] = strategy_results
return result
###
# Start output section
###
def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generates and returns a text table for the given backtest data and the results dataframe
:param pair_results: List of Dictionaries - one entry per pair + final TOTAL row
:param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string
"""
headers = _get_line_header('Pair', stake_currency)
floatfmt = _get_line_floatfmt()
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']
] for t in pair_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generate small table outlining Backtest results
:param sell_reason_stats: Sell reason metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
'Sell Reason',
'Sells',
'Wins',
'Draws',
'Losses',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
]
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_pct_total'],
] for t in sell_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
def text_table_strategy(strategy_results, stake_currency: str) -> str:
"""
Generate summary table per strategy
:param stake_currency: stake-currency - used to correctly name headers
:param max_open_trades: Maximum allowed open trades used for backtest
:param all_results: Dict of <Strategyname: BacktestResult> containing results for all strategies
:return: pretty printed table with tabulate as string
"""
floatfmt = _get_line_floatfmt()
headers = _get_line_header('Strategy', 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']
] for t in strategy_results]
# Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(output, headers=headers,
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
def show_backtest_results(config: Dict, backtest_stats: Dict):
stake_currency = config['stake_currency']
for strategy, results in backtest_stats['strategy'].items():
# Print results
print(f"Result for strategy {strategy}")
table = generate_text_table(pair_results, stake_currency=stake_currency)
table = text_table_bt_results(results['results_per_pair'], stake_currency=stake_currency)
if isinstance(table, str):
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table_sell_reason(sell_reason_stats=sell_reason_stats,
stake_currency=stake_currency,
)
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
stake_currency=stake_currency)
if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = generate_text_table(left_open_results, stake_currency=stake_currency)
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)
if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
@@ -283,13 +324,10 @@ def show_backtest_results(config: Dict, btdata: Dict[str, DataFrame],
print('=' * len(table.splitlines()[0]))
print()
if len(all_results) > 1:
if len(backtest_stats['strategy']) > 1:
# Print Strategy summary table
strategy_results = generate_strategy_metrics(stake_currency=stake_currency,
max_open_trades=max_open_trades,
all_results=all_results)
table = generate_text_table_strategy(strategy_results, stake_currency)
table = text_table_strategy(backtest_stats['strategy_comparison'], stake_currency)
print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))

View File

@@ -150,6 +150,9 @@ class IPairList(ABC):
black_listed
"""
markets = self._exchange.markets
if not markets:
raise OperationalException(
'Markets not loaded. Make sure that exchange is initialized correctly.')
sanitized_whitelist: List[str] = []
for pair in pairlist:

View File

@@ -131,6 +131,6 @@ class PairListManager():
def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes:
"""
Create list of pair tuples with (pair, ticker_interval)
Create list of pair tuples with (pair, timeframe)
"""
return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs]
return [(pair, timeframe or self._config['timeframe']) for pair in pairs]

View File

@@ -86,7 +86,7 @@ def check_migrate(engine) -> None:
logger.debug(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'sell_order_status'):
if not has_column(cols, 'timeframe'):
logger.info(f'Running database migration - backup available as {table_back_name}')
fee_open = get_column_def(cols, 'fee_open', 'fee')
@@ -107,7 +107,12 @@ def check_migrate(engine) -> None:
min_rate = get_column_def(cols, 'min_rate', 'null')
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
else:
timeframe = get_column_def(cols, 'timeframe', 'null')
open_trade_price = get_column_def(cols, 'open_trade_price',
f'amount * open_rate * (1 + {fee_open})')
close_profit_abs = get_column_def(
@@ -133,7 +138,7 @@ def check_migrate(engine) -> None:
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy,
ticker_interval, open_trade_price, close_profit_abs
timeframe, open_trade_price, close_profit_abs
)
select id, lower(exchange),
case
@@ -155,7 +160,7 @@ def check_migrate(engine) -> None:
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{strategy} strategy, {ticker_interval} ticker_interval,
{strategy} strategy, {timeframe} timeframe,
{open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
from {table_back_name}
""")
@@ -232,7 +237,7 @@ class Trade(_DECL_BASE):
sell_reason = Column(String, nullable=True)
sell_order_status = Column(String, nullable=True)
strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True)
timeframe = Column(Integer, nullable=True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -249,47 +254,58 @@ class Trade(_DECL_BASE):
'trade_id': self.id,
'pair': self.pair,
'is_open': self.is_open,
'exchange': self.exchange,
'amount': round(self.amount, 8),
'stake_amount': round(self.stake_amount, 8),
'strategy': self.strategy,
'ticker_interval': self.timeframe, # DEPRECATED
'timeframe': self.timeframe,
'fee_open': self.fee_open,
'fee_open_cost': self.fee_open_cost,
'fee_open_currency': self.fee_open_currency,
'fee_close': self.fee_close,
'fee_close_cost': self.fee_close_cost,
'fee_close_currency': self.fee_close_currency,
'open_date_hum': arrow.get(self.open_date).humanize(),
'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"),
'open_timestamp': int(self.open_date.timestamp() * 1000),
'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested,
'open_trade_price': self.open_trade_price,
'close_date_hum': (arrow.get(self.close_date).humanize()
if self.close_date else None),
'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S")
if self.close_date else None),
'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None,
'open_rate': self.open_rate,
'open_rate_requested': self.open_rate_requested,
'open_trade_price': self.open_trade_price,
'close_rate': self.close_rate,
'close_rate_requested': self.close_rate_requested,
'amount': round(self.amount, 8),
'stake_amount': round(self.stake_amount, 8),
'close_profit': self.close_profit,
'close_profit_abs': self.close_profit_abs,
'sell_reason': self.sell_reason,
'sell_order_status': self.sell_order_status,
'stop_loss': self.stop_loss,
'stop_loss': self.stop_loss, # Deprecated - should not be used
'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
'stoploss_order_id': self.stoploss_order_id,
'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S")
if self.stoploss_last_update else None),
'stoploss_last_update_timestamp': (int(self.stoploss_last_update.timestamp() * 1000)
if self.stoploss_last_update else None),
'initial_stop_loss': self.initial_stop_loss,
'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used
'initial_stop_loss_abs': self.initial_stop_loss,
'initial_stop_loss_ratio': (self.initial_stop_loss_pct
if self.initial_stop_loss_pct else None),
'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100
if self.initial_stop_loss_pct else None),
'min_rate': self.min_rate,
'max_rate': self.max_rate,
'strategy': self.strategy,
'ticker_interval': self.ticker_interval,
'open_order_id': self.open_order_id,
'exchange': self.exchange,
}
def adjust_min_max_rates(self, current_price: float) -> None:
@@ -364,7 +380,7 @@ class Trade(_DECL_BASE):
elif order_type in ('market', 'limit') and order['side'] == 'sell':
self.close(order['price'])
logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self)
elif order_type in ('stop_loss_limit', 'stop-loss'):
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
logger.info('%s is hit for %s.', order_type.upper(), self)

View File

@@ -45,7 +45,7 @@ def init_plotscript(config):
data = load_data(
datadir=config.get("datadir"),
pairs=pairs,
timeframe=config.get('ticker_interval', '5m'),
timeframe=config.get('timeframe', '5m'),
timerange=timerange,
data_format=config.get('dataformat_ohlcv', 'json'),
)
@@ -162,7 +162,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Trades can be empty
if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, "
trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, "
f"{row['sell_reason']}, {row['duration']} min",
axis=1)
trade_buys = go.Scatter(
@@ -181,9 +181,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
trade_sells = go.Scatter(
x=trades.loc[trades['profitperc'] > 0, "close_time"],
y=trades.loc[trades['profitperc'] > 0, "close_rate"],
text=trades.loc[trades['profitperc'] > 0, "desc"],
x=trades.loc[trades['profit_percent'] > 0, "close_time"],
y=trades.loc[trades['profit_percent'] > 0, "close_rate"],
text=trades.loc[trades['profit_percent'] > 0, "desc"],
mode='markers',
name='Sell - Profit',
marker=dict(
@@ -194,9 +194,9 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
)
)
trade_sells_loss = go.Scatter(
x=trades.loc[trades['profitperc'] <= 0, "close_time"],
y=trades.loc[trades['profitperc'] <= 0, "close_rate"],
text=trades.loc[trades['profitperc'] <= 0, "desc"],
x=trades.loc[trades['profit_percent'] <= 0, "close_time"],
y=trades.loc[trades['profit_percent'] <= 0, "close_rate"],
text=trades.loc[trades['profit_percent'] <= 0, "desc"],
mode='markers',
name='Sell - Loss',
marker=dict(
@@ -487,7 +487,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {}
)
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']),
directory=config['user_data_dir'] / "plot")
logger.info('End of plotting process. %s plots generated', pair_counter)
@@ -515,6 +515,6 @@ def plot_profit(config: Dict[str, Any]) -> None:
# Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"],
trades, config.get('ticker_interval', '5m'))
trades, config.get('timeframe', '5m'))
store_plot_file(fig, filename='freqtrade-profit-plot.html',
directory=config['user_data_dir'] / "plot", auto_open=True)

View File

@@ -77,8 +77,9 @@ class HyperOptLossResolver(IResolver):
config, kwargs={},
extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt
hyperoptloss.__class__.ticker_interval = str(config['ticker_interval'])
# Assign timeframe to be used in hyperopt
hyperoptloss.__class__.ticker_interval = str(config['timeframe'])
hyperoptloss.__class__.timeframe = str(config['timeframe'])
if not hasattr(hyperoptloss, 'hyperopt_loss_function'):
raise OperationalException(

View File

@@ -50,11 +50,19 @@ class StrategyResolver(IResolver):
if 'ask_strategy' not in config:
config['ask_strategy'] = {}
if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'):
# Assign ticker_interval to timeframe to keep compatibility
if 'timeframe' not in config:
logger.warning(
"DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'."
)
strategy.timeframe = strategy.ticker_interval
# Set attributes
# Check if we need to override configuration
# (Attribute name, default, subkey)
attributes = [("minimal_roi", {"0": 10.0}, None),
("ticker_interval", None, None),
("timeframe", None, None),
("stoploss", None, None),
("trailing_stop", None, None),
("trailing_stop_positive", None, None),
@@ -80,6 +88,9 @@ class StrategyResolver(IResolver):
StrategyResolver._override_attribute_helper(strategy, config,
attribute, default)
# Assign deprecated variable - to not break users code relying on this.
strategy.ticker_interval = strategy.timeframe
# Loop this list again to have output combined
for attribute, _, subkey in attributes:
if subkey and attribute in config[subkey]:

View File

@@ -172,8 +172,8 @@ class ApiServer(RPC):
self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy',
view_func=self._stopbuy, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf',
view_func=self._reload_conf, methods=['POST'])
self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config',
view_func=self._reload_config, methods=['POST'])
# Info commands
self.app.add_url_rule(f'{BASE_URI}/balance', 'balance',
view_func=self._balance, methods=['GET'])
@@ -304,12 +304,12 @@ class ApiServer(RPC):
@require_login
@rpc_catch_errors
def _reload_conf(self):
def _reload_config(self):
"""
Handler for /reload_conf.
Handler for /reload_config.
Triggers a config file reload
"""
msg = self._rpc_reload_conf()
msg = self._rpc_reload_config()
return self.rest_dump(msg)
@require_login
@@ -360,7 +360,6 @@ class ApiServer(RPC):
Returns a cumulative profit statistics
:return: stats
"""
logger.info("LocalRPC - Profit Command Called")
stats = self._rpc_trade_statistics(self._config['stake_currency'],
self._config.get('fiat_display_currency')
@@ -377,8 +376,6 @@ class ApiServer(RPC):
Returns a cumulative performance statistics
:return: stats
"""
logger.info("LocalRPC - performance Command Called")
stats = self._rpc_performance()
return self.rest_dump(stats)

View File

@@ -101,10 +101,13 @@ class RPC:
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'ticker_interval': config['ticker_interval'],
'ticker_interval': config['timeframe'], # DEPRECATED
'timeframe': config['timeframe'],
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),
'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}),
'state': str(self._freqtrade.state)
}
return val
@@ -130,6 +133,14 @@ class RPC:
except DependencyException:
current_rate = NAN
current_profit = trade.calc_profit_ratio(current_rate)
current_profit_abs = trade.calc_profit(current_rate)
# Calculate guaranteed profit (in case of trailing stop)
stoploss_entry_dist = trade.calc_profit(trade.stop_loss)
stoploss_entry_dist_ratio = trade.calc_profit_ratio(trade.stop_loss)
# calculate distance to stoploss
stoploss_current_dist = trade.stop_loss - current_rate
stoploss_current_dist_ratio = stoploss_current_dist / current_rate
fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
if trade.close_profit is not None else None)
trade_dict = trade.to_json()
@@ -140,6 +151,11 @@ class RPC:
current_rate=current_rate,
current_profit=current_profit,
current_profit_pct=round(current_profit * 100, 2),
current_profit_abs=current_profit_abs,
stoploss_current_dist=stoploss_current_dist,
stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8),
stoploss_entry_dist=stoploss_entry_dist,
stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8),
open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining']
) if order else None,
@@ -283,8 +299,9 @@ class RPC:
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
profit_closed_percent = (round(mean(profit_closed_ratio) * 100, 2) if profit_closed_ratio
else 0.0)
profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0
profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
profit_closed_fiat = self._fiat_converter.convert_amount(
profit_closed_coin_sum,
stake_currency,
@@ -292,7 +309,8 @@ class RPC:
) if self._fiat_converter else 0
profit_all_coin_sum = round(sum(profit_all_coin), 8)
profit_all_percent = round(mean(profit_all_ratio) * 100, 2) if profit_all_ratio else 0.0
profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum,
stake_currency,
@@ -304,10 +322,18 @@ class RPC:
num = float(len(durations) or 1)
return {
'profit_closed_coin': profit_closed_coin_sum,
'profit_closed_percent': profit_closed_percent,
'profit_closed_percent': round(profit_closed_ratio_mean * 100, 2), # DEPRECATED
'profit_closed_percent_mean': round(profit_closed_ratio_mean * 100, 2),
'profit_closed_ratio_mean': profit_closed_ratio_mean,
'profit_closed_percent_sum': round(profit_closed_ratio_sum * 100, 2),
'profit_closed_ratio_sum': profit_closed_ratio_sum,
'profit_closed_fiat': profit_closed_fiat,
'profit_all_coin': profit_all_coin_sum,
'profit_all_percent': profit_all_percent,
'profit_all_percent': round(profit_all_ratio_mean * 100, 2), # DEPRECATED
'profit_all_percent_mean': round(profit_all_ratio_mean * 100, 2),
'profit_all_ratio_mean': profit_all_ratio_mean,
'profit_all_percent_sum': round(profit_all_ratio_sum * 100, 2),
'profit_all_ratio_sum': profit_all_ratio_sum,
'profit_all_fiat': profit_all_fiat,
'trade_count': len(trades),
'closed_trade_count': len([t for t in trades if not t.is_open]),
@@ -393,9 +419,9 @@ class RPC:
return {'status': 'already stopped'}
def _rpc_reload_conf(self) -> Dict[str, str]:
""" Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF
def _rpc_reload_config(self) -> Dict[str, str]:
""" Handler for reload_config. """
self._freqtrade.state = State.RELOAD_CONFIG
return {'status': 'reloading config ...'}
def _rpc_stopbuy(self) -> Dict[str, str]:
@@ -406,7 +432,7 @@ class RPC:
# Set 'max_open_trades' to 0
self._freqtrade.config['max_open_trades'] = 0
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
"""
@@ -531,16 +557,26 @@ class RPC:
def _rpc_blacklist(self, add: List[str] = None) -> Dict:
""" Returns the currently active blacklist"""
errors = {}
if add:
stake_currency = self._freqtrade.config.get('stake_currency')
for pair in add:
if (self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency
and pair not in self._freqtrade.pairlists.blacklist):
self._freqtrade.pairlists.blacklist.append(pair)
if self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
if pair not in self._freqtrade.pairlists.blacklist:
self._freqtrade.pairlists.blacklist.append(pair)
else:
errors[pair] = {
'error_msg': f'Pair {pair} already in pairlist.'}
else:
errors[pair] = {
'error_msg': f"Pair {pair} does not match stake currency."
}
res = {'method': self._freqtrade.pairlists.name_list,
'length': len(self._freqtrade.pairlists.blacklist),
'blacklist': self._freqtrade.pairlists.blacklist,
'errors': errors,
}
return res

View File

@@ -72,7 +72,7 @@ class RPCManager:
minimal_roi = config['minimal_roi']
stoploss = config['stoploss']
trailing_stop = config['trailing_stop']
ticker_interval = config['ticker_interval']
timeframe = config['timeframe']
exchange_name = config['exchange']['name']
strategy_name = config.get('strategy', '')
self.send_msg({
@@ -81,7 +81,7 @@ class RPCManager:
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n'
f'*Ticker Interval:* `{ticker_interval}`\n'
f'*Timeframe:* `{timeframe}`\n'
f'*Strategy:* `{strategy_name}`'
})
self.send_msg({

View File

@@ -3,6 +3,7 @@
"""
This module manage Telegram communication
"""
import json
import logging
from typing import Any, Callable, Dict
@@ -19,7 +20,6 @@ logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
MAX_TELEGRAM_MESSAGE_LENGTH = 4096
@@ -29,6 +29,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
:param command_handler: Telegram CommandHandler
:return: decorated function
"""
def wrapper(self, *args, **kwargs):
""" Decorator logic """
update = kwargs.get('update') or args[0]
@@ -94,8 +95,8 @@ class Telegram(RPC):
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('show_config', self._show_config),
CommandHandler(['reload_config', 'reload_conf'], self._reload_config),
CommandHandler(['show_config', 'show_conf'], self._show_config),
CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist),
@@ -133,7 +134,7 @@ class Telegram(RPC):
else:
msg['stake_amount_fiat'] = 0
message = ("*{exchange}:* Buying {pair}\n"
message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{limit:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
@@ -144,7 +145,8 @@ class Telegram(RPC):
message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg)
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling Open Buy Order for {pair}".format(**msg))
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8)
@@ -153,7 +155,9 @@ class Telegram(RPC):
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
message = ("*{exchange}:* Selling {pair}\n"
msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair}\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
@@ -165,21 +169,21 @@ class Telegram(RPC):
# Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._fiat_converter):
and self._fiat_converter):
msg['profit_fiat'] = self._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = ("*{exchange}:* Cancelling Open Sell Order "
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair}. Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
message = '*Warning:* `{status}`'.format(**msg)
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION:
message = '{status}'.format(**msg)
@@ -189,6 +193,20 @@ class Telegram(RPC):
self._send_msg(message)
def _get_sell_emoji(self, msg):
"""
Get emoji for sell-side
"""
if float(msg['profit_percent']) >= 5.0:
return "\N{ROCKET}"
elif float(msg['profit_percent']) >= 0.0:
return "\N{EIGHT SPOKED ASTERISK}"
elif msg['sell_reason'] == "stop_loss":
return"\N{WARNING SIGN}"
else:
return "\N{CROSS MARK}"
@authorized_only
def _status(self, update: Update, context: CallbackContext) -> None:
"""
@@ -222,8 +240,8 @@ class Telegram(RPC):
# Adding initial stoploss only if it is different from stoploss
"*Initial Stoploss:* `{initial_stop_loss:.8f}` " +
("`({initial_stop_loss_pct:.2f}%)`") if (
r['stop_loss'] != r['initial_stop_loss']
and r['initial_stop_loss_pct'] is not None) else "",
r['stop_loss'] != r['initial_stop_loss']
and r['initial_stop_loss_pct'] is not None) else "",
# Adding stoploss and stoploss percentage only if it is not None
"*Stoploss:* `{stop_loss:.8f}` " +
@@ -315,10 +333,12 @@ class Telegram(RPC):
stake_cur,
fiat_disp_cur)
profit_closed_coin = stats['profit_closed_coin']
profit_closed_percent = stats['profit_closed_percent']
profit_closed_percent_mean = stats['profit_closed_percent_mean']
profit_closed_percent_sum = stats['profit_closed_percent_sum']
profit_closed_fiat = stats['profit_closed_fiat']
profit_all_coin = stats['profit_all_coin']
profit_all_percent = stats['profit_all_percent']
profit_all_percent_mean = stats['profit_all_percent_mean']
profit_all_percent_sum = stats['profit_all_percent_sum']
profit_all_fiat = stats['profit_all_fiat']
trade_count = stats['trade_count']
first_trade_date = stats['first_trade_date']
@@ -333,13 +353,16 @@ class Telegram(RPC):
if stats['closed_trade_count'] > 0:
markdown_msg = ("*ROI:* Closed trades\n"
f"∙ `{profit_closed_coin:.8f} {stake_cur} "
f"({profit_closed_percent:.2f}%)`\n"
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")
else:
markdown_msg = "`No closed trade` \n"
markdown_msg += (f"*ROI:* All trades\n"
f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n"
f"∙ `{profit_all_coin:.8f} {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"*Total Trade Count:* `{trade_count}`\n"
f"*First Trade opened:* `{first_trade_date}`\n"
@@ -363,14 +386,14 @@ class Telegram(RPC):
"This mode is still experimental!\n"
"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)
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)
else:
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
@@ -381,9 +404,9 @@ class Telegram(RPC):
else:
output += curr_output
output += "\n*Estimated Value*:\n" \
"\t`{stake}: {total: .8f}`\n" \
"\t`{symbol}: {value: .2f}`\n".format(**result)
output += ("\n*Estimated Value*:\n"
"\t`{stake}: {total: .8f}`\n"
"\t`{symbol}: {value: .2f}`\n").format(**result)
self._send_msg(output)
except RPCException as e:
self._send_msg(str(e))
@@ -413,15 +436,15 @@ class Telegram(RPC):
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _reload_conf(self, update: Update, context: CallbackContext) -> None:
def _reload_config(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /reload_conf.
Handler for /reload_config.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_reload_conf()
msg = self._rpc_reload_config()
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
@@ -539,6 +562,11 @@ class Telegram(RPC):
try:
blacklist = self._rpc_blacklist(context.args)
errmsgs = []
for pair, error in blacklist['errors'].items():
errmsgs.append(f"Error adding `{pair}` to blacklist: `{error['error_msg']}`")
if errmsgs:
self._send_msg('\n'.join(errmsgs))
message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`"
@@ -571,32 +599,32 @@ class Telegram(RPC):
:param update: message update
:return: None
"""
forcebuy_text = "*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. " \
"Optionally takes a rate at which to buy.` \n"
message = "*/start:* `Starts the trader`\n" \
"*/stop:* `Stops the trader`\n" \
"*/status [table]:* `Lists all open trades`\n" \
" *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" \
"*/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 '' }" \
"*/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`" \
"\n" \
"*/balance:* `Show account balance per currency`\n" \
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" \
"*/reload_conf:* `Reload configuration file` \n" \
"*/show_config:* `Show running configuration` \n" \
"*/whitelist:* `Show current whitelist` \n" \
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " \
"to the blacklist.` \n" \
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n" \
"*/help:* `This help message`\n" \
"*/version:* `Show version`"
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
"Optionally takes a rate at which to buy.` \n")
message = ("*/start:* `Starts the trader`\n"
"*/stop:* `Stops the trader`\n"
"*/status [table]:* `Lists all open trades`\n"
" *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"
"*/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 ''}"
"*/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`"
"\n"
"*/balance:* `Show account balance per currency`\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/reload_config:* `Reload configuration file` \n"
"*/show_config:* `Show running configuration` \n"
"*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
"to the blacklist.` \n"
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
"*/help:* `This help message`\n"
"*/version:* `Show version`")
self._send_msg(message)
@@ -638,8 +666,10 @@ class Telegram(RPC):
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Max open Trades:* `{val['max_open_trades']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
f"{sl_info}"
f"*Ticker Interval:* `{val['ticker_interval']}`\n"
f"*Timeframe:* `{val['timeframe']}`\n"
f"*Strategy:* `{val['strategy']}`\n"
f"*Current state:* `{val['state']}`"
)

View File

@@ -12,7 +12,7 @@ class State(Enum):
"""
RUNNING = 1
STOPPED = 2
RELOAD_CONF = 3
RELOAD_CONFIG = 3
def __str__(self):
return f"{self.name.lower()}"

View File

@@ -62,7 +62,7 @@ class IStrategy(ABC):
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy
timeframe -> str: value of the timeframe (ticker interval) to use with the strategy
"""
# Strategy interface version
# Default to version 2
@@ -85,8 +85,9 @@ class IStrategy(ABC):
trailing_stop_positive_offset: float = 0.0
trailing_only_offset_is_reached = False
# associated ticker interval
ticker_interval: str
# associated timeframe
ticker_interval: str # DEPRECATED
timeframe: str
# Optional order types
order_types: Dict = {

View File

@@ -4,7 +4,7 @@
"stake_amount": {{ stake_amount }},
"tradable_balance_ratio": 0.99,
"fiat_display_currency": "{{ fiat_display_currency }}",
"ticker_interval": "{{ ticker_interval }}",
"timeframe": "{{ timeframe }}",
"dry_run": {{ dry_run | lower }},
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
@@ -53,6 +53,15 @@
"token": "{{ telegram_token }}",
"chat_id": "{{ telegram_chat_id }}"
},
"api_server": {
"enabled": false,
"listen_ip_address": "127.0.0.1",
"listen_port": 8080,
"verbosity": "info",
"jwt_secret_key": "somethingrandom",
"username": "",
"password": ""
},
"initial_state": "running",
"forcebuy_enable": false,
"internals": {

View File

@@ -51,8 +51,8 @@ class {{ strategy }}(IStrategy):
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Optimal ticker interval for the strategy.
ticker_interval = '5m'
# Optimal timeframe for the strategy.
timeframe = '5m'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False

View File

@@ -53,7 +53,7 @@ class SampleStrategy(IStrategy):
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Optimal ticker interval for the strategy.
ticker_interval = '5m'
timeframe = '5m'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False

View File

@@ -71,7 +71,7 @@ class Worker:
state = None
while True:
state = self._worker(old_state=state)
if state == State.RELOAD_CONF:
if state == State.RELOAD_CONFIG:
self._reconfigure()
def _worker(self, old_state: Optional[State]) -> State: