Merge branch 'develop' into feat/short
This commit is contained in:
@@ -160,6 +160,7 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'buy': {'type': 'number', 'minimum': 1},
|
||||
'sell': {'type': 'number', 'minimum': 1},
|
||||
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
|
||||
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
|
||||
}
|
||||
},
|
||||
@@ -210,7 +211,10 @@ CONF_SCHEMA = {
|
||||
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'emergencysell': {
|
||||
'type': 'string',
|
||||
'enum': ORDERTYPE_POSSIBILITIES,
|
||||
'default': 'market'},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss_on_exchange': {'type': 'boolean'},
|
||||
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||
|
@@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
|
||||
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
|
||||
if len_before != len_after:
|
||||
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
|
||||
f" - {round(pct_missing * 100, 2)}%")
|
||||
f" - {pct_missing:.2%}")
|
||||
if pct_missing > 0.01:
|
||||
logger.info(message)
|
||||
else:
|
||||
|
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
class FreqtradeException(Exception):
|
||||
"""
|
||||
Freqtrade base exception. Handled at the outermost level.
|
||||
|
@@ -201,8 +201,9 @@ class Binance(Exchange):
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
since_ms: int, is_new_pair: bool = False,
|
||||
raise_: bool = False
|
||||
) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
|
||||
Does not work for other exchanges, which don't return the earliest data when called with "0"
|
||||
@@ -215,7 +216,8 @@ class Binance(Exchange):
|
||||
logger.info(f"Candle-data for {pair} available starting with "
|
||||
f"{arrow.get(since_ms // 1000).isoformat()}.")
|
||||
return await super()._async_get_historic_ohlcv(
|
||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair)
|
||||
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
|
||||
raise_=raise_)
|
||||
|
||||
def funding_fee_cutoff(self, open_date: datetime):
|
||||
"""
|
||||
|
@@ -81,9 +81,16 @@ def retrier_async(f):
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
if isinstance(ex, DDosProtection):
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
if "kucoin" in str(ex) and "429000" in str(ex):
|
||||
# Temporary fix for 429000 error on kucoin
|
||||
# see https://github.com/freqtrade/freqtrade/issues/5700 for details.
|
||||
logger.warning(
|
||||
f"Kucoin 429 error, avoid triggering DDosProtection backoff delay. "
|
||||
f"{count} tries left before giving up")
|
||||
else:
|
||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||
await asyncio.sleep(backoff_delay)
|
||||
return await wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
|
@@ -168,9 +168,10 @@ class Exchange:
|
||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||
self.validate_ordertypes(config.get('order_types', {}))
|
||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
||||
config.get('timeframe', ''))
|
||||
self.required_candle_call_count = self.validate_required_startup_candles(
|
||||
config.get('startup_candle_count', 0), config.get('timeframe', ''))
|
||||
self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral)
|
||||
|
||||
# Converts the interval provided in minutes in config to seconds
|
||||
self.markets_refresh_interval: int = exchange_config.get(
|
||||
"markets_refresh_interval", 60) * 60
|
||||
@@ -523,16 +524,29 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Time in force policies are not supported for {self.name} yet.')
|
||||
|
||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> None:
|
||||
def validate_required_startup_candles(self, startup_candles: int, timeframe: str) -> int:
|
||||
"""
|
||||
Checks if required startup_candles is more than ohlcv_candle_limit().
|
||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||
"""
|
||||
candle_limit = self.ohlcv_candle_limit(timeframe)
|
||||
if startup_candles + 5 > candle_limit:
|
||||
# Require one more candle - to account for the still open candle.
|
||||
candle_count = startup_candles + 1
|
||||
# Allow 5 calls to the exchange per pair
|
||||
required_candle_call_count = int(
|
||||
(candle_count / candle_limit) + (0 if candle_count % candle_limit == 0 else 1))
|
||||
|
||||
if required_candle_call_count > 5:
|
||||
# Only allow 5 calls per pair to somewhat limit the impact
|
||||
raise OperationalException(
|
||||
f"This strategy requires {startup_candles} candles to start. "
|
||||
f"{self.name} only provides {candle_limit - 5} for {timeframe}.")
|
||||
f"This strategy requires {startup_candles} candles to start, which is more than 5x "
|
||||
f"the amount of candles {self.name} provides for {timeframe}.")
|
||||
|
||||
if required_candle_call_count > 1:
|
||||
logger.warning(f"Using {required_candle_call_count} calls to get OHLCV. "
|
||||
f"This can result in slower operations for the bot. Please check "
|
||||
f"if you really need {startup_candles} candles for your strategy")
|
||||
return required_candle_call_count
|
||||
|
||||
def validate_trading_mode_and_collateral(
|
||||
self,
|
||||
@@ -1306,9 +1320,11 @@ class Exchange:
|
||||
:param since_ms: Timestamp in milliseconds to get history from
|
||||
:return: List with candle (OHLCV) data
|
||||
"""
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
pair, timeframe, data = asyncio.get_event_loop().run_until_complete(
|
||||
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
|
||||
since_ms=since_ms, is_new_pair=is_new_pair))
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
|
||||
def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
|
||||
since_ms: int) -> DataFrame:
|
||||
@@ -1324,8 +1340,9 @@ class Exchange:
|
||||
drop_incomplete=self._ohlcv_partial_candle)
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool
|
||||
) -> List:
|
||||
since_ms: int, is_new_pair: bool = False,
|
||||
raise_: bool = False
|
||||
) -> Tuple[str, str, List]:
|
||||
"""
|
||||
Download historic ohlcv
|
||||
:param is_new_pair: used by binance subclass to allow "fast" new pair downloading
|
||||
@@ -1349,15 +1366,17 @@ class Exchange:
|
||||
for res in results:
|
||||
if isinstance(res, Exception):
|
||||
logger.warning("Async code raised an exception: %s", res.__class__.__name__)
|
||||
if raise_:
|
||||
raise
|
||||
continue
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
else:
|
||||
# Deconstruct tuple if it's not an exception
|
||||
p, _, new_data = res
|
||||
if p == pair:
|
||||
data.extend(new_data)
|
||||
# Sort data again after extending the result - above calls return in "async order"
|
||||
data = sorted(data, key=lambda x: x[0])
|
||||
logger.info(f"Downloaded data for {pair} with length {len(data)}.")
|
||||
return data
|
||||
return pair, timeframe, data
|
||||
|
||||
def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *,
|
||||
since_ms: Optional[int] = None, cache: bool = True
|
||||
@@ -1377,10 +1396,22 @@ class Exchange:
|
||||
cached_pairs = []
|
||||
# Gather coroutines to run
|
||||
for pair, timeframe in set(pair_list):
|
||||
if (((pair, timeframe) not in self._klines)
|
||||
if ((pair, timeframe) not in self._klines
|
||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||
input_coroutines.append(self._async_get_candle_history(pair, timeframe,
|
||||
since_ms=since_ms))
|
||||
if not since_ms and self.required_candle_call_count > 1:
|
||||
# Multiple calls for one pair - to get more history
|
||||
one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe)
|
||||
move_to = one_call * self.required_candle_call_count
|
||||
now = timeframe_to_next_date(timeframe)
|
||||
since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000)
|
||||
|
||||
if since_ms:
|
||||
input_coroutines.append(self._async_get_historic_ohlcv(
|
||||
pair, timeframe, since_ms=since_ms, raise_=True))
|
||||
else:
|
||||
# One call ... "regular" refresh
|
||||
input_coroutines.append(self._async_get_candle_history(
|
||||
pair, timeframe, since_ms=since_ms))
|
||||
else:
|
||||
logger.debug(
|
||||
"Using cached candle (OHLCV) data for pair %s, timeframe %s ...",
|
||||
|
@@ -1,4 +1,4 @@
|
||||
""" Kucoin exchange subclass """
|
||||
"""Kucoin exchange subclass."""
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
@@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Kucoin(Exchange):
|
||||
"""
|
||||
Kucoin exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""Kucoin exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
|
||||
Please note that this exchange is not included in the list of exchanges
|
||||
officially supported by the Freqtrade development team. So some features
|
||||
|
@@ -9,9 +9,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Okex(Exchange):
|
||||
"""
|
||||
Okex exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""Okex exchange class.
|
||||
|
||||
Contains adjustments needed for Freqtrade to work with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
|
@@ -219,19 +219,20 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
def check_for_open_trades(self):
|
||||
"""
|
||||
Notify the user when the bot is stopped
|
||||
Notify the user when the bot is stopped (not reloaded)
|
||||
and there are still open trades active.
|
||||
"""
|
||||
open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all()
|
||||
|
||||
if len(open_trades) != 0:
|
||||
if len(open_trades) != 0 and self.state != State.RELOAD_CONFIG:
|
||||
msg = {
|
||||
'type': RPCMessageType.WARNING,
|
||||
'status': f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
|
||||
'status':
|
||||
f"{len(open_trades)} open trades active.\n\n"
|
||||
f"Handle these trades manually on {self.exchange.name}, "
|
||||
f"or '/start' the bot again and use '/stopbuy' "
|
||||
f"to handle open trades gracefully. \n"
|
||||
f"{'Note: Trades are simulated (dry run).' if self.config['dry_run'] else ''}",
|
||||
}
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
@@ -622,7 +623,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
side='short' if is_short else 'long'
|
||||
)
|
||||
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
if not stake_amount:
|
||||
return False
|
||||
@@ -1075,6 +1076,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
side = trade.enter_side if is_entering else trade.exit_side
|
||||
timed_out = self._check_timed_out(side, order)
|
||||
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
|
||||
if not_closed and (fully_cancelled or timed_out or (
|
||||
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
|
||||
@@ -1087,6 +1089,13 @@ class FreqtradeBot(LoggingMixin):
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
else:
|
||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
canceled_count = trade.get_exit_order_count()
|
||||
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||
f'timed out {max_timeouts} times.')
|
||||
self.execute_trade_exit(
|
||||
trade, order.get('price'),
|
||||
sell_reason=SellCheckTuple(sell_type=SellType.EMERGENCY_SELL))
|
||||
|
||||
def cancel_all_open_orders(self) -> None:
|
||||
"""
|
||||
@@ -1468,7 +1477,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
if self.exchange.check_order_canceled_empty(order):
|
||||
# Trade has been cancelled on exchange
|
||||
# Handling of this will happen in check_handle_timeout.
|
||||
# Handling of this will happen in check_handle_timedout.
|
||||
return True
|
||||
|
||||
# Try update amount (binance-fix)
|
||||
@@ -1559,14 +1568,17 @@ class FreqtradeBot(LoggingMixin):
|
||||
return self.apply_fee_conditional(trade, trade_base_currency,
|
||||
amount=order_amount, fee_abs=fee_cost)
|
||||
return order_amount
|
||||
return self.fee_detection_from_trades(trade, order, order_amount)
|
||||
return self.fee_detection_from_trades(trade, order, order_amount, order.get('trades', []))
|
||||
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float:
|
||||
def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float,
|
||||
trades: List) -> float:
|
||||
"""
|
||||
fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee.
|
||||
fee-detection fallback to Trades.
|
||||
Either uses provided trades list or the result of fetch_my_trades to get correct fee.
|
||||
"""
|
||||
trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order),
|
||||
trade.pair, trade.open_date)
|
||||
if not trades:
|
||||
trades = self.exchange.get_trades_for_order(
|
||||
self.exchange.get_order_id_conditional(order), trade.pair, trade.open_date)
|
||||
|
||||
if len(trades) == 0:
|
||||
logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade)
|
||||
|
@@ -379,16 +379,6 @@ class Backtesting:
|
||||
|
||||
if sell.sell_flag:
|
||||
trade.close_date = sell_candle_time
|
||||
trade.sell_reason = sell.sell_reason
|
||||
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# sell_row has the length for an exit tag column
|
||||
if(
|
||||
len(sell_row) > EXIT_TAG_IDX
|
||||
and sell_row[EXIT_TAG_IDX] is not None
|
||||
and len(sell_row[EXIT_TAG_IDX]) > 0
|
||||
):
|
||||
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||
|
||||
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
|
||||
closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
|
||||
@@ -403,6 +393,17 @@ class Backtesting:
|
||||
current_time=sell_candle_time):
|
||||
return None
|
||||
|
||||
trade.sell_reason = sell.sell_reason
|
||||
|
||||
# Checks and adds an exit tag, after checking that the length of the
|
||||
# sell_row has the length for an exit tag column
|
||||
if(
|
||||
len(sell_row) > EXIT_TAG_IDX
|
||||
and sell_row[EXIT_TAG_IDX] is not None
|
||||
and len(sell_row[EXIT_TAG_IDX]) > 0
|
||||
):
|
||||
trade.sell_reason = sell_row[EXIT_TAG_IDX]
|
||||
|
||||
trade.close(closerate, show_msg=False)
|
||||
return trade
|
||||
|
||||
@@ -451,7 +452,7 @@ class Backtesting:
|
||||
pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
|
||||
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
|
||||
side=direction)
|
||||
stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
|
||||
|
||||
if not stake_amount:
|
||||
return None
|
||||
|
@@ -284,10 +284,10 @@ class HyperoptTools():
|
||||
return (f"{results_metrics['total_trades']:6d} trades. "
|
||||
f"{results_metrics['wins']}/{results_metrics['draws']}"
|
||||
f"/{results_metrics['losses']} Wins/Draws/Losses. "
|
||||
f"Avg profit {results_metrics['profit_mean'] * 100: 6.2f}%. "
|
||||
f"Median profit {results_metrics['profit_median'] * 100: 6.2f}%. "
|
||||
f"Total profit {results_metrics['profit_total_abs']: 11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total'] * 100: 7.2f}%). "
|
||||
f"Avg profit {results_metrics['profit_mean']:7.2%}. "
|
||||
f"Median profit {results_metrics['profit_median']:7.2%}. "
|
||||
f"Total profit {results_metrics['profit_total_abs']:11.8f} {stake_currency} "
|
||||
f"({results_metrics['profit_total']:8.2%}). "
|
||||
f"Avg duration {results_metrics['holding_avg']} min."
|
||||
)
|
||||
|
||||
|
@@ -725,22 +725,22 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])),
|
||||
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"),
|
||||
('Total profit %', f"{strat_results['profit_total']:.2%}"),
|
||||
('Trades per day', strat_results['trades_per_day']),
|
||||
('Avg. daily profit %',
|
||||
f"{round(strat_results['profit_total'] / strat_results['backtest_days'] * 100, 2)}%"),
|
||||
f"{(strat_results['profit_total'] / strat_results['backtest_days']):.2%}"),
|
||||
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||
strat_results['stake_currency'])),
|
||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||
strat_results['stake_currency'])),
|
||||
('', ''), # Empty line to improve readability
|
||||
('Best Pair', f"{strat_results['best_pair']['key']} "
|
||||
f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"),
|
||||
f"{strat_results['best_pair']['profit_sum']:.2%}"),
|
||||
('Worst Pair', f"{strat_results['worst_pair']['key']} "
|
||||
f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"),
|
||||
('Best trade', f"{best_trade['pair']} {round(best_trade['profit_ratio'] * 100, 2)}%"),
|
||||
f"{strat_results['worst_pair']['profit_sum']:.2%}"),
|
||||
('Best trade', f"{best_trade['pair']} {best_trade['profit_ratio']:.2%}"),
|
||||
('Worst trade', f"{worst_trade['pair']} "
|
||||
f"{round(worst_trade['profit_ratio'] * 100, 2)}%"),
|
||||
f"{worst_trade['profit_ratio']:.2%}"),
|
||||
|
||||
('Best day', round_coin_value(strat_results['backtest_best_day_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
@@ -758,7 +758,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
('Max balance', round_coin_value(strat_results['csum_max'],
|
||||
strat_results['stake_currency'])),
|
||||
|
||||
('Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"),
|
||||
('Drawdown', f"{strat_results['max_drawdown']:.2%}"),
|
||||
('Drawdown', round_coin_value(strat_results['max_drawdown_abs'],
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown high', round_coin_value(strat_results['max_drawdown_high'],
|
||||
@@ -767,7 +767,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
||||
strat_results['stake_currency'])),
|
||||
('Drawdown Start', strat_results['drawdown_start']),
|
||||
('Drawdown End', strat_results['drawdown_end']),
|
||||
('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"),
|
||||
('Market change', f"{strat_results['market_change']:.2%}"),
|
||||
]
|
||||
|
||||
return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl")
|
||||
@@ -864,5 +864,5 @@ def show_sorted_pairlist(config: Dict, backtest_stats: Dict):
|
||||
print(f"Pairs for Strategy {strategy}: \n[")
|
||||
for result in results['results_per_pair']:
|
||||
if result["key"] != 'TOTAL':
|
||||
print(f'"{result["key"]}", // {round(result["profit_mean_pct"], 2)}%')
|
||||
print(f'"{result["key"]}", // {result["profit_mean"]:.2%}')
|
||||
print("]")
|
||||
|
@@ -200,6 +200,8 @@ class Order(_DECL_BASE):
|
||||
@staticmethod
|
||||
def get_open_orders() -> List['Order']:
|
||||
"""
|
||||
Retrieve open orders from the database
|
||||
:return: List of open orders
|
||||
"""
|
||||
return Order.query.filter(Order.ft_is_open.is_(True)).all()
|
||||
|
||||
@@ -620,6 +622,13 @@ class LocalTrade():
|
||||
def update_order(self, order: Dict) -> None:
|
||||
Order.update_orders(self.orders, order)
|
||||
|
||||
def get_exit_order_count(self) -> int:
|
||||
"""
|
||||
Get amount of failed exiting orders
|
||||
assumes full exits.
|
||||
"""
|
||||
return len([o for o in self.orders if o.ft_order_side == 'sell'])
|
||||
|
||||
def _calc_open_trade_value(self) -> float:
|
||||
"""
|
||||
Calculate the open_rate including open_fee.
|
||||
@@ -1002,7 +1011,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return Trade.query
|
||||
|
||||
@staticmethod
|
||||
def get_open_order_trades():
|
||||
def get_open_order_trades() -> List['Trade']:
|
||||
"""
|
||||
Returns all open trades
|
||||
NOTE: Not supported in Backtesting.
|
||||
@@ -1190,6 +1199,7 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
if not any(item["mix_tag"] == mix_tag for item in return_list):
|
||||
return_list.append({'mix_tag': mix_tag,
|
||||
'profit': profit,
|
||||
'profit_pct': round(profit * 100, 2),
|
||||
'profit_abs': profit_abs,
|
||||
'count': count})
|
||||
else:
|
||||
@@ -1198,11 +1208,11 @@ class Trade(_DECL_BASE, LocalTrade):
|
||||
return_list[i] = {
|
||||
'mix_tag': mix_tag,
|
||||
'profit': profit + return_list[i]["profit"],
|
||||
'profit_pct': round(profit + return_list[i]["profit"] * 100, 2),
|
||||
'profit_abs': profit_abs + return_list[i]["profit_abs"],
|
||||
'count': 1 + return_list[i]["count"]}
|
||||
i += 1
|
||||
|
||||
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in return_list]
|
||||
return return_list
|
||||
|
||||
@staticmethod
|
||||
|
@@ -169,8 +169,8 @@ def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame,
|
||||
df_comb.loc[timeframe_to_prev_date(timeframe, lowdate), 'cum_profit'],
|
||||
],
|
||||
mode='markers',
|
||||
name=f"Max drawdown {max_drawdown * 100:.2f}%",
|
||||
text=f"Max drawdown {max_drawdown * 100:.2f}%",
|
||||
name=f"Max drawdown {max_drawdown:.2%}",
|
||||
text=f"Max drawdown {max_drawdown:.2%}",
|
||||
marker=dict(
|
||||
symbol='square-open',
|
||||
size=9,
|
||||
@@ -192,7 +192,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['profit_ratio'] * 100, 1)}%, "
|
||||
trades['desc'] = trades.apply(lambda row: f"{row['profit_ratio']:.2%}, "
|
||||
f"{row['sell_reason']}, "
|
||||
f"{row['trade_duration']} min",
|
||||
axis=1)
|
||||
|
@@ -50,7 +50,7 @@ class PriceFilter(IPairList):
|
||||
"""
|
||||
active_price_filters = []
|
||||
if self._low_price_ratio != 0:
|
||||
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
|
||||
active_price_filters.append(f"below {self._low_price_ratio:.1%}")
|
||||
if self._min_price != 0:
|
||||
active_price_filters.append(f"below {self._min_price:.8f}")
|
||||
if self._max_price != 0:
|
||||
@@ -82,7 +82,7 @@ class PriceFilter(IPairList):
|
||||
changeperc = compare / ticker['last']
|
||||
if changeperc > self._low_price_ratio:
|
||||
self.log_once(f"Removed {pair} from whitelist, "
|
||||
f"because 1 unit is {changeperc * 100:.3f}%", logger.info)
|
||||
f"because 1 unit is {changeperc:.3%}", logger.info)
|
||||
return False
|
||||
|
||||
# Perform low_amount check
|
||||
|
@@ -34,7 +34,7 @@ class SpreadFilter(IPairList):
|
||||
Short whitelist method description - used for startup-messages
|
||||
"""
|
||||
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||
f"{self._max_spread_ratio * 100}%.")
|
||||
f"{self._max_spread_ratio:.2%}.")
|
||||
|
||||
def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
@@ -47,7 +47,7 @@ class SpreadFilter(IPairList):
|
||||
spread = 1 - ticker['bid'] / ticker['ask']
|
||||
if spread > self._max_spread_ratio:
|
||||
self.log_once(f"Removed {pair} from whitelist, because spread "
|
||||
f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%",
|
||||
f"{spread * 100:.3%} > {self._max_spread_ratio:.3%}",
|
||||
logger.info)
|
||||
return False
|
||||
else:
|
||||
|
@@ -95,6 +95,7 @@ class Profit(BaseModel):
|
||||
avg_duration: str
|
||||
best_pair: str
|
||||
best_rate: float
|
||||
best_pair_profit_ratio: float
|
||||
winning_trades: int
|
||||
losing_trades: int
|
||||
|
||||
@@ -123,7 +124,26 @@ class Daily(BaseModel):
|
||||
stake_currency: str
|
||||
|
||||
|
||||
class UnfilledTimeout(BaseModel):
|
||||
buy: int
|
||||
sell: int
|
||||
unit: str
|
||||
exit_timeout_count: Optional[int]
|
||||
|
||||
|
||||
class OrderTypes(BaseModel):
|
||||
buy: str
|
||||
sell: str
|
||||
emergencysell: Optional[str]
|
||||
forcesell: Optional[str]
|
||||
forcebuy: Optional[str]
|
||||
stoploss: str
|
||||
stoploss_on_exchange: bool
|
||||
stoploss_on_exchange_interval: Optional[int]
|
||||
|
||||
|
||||
class ShowConfig(BaseModel):
|
||||
version: str
|
||||
dry_run: bool
|
||||
stake_currency: str
|
||||
stake_amount: Union[float, str]
|
||||
@@ -136,6 +156,8 @@ class ShowConfig(BaseModel):
|
||||
trailing_stop_positive: Optional[float]
|
||||
trailing_stop_positive_offset: Optional[float]
|
||||
trailing_only_offset_is_reached: Optional[bool]
|
||||
unfilledtimeout: UnfilledTimeout
|
||||
order_types: OrderTypes
|
||||
use_custom_stoploss: Optional[bool]
|
||||
timeframe: Optional[str]
|
||||
timeframe_ms: int
|
||||
|
@@ -9,9 +9,11 @@ from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import arrow
|
||||
import psutil
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from numpy import NAN, inf, int64, mean
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import __version__
|
||||
from freqtrade.configuration.timerange import TimeRange
|
||||
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
||||
from freqtrade.data.history import load_data
|
||||
@@ -104,6 +106,7 @@ class RPC:
|
||||
information via rpc.
|
||||
"""
|
||||
val = {
|
||||
'version': __version__,
|
||||
'dry_run': config['dry_run'],
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
@@ -117,7 +120,9 @@ 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'),
|
||||
'unfilledtimeout': config.get('unfilledtimeout'),
|
||||
'use_custom_stoploss': config.get('use_custom_stoploss'),
|
||||
'order_types': config.get('order_types'),
|
||||
'bot_name': config.get('bot_name', 'freqtrade'),
|
||||
'timeframe': config.get('timeframe'),
|
||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']
|
||||
@@ -222,9 +227,8 @@ class RPC:
|
||||
trade.pair, refresh=False, side=closing_side)
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
||||
trade_profit = trade.calc_profit(current_rate)
|
||||
profit_str = f'{trade_percent:.2f}%'
|
||||
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
||||
if self._fiat_converter:
|
||||
fiat_profit = self._fiat_converter.convert_amount(
|
||||
trade_profit,
|
||||
@@ -253,7 +257,7 @@ class RPC:
|
||||
def _rpc_daily_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
today = datetime.utcnow().date()
|
||||
today = datetime.now(timezone.utc).date()
|
||||
profit_days: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
@@ -292,6 +296,91 @@ class RPC:
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_weekly_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
today = datetime.now(timezone.utc).date()
|
||||
first_iso_day_of_week = today - timedelta(days=today.weekday()) # Monday
|
||||
profit_weeks: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
for week in range(0, timescale):
|
||||
profitweek = first_iso_day_of_week - timedelta(weeks=week)
|
||||
trades = Trade.get_trades(trade_filter=[
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitweek,
|
||||
Trade.close_date < (profitweek + timedelta(weeks=1))
|
||||
]).order_by(Trade.close_date).all()
|
||||
curweekprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
profit_weeks[profitweek] = {
|
||||
'amount': curweekprofit,
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
data = [
|
||||
{
|
||||
'date': key,
|
||||
'abs_profit': value["amount"],
|
||||
'fiat_value': self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
) if self._fiat_converter else 0,
|
||||
'trade_count': value["trades"],
|
||||
}
|
||||
for key, value in profit_weeks.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_display_currency': fiat_display_currency,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_monthly_profit(
|
||||
self, timescale: int,
|
||||
stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||
first_day_of_month = datetime.now(timezone.utc).date().replace(day=1)
|
||||
profit_months: Dict[date, Dict] = {}
|
||||
|
||||
if not (isinstance(timescale, int) and timescale > 0):
|
||||
raise RPCException('timescale must be an integer greater than 0')
|
||||
|
||||
for month in range(0, timescale):
|
||||
profitmonth = first_day_of_month - relativedelta(months=month)
|
||||
trades = Trade.get_trades(trade_filter=[
|
||||
Trade.is_open.is_(False),
|
||||
Trade.close_date >= profitmonth,
|
||||
Trade.close_date < (profitmonth + relativedelta(months=1))
|
||||
]).order_by(Trade.close_date).all()
|
||||
curmonthprofit = sum(
|
||||
trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None)
|
||||
profit_months[profitmonth] = {
|
||||
'amount': curmonthprofit,
|
||||
'trades': len(trades)
|
||||
}
|
||||
|
||||
data = [
|
||||
{
|
||||
'date': f"{key.year}-{key.month:02d}",
|
||||
'abs_profit': value["amount"],
|
||||
'fiat_value': self._fiat_converter.convert_amount(
|
||||
value['amount'],
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
) if self._fiat_converter else 0,
|
||||
'trade_count': value["trades"],
|
||||
}
|
||||
for key, value in profit_months.items()
|
||||
]
|
||||
return {
|
||||
'stake_currency': stake_currency,
|
||||
'fiat_display_currency': fiat_display_currency,
|
||||
'data': data
|
||||
}
|
||||
|
||||
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
|
||||
""" Returns the X last trades """
|
||||
order_by = Trade.id if order_by_id else Trade.close_date.desc()
|
||||
@@ -448,7 +537,8 @@ class RPC:
|
||||
'latest_trade_timestamp': int(last_date.timestamp() * 1000) if last_date else 0,
|
||||
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
|
||||
'best_pair': best_pair[0] if best_pair else '',
|
||||
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,
|
||||
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, # Deprecated
|
||||
'best_pair_profit_ratio': best_pair[1] if best_pair else 0,
|
||||
'winning_trades': winning_trades,
|
||||
'losing_trades': losing_trades,
|
||||
}
|
||||
@@ -824,15 +914,15 @@ class RPC:
|
||||
if has_content:
|
||||
|
||||
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
||||
# Move open to separate column when signal for easy plotting
|
||||
# Move signal close to separate column when signal for easy plotting
|
||||
if 'buy' in dataframe.columns:
|
||||
buy_mask = (dataframe['buy'] == 1)
|
||||
buy_signals = int(buy_mask.sum())
|
||||
dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open']
|
||||
dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
|
||||
if 'sell' in dataframe.columns:
|
||||
sell_mask = (dataframe['sell'] == 1)
|
||||
sell_signals = int(sell_mask.sum())
|
||||
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
|
||||
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
|
||||
dataframe = dataframe.replace([inf, -inf], NAN)
|
||||
dataframe = dataframe.replace({NAN: None})
|
||||
|
||||
|
@@ -159,6 +159,8 @@ class Telegram(RPCHandler):
|
||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||
CommandHandler('stats', self._stats),
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('weekly', self._weekly),
|
||||
CommandHandler('monthly', self._monthly),
|
||||
CommandHandler('count', self._count),
|
||||
CommandHandler('locks', self._locks),
|
||||
CommandHandler(['unlock', 'delete_locks'], self._delete_locks),
|
||||
@@ -175,6 +177,8 @@ class Telegram(RPCHandler):
|
||||
callbacks = [
|
||||
CallbackQueryHandler(self._status_table, pattern='update_status_table'),
|
||||
CallbackQueryHandler(self._daily, pattern='update_daily'),
|
||||
CallbackQueryHandler(self._weekly, pattern='update_weekly'),
|
||||
CallbackQueryHandler(self._monthly, pattern='update_monthly'),
|
||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||
@@ -215,26 +219,28 @@ class Telegram(RPCHandler):
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
is_fill = msg['type'] == RPCMessageType.BUY_FILL
|
||||
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
||||
|
||||
content = []
|
||||
content.append(
|
||||
f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
|
||||
message = (
|
||||
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
||||
f" (#{msg['trade_id']})\n"
|
||||
)
|
||||
if msg.get('buy_tag', None):
|
||||
content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n")
|
||||
content.append(f"*Amount:* `{msg['amount']:.8f}`\n")
|
||||
content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n")
|
||||
content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n")
|
||||
content.append(
|
||||
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||
)
|
||||
if msg.get('fiat_currency', None):
|
||||
content.append(
|
||||
f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||
)
|
||||
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
|
||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_FILL:
|
||||
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
|
||||
elif msg['type'] == RPCMessageType.BUY:
|
||||
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
|
||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
|
||||
message += f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}"
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
|
||||
|
||||
message = ''.join(content)
|
||||
message += ")`"
|
||||
return message
|
||||
|
||||
@@ -254,54 +260,57 @@ class Telegram(RPCHandler):
|
||||
and self._rpc._fiat_converter):
|
||||
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
|
||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
msg['profit_extra'] = (' ({gain}: {profit_amount:.8f} {stake_currency}'
|
||||
' / {profit_fiat:.3f} {fiat_currency})').format(**msg)
|
||||
msg['profit_extra'] = (
|
||||
f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}"
|
||||
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
|
||||
else:
|
||||
msg['profit_extra'] = ''
|
||||
is_fill = msg['type'] == RPCMessageType.SELL_FILL
|
||||
message = (
|
||||
f"{msg['emoji']} *{msg['exchange']}:* "
|
||||
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
||||
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||
f"*Amount:* `{msg['amount']:.8f}`\n")
|
||||
|
||||
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
|
||||
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
|
||||
"*Buy Tag:* `{buy_tag}`\n"
|
||||
"*Sell Reason:* `{sell_reason}`\n"
|
||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Close Rate:* `{limit:.8f}`").format(**msg)
|
||||
if msg['type'] == RPCMessageType.SELL:
|
||||
message += (f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_FILL:
|
||||
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
|
||||
|
||||
return message
|
||||
|
||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||
|
||||
if msg_type == RPCMessageType.BUY:
|
||||
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
|
||||
message = self._format_buy_msg(msg)
|
||||
|
||||
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
|
||||
message = self._format_sell_msg(msg)
|
||||
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Reason: {reason}.".format(**msg))
|
||||
|
||||
elif msg_type == RPCMessageType.BUY_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Buy order for {pair} (#{trade_id}) filled "
|
||||
"for {open_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL_FILL:
|
||||
message = ("\N{LARGE CIRCLE} *{exchange}:* "
|
||||
"Sell order for {pair} (#{trade_id}) filled "
|
||||
"for {close_rate}.".format(**msg))
|
||||
elif msg_type == RPCMessageType.SELL:
|
||||
message = self._format_sell_msg(msg)
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||
message = (
|
||||
"*Protection* triggered due to {reason}. "
|
||||
"`{pair}` will be locked until `{lock_end_time}`."
|
||||
).format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER_GLOBAL:
|
||||
message = (
|
||||
"*Protection* triggered due to {reason}. "
|
||||
"*All pairs* will be locked until `{lock_end_time}`."
|
||||
).format(**msg)
|
||||
|
||||
elif msg_type == RPCMessageType.STATUS:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
|
||||
@@ -353,7 +362,7 @@ class Telegram(RPCHandler):
|
||||
elif float(msg['profit_percent']) >= 0.0:
|
||||
return "\N{EIGHT SPOKED ASTERISK}"
|
||||
elif msg['sell_reason'] == "stop_loss":
|
||||
return"\N{WARNING SIGN}"
|
||||
return "\N{WARNING SIGN}"
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
|
||||
@@ -393,19 +402,19 @@ class Telegram(RPCHandler):
|
||||
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "",
|
||||
"*Current Rate:* `{current_rate:.8f}`",
|
||||
("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
|
||||
+ "`{profit_pct:.2f}%`",
|
||||
+ "`{profit_ratio:.2%}`",
|
||||
]
|
||||
if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
|
||||
and r['initial_stop_loss_pct'] is not None):
|
||||
and r['initial_stop_loss_ratio'] is not None):
|
||||
# Adding initial stoploss only if it is different from stoploss
|
||||
lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` "
|
||||
"`({initial_stop_loss_pct:.2f}%)`")
|
||||
"`({initial_stop_loss_ratio:.2%})`")
|
||||
|
||||
# Adding stoploss and stoploss percentage only if it is not None
|
||||
lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " +
|
||||
("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""))
|
||||
("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else ""))
|
||||
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
|
||||
"`({stoploss_current_dist_pct:.2f}%)`")
|
||||
"`({stoploss_current_dist_ratio:.2%})`")
|
||||
if r['open_order']:
|
||||
if r['sell_order_status']:
|
||||
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
|
||||
@@ -501,6 +510,86 @@ class Telegram(RPCHandler):
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _weekly(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /weekly <n>
|
||||
Returns a weekly profit (in BTC) over the last n weeks.
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else 8
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 8
|
||||
try:
|
||||
stats = self._rpc._rpc_weekly_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[week['date'],
|
||||
f"{round_coin_value(week['abs_profit'], stats['stake_currency'])}",
|
||||
f"{week['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||
f"{week['trade_count']} trades"] for week in stats['data']],
|
||||
headers=[
|
||||
'Monday',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Weekly Profit over the last {timescale} weeks ' \
|
||||
f'(starting from Monday)</b>:\n<pre>{stats_tab}</pre> '
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path="update_weekly", query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _monthly(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /monthly <n>
|
||||
Returns a monthly profit (in BTC) over the last n months.
|
||||
:param bot: telegram bot
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
stake_cur = self._config['stake_currency']
|
||||
fiat_disp_cur = self._config.get('fiat_display_currency', '')
|
||||
try:
|
||||
timescale = int(context.args[0]) if context.args else 6
|
||||
except (TypeError, ValueError, IndexError):
|
||||
timescale = 6
|
||||
try:
|
||||
stats = self._rpc._rpc_monthly_profit(
|
||||
timescale,
|
||||
stake_cur,
|
||||
fiat_disp_cur
|
||||
)
|
||||
stats_tab = tabulate(
|
||||
[[month['date'],
|
||||
f"{round_coin_value(month['abs_profit'], stats['stake_currency'])}",
|
||||
f"{month['fiat_value']:.3f} {stats['fiat_display_currency']}",
|
||||
f"{month['trade_count']} trades"] for month in stats['data']],
|
||||
headers=[
|
||||
'Month',
|
||||
f'Profit {stake_cur}',
|
||||
f'Profit {fiat_disp_cur}',
|
||||
'Trades',
|
||||
],
|
||||
tablefmt='simple')
|
||||
message = f'<b>Monthly Profit over the last {timescale} months' \
|
||||
f'</b>:\n<pre>{stats_tab}</pre> '
|
||||
self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True,
|
||||
callback_path="update_monthly", query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _profit(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@@ -528,11 +617,11 @@ class Telegram(RPCHandler):
|
||||
fiat_disp_cur,
|
||||
start_date)
|
||||
profit_closed_coin = stats['profit_closed_coin']
|
||||
profit_closed_percent_mean = stats['profit_closed_percent_mean']
|
||||
profit_closed_ratio_mean = stats['profit_closed_ratio_mean']
|
||||
profit_closed_percent = stats['profit_closed_percent']
|
||||
profit_closed_fiat = stats['profit_closed_fiat']
|
||||
profit_all_coin = stats['profit_all_coin']
|
||||
profit_all_percent_mean = stats['profit_all_percent_mean']
|
||||
profit_all_ratio_mean = stats['profit_all_ratio_mean']
|
||||
profit_all_percent = stats['profit_all_percent']
|
||||
profit_all_fiat = stats['profit_all_fiat']
|
||||
trade_count = stats['trade_count']
|
||||
@@ -540,7 +629,7 @@ class Telegram(RPCHandler):
|
||||
latest_trade_date = stats['latest_trade_date']
|
||||
avg_duration = stats['avg_duration']
|
||||
best_pair = stats['best_pair']
|
||||
best_rate = stats['best_rate']
|
||||
best_pair_profit_ratio = stats['best_pair_profit_ratio']
|
||||
if stats['trade_count'] == 0:
|
||||
markdown_msg = 'No trades yet.'
|
||||
else:
|
||||
@@ -548,7 +637,7 @@ class Telegram(RPCHandler):
|
||||
if stats['closed_trade_count'] > 0:
|
||||
markdown_msg = ("*ROI:* Closed trades\n"
|
||||
f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
|
||||
f"({profit_closed_percent_mean:.2f}%) "
|
||||
f"({profit_closed_ratio_mean:.2%}) "
|
||||
f"({profit_closed_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||
f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
|
||||
else:
|
||||
@@ -557,7 +646,7 @@ class Telegram(RPCHandler):
|
||||
markdown_msg += (
|
||||
f"*ROI:* All trades\n"
|
||||
f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
|
||||
f"({profit_all_percent_mean:.2f}%) "
|
||||
f"({profit_all_ratio_mean:.2%}) "
|
||||
f"({profit_all_percent} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
|
||||
f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
|
||||
f"*Total Trade Count:* `{trade_count}`\n"
|
||||
@@ -568,7 +657,7 @@ class Telegram(RPCHandler):
|
||||
)
|
||||
if stats['closed_trade_count'] > 0:
|
||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
||||
f"*Best Performing:* `{best_pair}: {best_pair_profit_ratio:.2%}`")
|
||||
self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit",
|
||||
query=update.callback_query)
|
||||
|
||||
@@ -597,10 +686,16 @@ class Telegram(RPCHandler):
|
||||
count['losses']
|
||||
] for reason, count in stats['sell_reasons'].items()
|
||||
]
|
||||
sell_reasons_msg = tabulate(
|
||||
sell_reasons_tabulate,
|
||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||
)
|
||||
sell_reasons_msg = 'No trades yet.'
|
||||
for reason in chunks(sell_reasons_tabulate, 25):
|
||||
sell_reasons_msg = tabulate(
|
||||
reason,
|
||||
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
|
||||
)
|
||||
if len(sell_reasons_tabulate) > 25:
|
||||
self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN)
|
||||
sell_reasons_msg = ''
|
||||
|
||||
durations = stats['durations']
|
||||
duration_msg = tabulate(
|
||||
[
|
||||
@@ -671,10 +766,10 @@ class Telegram(RPCHandler):
|
||||
output += ("\n*Estimated Value*:\n"
|
||||
f"\t`{result['stake']}: "
|
||||
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||
f" `({result['starting_capital_pct']}%)`\n"
|
||||
f" `({result['starting_capital_ratio']:.2%})`\n"
|
||||
f"\t`{result['symbol']}: "
|
||||
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||
f" `({result['starting_capital_fiat_ratio']:.2%})`\n")
|
||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
@@ -809,7 +904,7 @@ class Telegram(RPCHandler):
|
||||
trades_tab = tabulate(
|
||||
[[arrow.get(trade['close_date']).humanize(),
|
||||
trade['pair'] + " (#" + str(trade['trade_id']) + ")",
|
||||
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
||||
f"{(trade['close_profit']):.2%} ({trade['close_profit_abs']})"]
|
||||
for trade in trades['trades']],
|
||||
headers=[
|
||||
'Close Date',
|
||||
@@ -861,7 +956,7 @@ class Telegram(RPCHandler):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['pair']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
@@ -896,7 +991,7 @@ class Telegram(RPCHandler):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
@@ -931,7 +1026,7 @@ class Telegram(RPCHandler):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['sell_reason']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_pct']:.2f}%) "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
@@ -966,7 +1061,7 @@ class Telegram(RPCHandler):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['mix_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit']:.2f}%) "
|
||||
f"({trade['profit']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
|
||||
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
|
||||
@@ -1149,44 +1244,56 @@ class Telegram(RPCHandler):
|
||||
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||
"Optionally takes a rate at which to buy "
|
||||
"(only applies to limit orders).` \n")
|
||||
message = ("*/start:* `Starts the trader`\n"
|
||||
"*/stop:* `Stops the trader`\n"
|
||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||
" *<trade_id> :* `Lists one or more specific trades.`\n"
|
||||
" `Separate multiple <trade_id> with a blank space.`\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"
|
||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||
"over the last n days`\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"
|
||||
"*/stats:* `Shows Wins / losses by Sell reason as well as "
|
||||
"Avg. holding durationsfor buys and sells.`\n"
|
||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||
"*/locks:* `Show currently locked pairs`\n"
|
||||
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\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"
|
||||
"*/logs [limit]:* `Show latest logs - defaults to 10` \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`")
|
||||
message = (
|
||||
"_BotControl_\n"
|
||||
"------------\n"
|
||||
"*/start:* `Starts the trader`\n"
|
||||
"*/stop:* Stops the trader\n"
|
||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \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"
|
||||
"*/whitelist:* `Show current whitelist` \n"
|
||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||
"to the blacklist.` \n"
|
||||
"*/reload_config:* `Reload configuration file` \n"
|
||||
"*/unlock <pair|id>:* `Unlock this Pair (or this lock id if it's numeric)`\n"
|
||||
|
||||
self._send_msg(message)
|
||||
"_Current state_\n"
|
||||
"------------\n"
|
||||
"*/show_config:* `Show running configuration` \n"
|
||||
"*/locks:* `Show currently locked pairs`\n"
|
||||
"*/balance:* `Show account balance per currency`\n"
|
||||
"*/logs [limit]:* `Show latest logs - defaults to 10` \n"
|
||||
"*/count:* `Show number of active trades compared to allowed number of trades`\n"
|
||||
"*/edge:* `Shows validated pairs by Edge if it is enabled` \n"
|
||||
|
||||
"_Statistics_\n"
|
||||
"------------\n"
|
||||
"*/status <trade_id>|[table]:* `Lists all open trades`\n"
|
||||
" *<trade_id> :* `Lists one or more specific trades.`\n"
|
||||
" `Separate multiple <trade_id> with a blank space.`\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"
|
||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
|
||||
"over the last n days`\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"
|
||||
"*/weekly <n>:* `Shows statistics per week, over the last n weeks`\n"
|
||||
"*/monthly <n>:* `Shows statistics per month, over the last n months`\n"
|
||||
"*/stats:* `Shows Wins / losses by Sell reason as well as "
|
||||
"Avg. holding durationsfor buys and sells.`\n"
|
||||
"*/help:* `This help message`\n"
|
||||
"*/version:* `Show version`"
|
||||
)
|
||||
|
||||
self._send_msg(message, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
@authorized_only
|
||||
def _version(self, update: Update, context: CallbackContext) -> None:
|
||||
|
@@ -292,7 +292,7 @@ class BooleanParameter(CategoricalParameter):
|
||||
load=load, **kwargs)
|
||||
|
||||
|
||||
class HyperStrategyMixin(object):
|
||||
class HyperStrategyMixin:
|
||||
"""
|
||||
A helper base class which allows HyperOptAuto class to reuse implementations of buy/sell
|
||||
strategy logic.
|
||||
|
@@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
|
||||
CUSTOM_SELL_MAX_LENGTH = 64
|
||||
|
||||
|
||||
class SellCheckTuple(object):
|
||||
class SellCheckTuple:
|
||||
"""
|
||||
NamedTuple for Sell type + reason
|
||||
"""
|
||||
@@ -868,7 +868,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
||||
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
|
||||
stop_loss_value = self.trailing_stop_positive
|
||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
||||
f"offset: {sl_offset:.4g} profit: {current_profit:.2%}")
|
||||
|
||||
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
|
||||
from freqtrade.exceptions import StrategyError
|
||||
|
||||
@@ -14,6 +15,9 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_err
|
||||
"""
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
if 'trade' in kwargs:
|
||||
# Protect accidental modifications from within the strategy
|
||||
kwargs['trade'] = deepcopy(kwargs['trade'])
|
||||
return f(*args, **kwargs)
|
||||
except ValueError as error:
|
||||
logger.warning(
|
||||
|
@@ -73,7 +73,7 @@ class Wallets:
|
||||
tot_profit = Trade.get_total_closed_profit()
|
||||
else:
|
||||
tot_profit = LocalTrade.total_profit
|
||||
tot_in_trades = sum([trade.stake_amount for trade in open_trades])
|
||||
tot_in_trades = sum(trade.stake_amount for trade in open_trades)
|
||||
|
||||
current_stake = self.start_cap + tot_profit - tot_in_trades
|
||||
_wallets[self._config['stake_currency']] = Wallet(
|
||||
@@ -238,7 +238,7 @@ class Wallets:
|
||||
|
||||
return self._check_available_stake_amount(stake_amount, available_amount)
|
||||
|
||||
def _validate_stake_amount(self, pair, stake_amount, min_stake_amount):
|
||||
def validate_stake_amount(self, pair, stake_amount, min_stake_amount):
|
||||
if not stake_amount:
|
||||
logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.")
|
||||
return 0
|
||||
@@ -250,17 +250,27 @@ class Wallets:
|
||||
logger.warning("Minimum stake amount > available balance.")
|
||||
return 0
|
||||
if min_stake_amount is not None and stake_amount < min_stake_amount:
|
||||
stake_amount = min_stake_amount
|
||||
if self._log:
|
||||
logger.info(
|
||||
f"Stake amount for pair {pair} is too small "
|
||||
f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}."
|
||||
)
|
||||
if stake_amount * 1.3 < min_stake_amount:
|
||||
# Top-cap stake-amount adjustments to +30%.
|
||||
if self._log:
|
||||
logger.info(
|
||||
f"Adjusted stake amount for pair {pair} is more than 30% bigger than "
|
||||
f"the desired stake ({stake_amount} * 1.3 > {max_stake_amount}), "
|
||||
f"ignoring trade."
|
||||
)
|
||||
return 0
|
||||
stake_amount = min_stake_amount
|
||||
|
||||
if stake_amount > max_stake_amount:
|
||||
stake_amount = max_stake_amount
|
||||
if self._log:
|
||||
logger.info(
|
||||
f"Stake amount for pair {pair} is too big "
|
||||
f"({stake_amount} > {max_stake_amount}), adjusting to {max_stake_amount}."
|
||||
)
|
||||
stake_amount = max_stake_amount
|
||||
return stake_amount
|
||||
|
Reference in New Issue
Block a user