Updated LocalTrade and Order classes

This commit is contained in:
Sam Germain 2021-06-19 22:06:51 -06:00
parent a49ca9cbf7
commit abe0b3e3bd
19 changed files with 179 additions and 121 deletions

View File

@ -55,7 +55,7 @@ Currently, the arguments are:
* `results`: DataFrame containing the result * `results`: DataFrame containing the result
The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`):
`pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, sell_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs` `pair, profit_ratio, profit_abs, open_date, open_rate, fee_open, close_date, close_rate, fee_close, amount, trade_duration, is_open, close_reason, stake_amount, min_rate, max_rate, stop_loss_ratio, stop_loss_abs`
* `trade_count`: Amount of trades (identical to `len(results)`) * `trade_count`: Amount of trades (identical to `len(results)`)
* `min_date`: Start date of the timerange used * `min_date`: Start date of the timerange used
* `min_date`: End date of the timerange used * `min_date`: End date of the timerange used

View File

@ -65,7 +65,7 @@ SET is_open=0,
close_rate=<close_rate>, close_rate=<close_rate>,
close_profit = close_rate / open_rate - 1, close_profit = close_rate / open_rate - 1,
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
sell_reason=<sell_reason> close_reason=<close_reason>
WHERE id=<trade_ID_to_update>; WHERE id=<trade_ID_to_update>;
``` ```
@ -78,7 +78,7 @@ SET is_open=0,
close_rate=0.19638016, close_rate=0.19638016,
close_profit=0.0496, close_profit=0.0496,
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))),
sell_reason='force_sell' close_reason='force_sell'
WHERE id=31; WHERE id=31;
``` ```

View File

@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date
class AwesomeStrategy(IStrategy): class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, close_reason: str,
current_time: 'datetime', **kwargs) -> bool: current_time: 'datetime', **kwargs) -> bool:
# Obtain pair dataframe. # Obtain pair dataframe.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
@ -490,7 +490,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods # ... populate_* methods
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: rate: float, time_in_force: str, close_reason: str, **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or Timing for this function is critical, so avoid doing heavy computations or
@ -505,14 +505,14 @@ class AwesomeStrategy(IStrategy):
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason. :param close_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange. :return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process False aborts the process
""" """
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: if close_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit # Reject force-sells with negative profit
# This is just a sample, please adjust to your needs # This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling) # (this does not necessarily make sense, assuming you know when you're force-selling)

View File

@ -127,7 +127,7 @@ print(stats['strategy_comparison'])
trades = load_backtest_data(backtest_dir) trades = load_backtest_data(backtest_dir)
# Show value-counts per pair # Show value-counts per pair
trades.groupby("pair")["sell_reason"].value_counts() trades.groupby("pair")["close_reason"].value_counts()
``` ```
### Load live trading results into a pandas dataframe ### Load live trading results into a pandas dataframe
@ -142,7 +142,7 @@ from freqtrade.data.btanalysis import load_trades_from_db
trades = load_trades_from_db("sqlite:///tradesv3.sqlite") trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
# Display results # Display results
trades.groupby("pair")["sell_reason"].value_counts() trades.groupby("pair")["close_reason"].value_counts()
``` ```
## Analyze the loaded trades for trade parallelism ## Analyze the loaded trades for trade parallelism

View File

@ -132,7 +132,7 @@ Possible parameters are:
* `profit_ratio` * `profit_ratio`
* `stake_currency` * `stake_currency`
* `fiat_currency` * `fiat_currency`
* `sell_reason` * `close_reason`
* `order_type` * `order_type`
* `open_date` * `open_date`
* `close_date` * `close_date`
@ -154,7 +154,7 @@ Possible parameters are:
* `profit_ratio` * `profit_ratio`
* `stake_currency` * `stake_currency`
* `fiat_currency` * `fiat_currency`
* `sell_reason` * `close_reason`
* `order_type` * `order_type`
* `open_date` * `open_date`
* `close_date` * `close_date`
@ -176,7 +176,7 @@ Possible parameters are:
* `profit_ratio` * `profit_ratio`
* `stake_currency` * `stake_currency`
* `fiat_currency` * `fiat_currency`
* `sell_reason` * `close_reason`
* `order_type` * `order_type`
* `open_date` * `open_date`
* `close_date` * `close_date`

View File

@ -17,18 +17,18 @@ logger = logging.getLogger(__name__)
# Old format - maybe remove? # Old format - maybe remove?
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index", BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] "trade_duration", "open_rate", "close_rate", "open_at_end", "close_reason"]
# Mid-term format, crated by BacktestResult Named Tuple # Mid-term format, crated by BacktestResult Named Tuple
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration', BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open', 'open_rate', 'close_rate', 'open_at_end', 'close_reason', 'fee_open',
'fee_close', 'amount', 'profit_abs', 'profit_ratio'] 'fee_close', 'amount', 'profit_abs', 'profit_ratio']
# Newest format # Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'open_rate', 'close_rate', 'open_rate', 'close_rate',
'fee_open', 'fee_close', 'trade_duration', 'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'sell_reason', 'profit_ratio', 'profit_abs', 'close_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ] 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ]

View File

@ -752,7 +752,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully') logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellCheckTuple( self.execute_sell(trade, trade.stop_loss, close_reason=SellCheckTuple(
sell_type=SellType.EMERGENCY_SELL)) sell_type=SellType.EMERGENCY_SELL))
except ExchangeError: except ExchangeError:
@ -783,7 +783,7 @@ class FreqtradeBot(LoggingMixin):
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value
self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order,
stoploss_order=True) stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
@ -1071,16 +1071,16 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: def execute_sell(self, trade: Trade, limit: float, close_reason: SellCheckTuple) -> bool:
""" """
Executes a limit sell for the given trade and limit Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:param sell_reason: Reason the sell was triggered :param close_reason: Reason the sell was triggered
:return: True if it succeeds (supported) False (not supported) :return: True if it succeeds (supported) False (not supported)
""" """
sell_type = 'sell' sell_type = 'sell'
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if close_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
sell_type = 'stoploss' sell_type = 'stoploss'
# if stoploss is on exchange and we are on dry_run mode, # if stoploss is on exchange and we are on dry_run mode,
@ -1099,10 +1099,10 @@ class FreqtradeBot(LoggingMixin):
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
order_type = self.strategy.order_types[sell_type] order_type = self.strategy.order_types[sell_type]
if sell_reason.sell_type == SellType.EMERGENCY_SELL: if close_reason.sell_type == SellType.EMERGENCY_SELL:
# Emergency sells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
if sell_reason.sell_type == SellType.FORCE_SELL: if close_reason.sell_type == SellType.FORCE_SELL:
# Force sells (default to the sell_type defined in the strategy, # Force sells (default to the sell_type defined in the strategy,
# but we allow this value to be changed) # but we allow this value to be changed)
order_type = self.strategy.order_types.get("forcesell", order_type) order_type = self.strategy.order_types.get("forcesell", order_type)
@ -1112,7 +1112,7 @@ class FreqtradeBot(LoggingMixin):
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, time_in_force=time_in_force, close_reason=close_reason.close_reason,
current_time=datetime.now(timezone.utc)): current_time=datetime.now(timezone.utc)):
logger.info(f"User requested abortion of selling {trade.pair}") logger.info(f"User requested abortion of selling {trade.pair}")
return False return False
@ -1134,9 +1134,9 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj) trade.orders.append(order_obj)
trade.open_order_id = order['id'] trade.open_order_id = order['id']
trade.sell_order_status = '' trade.close_order_status = ''
trade.close_rate_requested = limit trade.close_rate_requested = limit
trade.sell_reason = sell_reason.sell_reason trade.close_reason = close_reason.close_reason
# In case of market sell orders the order can be closed immediately # In case of market sell orders the order can be closed immediately
if order.get('status', 'unknown') == 'closed': if order.get('status', 'unknown') == 'closed':
self.update_trade_state(trade, trade.open_order_id, order) self.update_trade_state(trade, trade.open_order_id, order)
@ -1176,7 +1176,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason, 'close_reason': trade.close_reason,
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(), 'close_date': trade.close_date or datetime.utcnow(),
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
@ -1195,10 +1195,10 @@ class FreqtradeBot(LoggingMixin):
""" """
Sends rpc notification when a sell cancel occurred. Sends rpc notification when a sell cancel occurred.
""" """
if trade.sell_order_status == reason: if trade.close_order_status == reason:
return return
else: else:
trade.sell_order_status = reason trade.close_order_status = reason
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate) profit_trade = trade.calc_profit(rate=profit_rate)
@ -1219,7 +1219,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_trade, 'profit_amount': profit_trade,
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason, 'close_reason': trade.close_reason,
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date, 'close_date': trade.close_date,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],

View File

@ -267,7 +267,7 @@ class Backtesting:
if sell.sell_flag: if sell.sell_flag:
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.sell_reason = sell.sell_reason trade.close_reason = sell.close_reason
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) 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) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur)
@ -277,7 +277,7 @@ class Backtesting:
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate, rate=closerate,
time_in_force=time_in_force, time_in_force=time_in_force,
sell_reason=sell.sell_reason, close_reason=sell.close_reason,
current_time=sell_row[DATE_IDX].to_pydatetime()): current_time=sell_row[DATE_IDX].to_pydatetime()):
return None return None
@ -329,7 +329,7 @@ class Backtesting:
sell_row = data[pair][-1] sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime() trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.sell_reason = SellType.FORCE_SELL.value trade.close_reason = SellType.FORCE_SELL.value
trade.close(sell_row[OPEN_IDX], show_msg=False) trade.close(sell_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade) LocalTrade.close_bt_trade(trade)
# Deepcopy object to have wallets update correctly # Deepcopy object to have wallets update correctly

View File

@ -127,7 +127,7 @@ def generate_pair_metrics(data: Dict[str, Dict], stake_currency: str, starting_b
return tabular_data return tabular_data
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: def generate_close_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param max_open_trades: Max_open_trades parameter :param max_open_trades: Max_open_trades parameter
@ -136,8 +136,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
""" """
tabular_data = [] tabular_data = []
for reason, count in results['sell_reason'].value_counts().iteritems(): for reason, count in results['close_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason] result = results.loc[results['close_reason'] == reason]
profit_mean = result['profit_ratio'].mean() profit_mean = result['profit_ratio'].mean()
profit_sum = result['profit_ratio'].sum() profit_sum = result['profit_ratio'].sum()
@ -145,7 +145,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List
tabular_data.append( tabular_data.append(
{ {
'sell_reason': reason, 'close_reason': reason,
'trades': count, 'trades': count,
'wins': len(result[result['profit_abs'] > 0]), 'wins': len(result[result['profit_abs'] > 0]),
'draws': len(result[result['profit_abs'] == 0]), 'draws': len(result[result['profit_abs'] == 0]),
@ -230,7 +230,7 @@ def generate_trading_stats(results: DataFrame) -> Dict[str, Any]:
draw_trades = results.loc[results['profit_ratio'] == 0] draw_trades = results.loc[results['profit_ratio'] == 0]
losing_trades = results.loc[results['profit_ratio'] < 0] losing_trades = results.loc[results['profit_ratio'] < 0]
zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) & zero_duration_trades = len(results.loc[(results['trade_duration'] == 0) &
(results['sell_reason'] == 'trailing_stop_loss')]) (results['close_reason'] == 'trailing_stop_loss')])
holding_avg = (timedelta(minutes=round(results['trade_duration'].mean())) holding_avg = (timedelta(minutes=round(results['trade_duration'].mean()))
if not results.empty else timedelta()) if not results.empty else timedelta())
@ -313,7 +313,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance, starting_balance=starting_balance,
results=results, skip_nan=False) results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades, close_reason_stats = generate_close_reason_stats(max_open_trades=max_open_trades,
results=results) results=results)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency, left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance, starting_balance=starting_balance,
@ -335,7 +335,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'best_pair': best_pair, 'best_pair': best_pair,
'worst_pair': worst_pair, 'worst_pair': worst_pair,
'results_per_pair': pair_results, 'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats, 'close_reason_summary': close_reason_stats,
'left_open_trades': left_open_results, 'left_open_trades': left_open_results,
'total_trades': len(results), 'total_trades': len(results),
'total_volume': float(results['stake_amount'].sum()), 'total_volume': float(results['stake_amount'].sum()),
@ -477,10 +477,10 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str: def text_table_close_reason(close_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
""" """
Generate small table outlining Backtest results Generate small table outlining Backtest results
:param sell_reason_stats: Sell reason metrics :param close_reason_stats: Sell reason metrics
:param stake_currency: Stakecurrency used :param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string :return: pretty printed table with tabulate as string
""" """
@ -495,12 +495,12 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren
] ]
output = [[ output = [[
t['sell_reason'], t['trades'], t['close_reason'], t['trades'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']), _generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
t['profit_mean_pct'], t['profit_sum_pct'], t['profit_mean_pct'], t['profit_sum_pct'],
round_coin_value(t['profit_total_abs'], stake_currency, False), round_coin_value(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'], t['profit_total_pct'],
] for t in sell_reason_stats] ] for t in close_reason_stats]
return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right")
@ -633,7 +633,7 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '=')) print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table) print(table)
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], table = text_table_close_reason(close_reason_stats=results['close_reason_summary'],
stake_currency=stake_currency) stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0: if isinstance(table, str) and len(table) > 0:
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))

View File

@ -45,7 +45,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
max_rate = get_column_def(cols, 'max_rate', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0')
min_rate = get_column_def(cols, 'min_rate', 'null') min_rate = get_column_def(cols, 'min_rate', 'null')
sell_reason = get_column_def(cols, 'sell_reason', 'null') close_reason = get_column_def(cols, 'close_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null') strategy = get_column_def(cols, 'strategy', 'null')
# If ticker-interval existed use that, else null. # If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'): if has_column(cols, 'ticker_interval'):
@ -58,7 +58,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
close_profit_abs = get_column_def( close_profit_abs = get_column_def(
cols, 'close_profit_abs', cols, 'close_profit_abs',
f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}")
sell_order_status = get_column_def(cols, 'sell_order_status', 'null') close_order_status = get_column_def(cols, 'close_order_status', 'null')
amount_requested = get_column_def(cols, 'amount_requested', 'amount') amount_requested = get_column_def(cols, 'amount_requested', 'amount')
# Schema migration necessary # Schema migration necessary
@ -80,7 +80,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update, stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, max_rate, min_rate, close_reason, close_order_status, strategy,
timeframe, open_trade_value, close_profit_abs timeframe, open_trade_value, close_profit_abs
) )
select id, lower(exchange), select id, lower(exchange),
@ -101,8 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
{initial_stop_loss} initial_stop_loss, {initial_stop_loss} initial_stop_loss,
{initial_stop_loss_pct} initial_stop_loss_pct, {initial_stop_loss_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason,
{sell_order_status} sell_order_status, {close_order_status} close_order_status,
{strategy} strategy, {timeframe} timeframe, {strategy} strategy, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
from {table_back_name} from {table_back_name}

View File

@ -29,13 +29,13 @@ _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database
def init_db(db_url: str, clean_open_orders: bool = False) -> None: def init_db(db_url: str, clean_open_orders: bool = False) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param db_url: Database to use :param db_url: Database to use
:param clean_open_orders: Remove open orders from the database. :param clean_open_orders: Remove open orders from the database.
Useful for dry-run or if all orders have been reset on the exchange. Useful for dry-run or if all orders have been reset on the exchange.
:return: None :return: None
""" """
kwargs = {} kwargs = {}
@ -131,9 +131,10 @@ class Order(_DECL_BASE):
order_date = Column(DateTime, nullable=True, default=datetime.utcnow) order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
order_filled_date = Column(DateTime, nullable=True) order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True)
leverage = Column(Float, nullable=True)
def __repr__(self): def __repr__(self):
return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
f'side={self.side}, order_type={self.order_type}, status={self.status})') f'side={self.side}, order_type={self.order_type}, status={self.status})')
@ -155,6 +156,7 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average) self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining) self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost) self.cost = order.get('cost', self.cost)
self.leverage = order.get('leverage', self.leverage)
if 'timestamp' in order and order['timestamp'] is not None: if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -226,6 +228,7 @@ class LocalTrade():
fee_close_currency: str = '' fee_close_currency: str = ''
open_rate: float = 0.0 open_rate: float = 0.0
open_rate_requested: Optional[float] = None open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value # open_trade_value - calculated via _calc_open_trade_value
open_trade_value: float = 0.0 open_trade_value: float = 0.0
close_rate: Optional[float] = None close_rate: Optional[float] = None
@ -254,11 +257,20 @@ class LocalTrade():
max_rate: float = 0.0 max_rate: float = 0.0
# Lowest price reached # Lowest price reached
min_rate: float = 0.0 min_rate: float = 0.0
sell_reason: str = '' close_reason: str = ''
sell_order_status: str = '' close_order_status: str = ''
strategy: str = '' strategy: str = ''
timeframe: Optional[int] = None timeframe: Optional[int] = None
#Margin trading properties
leverage: float = 1.0
borrowed: float = 0
borrowed_currency: float = None
interest_rate: float = 0
min_stoploss: float = None
isShort: boolean = False
#End of margin trading properties
def __init__(self, **kwargs): def __init__(self, **kwargs):
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
@ -322,8 +334,8 @@ class LocalTrade():
'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None,
'profit_abs': self.close_profit_abs, 'profit_abs': self.close_profit_abs,
'sell_reason': self.sell_reason, 'close_reason': self.close_reason,
'sell_order_status': self.sell_order_status, 'close_order_status': self.close_order_status,
'stop_loss_abs': self.stop_loss, 'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, '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, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
@ -340,6 +352,13 @@ class LocalTrade():
'min_rate': self.min_rate, 'min_rate': self.min_rate,
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'leverage': self.leverage,
'borrowed': self.borrowed,
'borrowed_currency': self.borrowed_currency,
'interest_rate': self.interest_rate,
'min_stoploss': self.min_stoploss,
'leverage': self.leverage,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
} }
@ -379,6 +398,9 @@ class LocalTrade():
return return
new_loss = float(current_price * (1 - abs(stoploss))) new_loss = float(current_price * (1 - abs(stoploss)))
#TODO: Could maybe move this if into the new stoploss if branch
if (self.min_stoploss): #If trading on margin, don't set the stoploss below the liquidation price
new_loss = min(self.min_stoploss, new_loss)
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: if not self.stop_loss:
@ -389,7 +411,7 @@ class LocalTrade():
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down! if (new_loss > self.stop_loss and not self.isShort) or (new_loss < self.stop_loss and self.isShort): # stop losses only walk up, never down!, #TODO: But adding more to a margin account would create a lower liquidation price, decreasing the minimum stoploss
logger.debug(f"{self.pair} - Adjusting stoploss...") logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss) self._set_new_stoploss(new_loss, stoploss)
else: else:
@ -403,6 +425,20 @@ class LocalTrade():
f"Trailing stoploss saved us: " f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
def is_opening_trade(self, side) -> bool:
"""
Determines if the trade is an opening (long buy or short sell) trade
:param side (string): the side (buy/sell) that order happens on
"""
return (side == 'buy' and not self.isShort) or (side == 'sell' and self.isShort)
def is_closing_trade(self, side) -> bool:
"""
Determines if the trade is an closing (long sell or short buy) trade
:param side (string): the side (buy/sell) that order happens on
"""
return (side == 'sell' and not self.isShort) or (side == 'buy' and self.isShort)
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.
@ -416,22 +452,24 @@ class LocalTrade():
logger.info('Updating trade (id=%s) ...', self.id) logger.info('Updating trade (id=%s) ...', self.id)
if order_type in ('market', 'limit') and order['side'] == 'buy': if order_type in ('market', 'limit') and self.isOpeningTrade(order['side']):
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = float(safe_value_fallback(order, 'average', 'price')) self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount')) self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_value() self.recalc_open_trade_value()
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') payment = "SELL" if self.isShort else "BUY"
logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.')
self.open_order_id = None self.open_order_id = None
elif order_type in ('market', 'limit') and order['side'] == 'sell': elif order_type in ('market', 'limit') and self.isClosingTrade(order['side']):
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') payment = "BUY" if self.isShort else "SELL"
self.close(safe_value_fallback(order, 'average', 'price')) logger.info(f'{order_type.upper()}_{payment} order has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) #TODO: Double check this
elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
self.stoploss_order_id = None self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.close_reason = SellType.STOPLOSS_ON_EXCHANGE.value
if self.is_open: if self.is_open:
logger.info(f'{order_type.upper()} is hit for {self}.') logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price')) self.close(safe_value_fallback(order, 'average', 'price'))
@ -445,11 +483,11 @@ class LocalTrade():
and marks trade as closed and marks trade as closed
""" """
self.close_rate = rate self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio() self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit() self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
self.is_open = False self.is_open = False
self.sell_order_status = 'closed' self.close_order_status = 'closed'
self.open_order_id = None self.open_order_id = None
if show_msg: if show_msg:
logger.info( logger.info(
@ -462,14 +500,14 @@ class LocalTrade():
""" """
Update Fee parameters. Only acts once per side Update Fee parameters. Only acts once per side
""" """
if side == 'buy' and self.fee_open_currency is None: if self.is_opening_trade(side) and self.fee_open_currency is None:
self.fee_open_cost = fee_cost self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency self.fee_open_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
self.fee_open = fee_rate self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess # Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None: elif self.is_closing_trade(side) and self.fee_close_currency is None:
self.fee_close_cost = fee_cost self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency self.fee_close_currency = fee_currency
if fee_rate is not None: if fee_rate is not None:
@ -479,9 +517,9 @@ class LocalTrade():
""" """
Verify if this side (buy / sell) has already been updated Verify if this side (buy / sell) has already been updated
""" """
if side == 'buy': if self.is_opening_trade(side):
return self.fee_open_currency is not None return self.fee_open_currency is not None
elif side == 'sell': elif self.is_closing_trade(side):
return self.fee_close_currency is not None return self.fee_close_currency is not None
else: else:
return False return False
@ -494,9 +532,13 @@ class LocalTrade():
Calculate the open_rate including open_fee. Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
buy_trade = Decimal(self.amount) * Decimal(self.open_rate) open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open) fees = open_trade * Decimal(self.fee_open)
return float(buy_trade + fees) if (self.isShort):
return float(open_trade - fees)
else:
return float(open_trade + fees)
def recalc_open_trade_value(self) -> None: def recalc_open_trade_value(self) -> None:
""" """
@ -513,34 +555,47 @@ class LocalTrade():
If rate is not set self.fee will be used If rate is not set self.fee will be used
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param borrowed: amount borrowed to make this trade
If borrowed is not set self.borrowed will be used
:return: Price in BTC of the open trade :return: Price in BTC of the open trade
""" """
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close) fees = close_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees) if (self.isShort):
interest = (self.interest_rate * Decimal(borrowed or self.borrowed)) * (datetime.utcnow() - self.open_date).days #Interest/day * num of days, 0 if not margin
return float(close_trade + fees + interest)
else:
return float(close_trade - fees)
def calc_profit(self, rate: Optional[float] = None, def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used If fee is not set self.fee will be used
:param rate: close rate to compare with (optional). :param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:param borrowed: amount borrowed to make this trade
If borrowed is not set self.borrowed will be used
:return: profit in stake currency as float :return: profit in stake currency as float
""" """
close_trade_value = self.calc_close_trade_value( close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
profit = close_trade_value - self.open_trade_value
if self.isShort:
profit = self.open_trade_value - close_trade_value
else:
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None, def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float: fee: Optional[float] = None,
borrowed: Optional[float] = None) -> float:
""" """
Calculates the profit as ratio (including fee). Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
@ -554,7 +609,10 @@ class LocalTrade():
) )
if self.open_trade_value == 0.0: if self.open_trade_value == 0.0:
return 0.0 return 0.0
profit_ratio = (close_trade_value / self.open_trade_value) - 1 if self.isShort:
profit_ratio = (close_trade_value / self.open_trade_value) - 1
else:
profit_ratio = (self.open_trade_value / close_trade_value) - 1
return float(f"{profit_ratio:.8f}") return float(f"{profit_ratio:.8f}")
def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]:
@ -604,7 +662,7 @@ class LocalTrade():
sel_trades = [trade for trade in sel_trades if trade.close_date sel_trades = [trade for trade in sel_trades if trade.close_date
and trade.close_date > close_date] and trade.close_date > close_date]
return sel_trades return sel_trades #TODO: What is sel_trades does it mean sell_trades? If so, update this for margin
@staticmethod @staticmethod
def close_bt_trade(trade): def close_bt_trade(trade):
@ -700,8 +758,8 @@ class Trade(_DECL_BASE, LocalTrade):
max_rate = Column(Float, nullable=True, default=0.0) max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached # Lowest price reached
min_rate = Column(Float, nullable=True) min_rate = Column(Float, nullable=True)
sell_reason = Column(String(100), nullable=True) close_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True) close_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True) strategy = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True) timeframe = Column(Integer, nullable=True)

View File

@ -193,7 +193,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
if trades is not None and len(trades) > 0: if trades is not None and len(trades) > 0:
# Create description for sell summarizing the trade # 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"{round(row['profit_ratio'] * 100, 1)}%, "
f"{row['sell_reason']}, " f"{row['close_reason']}, "
f"{row['trade_duration']} min", f"{row['trade_duration']} min",
axis=1) axis=1)
trade_buys = go.Scatter( trade_buys = go.Scatter(

View File

@ -44,8 +44,8 @@ class StoplossGuard(IProtection):
# filters = [ # filters = [
# Trade.is_open.is_(False), # Trade.is_open.is_(False),
# Trade.close_date > look_back_until, # Trade.close_date > look_back_until,
# or_(Trade.sell_reason == SellType.STOP_LOSS.value, # or_(Trade.close_reason == SellType.STOP_LOSS.value,
# and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, # and_(Trade.close_reason == SellType.TRAILING_STOP_LOSS.value,
# Trade.close_profit < 0)) # Trade.close_profit < 0))
# ] # ]
# if pair: # if pair:
@ -53,7 +53,7 @@ class StoplossGuard(IProtection):
# trades = Trade.get_trades(filters).all() # trades = Trade.get_trades(filters).all()
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( trades = [trade for trade in trades1 if (str(trade.close_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value, SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value) SellType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < 0)] and trade.close_profit and trade.close_profit < 0)]

View File

@ -94,7 +94,7 @@ class SellReason(BaseModel):
class Stats(BaseModel): class Stats(BaseModel):
sell_reasons: Dict[str, SellReason] close_reasons: Dict[str, SellReason]
durations: Dict[str, Union[str, float]] durations: Dict[str, Union[str, float]]
@ -168,8 +168,8 @@ class TradeSchema(BaseModel):
profit_pct: Optional[float] profit_pct: Optional[float]
profit_abs: Optional[float] profit_abs: Optional[float]
profit_fiat: Optional[float] profit_fiat: Optional[float]
sell_reason: Optional[str] close_reason: Optional[str]
sell_order_status: Optional[str] close_order_status: Optional[str]
stop_loss_abs: Optional[float] stop_loss_abs: Optional[float]
stop_loss_ratio: Optional[float] stop_loss_ratio: Optional[float]
stop_loss_pct: Optional[float] stop_loss_pct: Optional[float]

View File

@ -315,11 +315,11 @@ class RPC:
return 'draws' return 'draws'
trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
# Sell reason # Sell reason
sell_reasons = {} close_reasons = {}
for trade in trades: for trade in trades:
if trade.sell_reason not in sell_reasons: if trade.close_reason not in close_reasons:
sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} close_reasons[trade.close_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 close_reasons[trade.close_reason][trade_win_loss(trade)] += 1
# Duration # Duration
dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
@ -333,7 +333,7 @@ class RPC:
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A'
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
return {'sell_reasons': sell_reasons, 'durations': durations} return {'close_reasons': close_reasons, 'durations': durations}
def _rpc_trade_statistics( def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str, self, stake_currency: str, fiat_display_currency: str,
@ -539,8 +539,8 @@ class RPC:
if not fully_canceled: if not fully_canceled:
# Get current rate and execute sell # Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False) current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) close_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason) self._freqtrade.execute_sell(trade, current_rate, close_reason)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:

View File

@ -230,7 +230,7 @@ class Telegram(RPCHandler):
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Profit:* `{profit_percent:.2f}%{profit_extra}`\n" "*Profit:* `{profit_percent:.2f}%{profit_extra}`\n"
"*Sell Reason:* `{sell_reason}`\n" "*Sell Reason:* `{close_reason}`\n"
"*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n"
"*Amount:* `{amount:.8f}`\n" "*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n"
@ -253,7 +253,7 @@ class Telegram(RPCHandler):
if isinstance(sell_noti, str): if isinstance(sell_noti, str):
noti = sell_noti noti = sell_noti
else: else:
noti = sell_noti.get(str(msg['sell_reason']), default_noti) noti = sell_noti.get(str(msg['close_reason']), default_noti)
else: else:
noti = self._config['telegram'] \ noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), default_noti) .get('notification_settings', {}).get(str(msg_type), default_noti)
@ -306,7 +306,7 @@ class Telegram(RPCHandler):
return "\N{ROCKET}" return "\N{ROCKET}"
elif float(msg['profit_percent']) >= 0.0: elif float(msg['profit_percent']) >= 0.0:
return "\N{EIGHT SPOKED ASTERISK}" return "\N{EIGHT SPOKED ASTERISK}"
elif msg['sell_reason'] == "stop_loss": elif msg['close_reason'] == "stop_loss":
return"\N{WARNING SIGN}" return"\N{WARNING SIGN}"
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
@ -360,8 +360,8 @@ class Telegram(RPCHandler):
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_pct:.2f}%)`") "`({stoploss_current_dist_pct:.2f}%)`")
if r['open_order']: if r['open_order']:
if r['sell_order_status']: if r['close_order_status']:
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") lines.append("*Open Order:* `{open_order}` - `{close_order_status}`")
else: else:
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
@ -538,16 +538,16 @@ class Telegram(RPCHandler):
'force_sell': 'Forcesell', 'force_sell': 'Forcesell',
'emergency_sell': 'Emergency Sell', 'emergency_sell': 'Emergency Sell',
} }
sell_reasons_tabulate = [ close_reasons_tabulate = [
[ [
reason_map.get(reason, reason), reason_map.get(reason, reason),
sum(count.values()), sum(count.values()),
count['wins'], count['wins'],
count['losses'] count['losses']
] for reason, count in stats['sell_reasons'].items() ] for reason, count in stats['close_reasons'].items()
] ]
sell_reasons_msg = tabulate( close_reasons_msg = tabulate(
sell_reasons_tabulate, close_reasons_tabulate,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
) )
durations = stats['durations'] durations = stats['durations']
@ -559,7 +559,7 @@ class Telegram(RPCHandler):
], ],
headers=['', 'Avg. Duration'] headers=['', 'Avg. Duration']
) )
msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") msg = (f"""```\n{close_reasons_msg}```\n```\n{duration_msg}```""")
self._send_msg(msg, ParseMode.MARKDOWN) self._send_msg(msg, ParseMode.MARKDOWN)

View File

@ -32,11 +32,11 @@ class SellCheckTuple(object):
NamedTuple for Sell type + reason NamedTuple for Sell type + reason
""" """
sell_type: SellType sell_type: SellType
sell_reason: str = '' close_reason: str = ''
def __init__(self, sell_type: SellType, sell_reason: str = ''): def __init__(self, sell_type: SellType, close_reason: str = ''):
self.sell_type = sell_type self.sell_type = sell_type
self.sell_reason = sell_reason or sell_type.value self.close_reason = close_reason or sell_type.value
@property @property
def sell_flag(self): def sell_flag(self):
@ -225,7 +225,7 @@ class IStrategy(ABC, HyperStrategyMixin):
return True return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, close_reason: str,
current_time: datetime, **kwargs) -> bool: current_time: datetime, **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell order.
@ -242,7 +242,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason. :param close_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime
@ -586,7 +586,7 @@ class IStrategy(ABC, HyperStrategyMixin):
logger.debug(f"{trade.pair} - Sell signal received. " logger.debug(f"{trade.pair} - Sell signal received. "
f"sell_type=SellType.{sell_signal.name}" + f"sell_type=SellType.{sell_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else "")) (f", custom_reason={custom_reason}" if custom_reason else ""))
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason) return SellCheckTuple(sell_type=sell_signal, close_reason=custom_reason)
if stoplossflag.sell_flag: if stoplossflag.sell_flag:

View File

@ -185,7 +185,7 @@
"trades = load_backtest_data(backtest_dir)\n", "trades = load_backtest_data(backtest_dir)\n",
"\n", "\n",
"# Show value-counts per pair\n", "# Show value-counts per pair\n",
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" "trades.groupby(\"pair\")[\"close_reason\"].value_counts()"
] ]
}, },
{ {
@ -209,7 +209,7 @@
"trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n", "trades = load_trades_from_db(\"sqlite:///tradesv3.sqlite\")\n",
"\n", "\n",
"# Display results\n", "# Display results\n",
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" "trades.groupby(\"pair\")[\"close_reason\"].value_counts()"
] ]
}, },
{ {

View File

@ -62,7 +62,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
return True return True
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, rate: float, time_in_force: str, close_reason: str,
current_time: 'datetime', **kwargs) -> bool: current_time: 'datetime', **kwargs) -> bool:
""" """
Called right before placing a regular sell order. Called right before placing a regular sell order.
@ -79,7 +79,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount:
:param amount: Amount in quote currency. :param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders :param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason. :param close_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell'] 'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime :param current_time: datetime object, containing the current datetime