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
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)`)
* `min_date`: Start 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_profit = close_rate / open_rate - 1,
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>;
```
@ -78,7 +78,7 @@ SET is_open=0,
close_rate=0.19638016,
close_profit=0.0496,
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;
```

View File

@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date
class AwesomeStrategy(IStrategy):
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:
# Obtain pair dataframe.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
@ -490,7 +490,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods
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.
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 rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
:param close_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
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
# This is just a sample, please adjust to your needs
# (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)
# 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
@ -142,7 +142,7 @@ from freqtrade.data.btanalysis import load_trades_from_db
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
# Display results
trades.groupby("pair")["sell_reason"].value_counts()
trades.groupby("pair")["close_reason"].value_counts()
```
## Analyze the loaded trades for trade parallelism

View File

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

View File

@ -17,18 +17,18 @@ logger = logging.getLogger(__name__)
# Old format - maybe remove?
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
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']
# Newest format
BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'open_rate', 'close_rate',
'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',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ]

View File

@ -752,7 +752,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
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))
except ExchangeError:
@ -783,7 +783,7 @@ class FreqtradeBot(LoggingMixin):
# We check if stoploss order is fulfilled
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,
stoploss_order=True)
# Lock pair for one candle to prevent immediate rebuys
@ -1071,16 +1071,16 @@ class FreqtradeBot(LoggingMixin):
raise DependencyException(
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
:param trade: Trade instance
: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)
"""
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'
# 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}")
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!)
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,
# but we allow this value to be changed)
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)(
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)):
logger.info(f"User requested abortion of selling {trade.pair}")
return False
@ -1134,9 +1134,9 @@ class FreqtradeBot(LoggingMixin):
trade.orders.append(order_obj)
trade.open_order_id = order['id']
trade.sell_order_status = ''
trade.close_order_status = ''
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
if order.get('status', 'unknown') == 'closed':
self.update_trade_state(trade, trade.open_order_id, order)
@ -1176,7 +1176,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason,
'close_reason': trade.close_reason,
'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(),
'stake_currency': self.config['stake_currency'],
@ -1195,10 +1195,10 @@ class FreqtradeBot(LoggingMixin):
"""
Sends rpc notification when a sell cancel occurred.
"""
if trade.sell_order_status == reason:
if trade.close_order_status == reason:
return
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_trade = trade.calc_profit(rate=profit_rate)
@ -1219,7 +1219,7 @@ class FreqtradeBot(LoggingMixin):
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
'sell_reason': trade.sell_reason,
'close_reason': trade.close_reason,
'open_date': trade.open_date,
'close_date': trade.close_date,
'stake_currency': self.config['stake_currency'],

View File

@ -267,7 +267,7 @@ class Backtesting:
if sell.sell_flag:
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)
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,
rate=closerate,
time_in_force=time_in_force,
sell_reason=sell.sell_reason,
close_reason=sell.close_reason,
current_time=sell_row[DATE_IDX].to_pydatetime()):
return None
@ -329,7 +329,7 @@ class Backtesting:
sell_row = data[pair][-1]
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)
LocalTrade.close_bt_trade(trade)
# 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
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
: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 = []
for reason, count in results['sell_reason'].value_counts().iteritems():
result = results.loc[results['sell_reason'] == reason]
for reason, count in results['close_reason'].value_counts().iteritems():
result = results.loc[results['close_reason'] == reason]
profit_mean = result['profit_ratio'].mean()
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(
{
'sell_reason': reason,
'close_reason': reason,
'trades': count,
'wins': 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]
losing_trades = results.loc[results['profit_ratio'] < 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()))
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,
starting_balance=starting_balance,
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)
left_open_results = generate_pair_metrics(btdata, stake_currency=stake_currency,
starting_balance=starting_balance,
@ -335,7 +335,7 @@ def generate_strategy_stats(btdata: Dict[str, DataFrame],
'best_pair': best_pair,
'worst_pair': worst_pair,
'results_per_pair': pair_results,
'sell_reason_summary': sell_reason_stats,
'close_reason_summary': close_reason_stats,
'left_open_trades': left_open_results,
'total_trades': len(results),
'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")
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
:param sell_reason_stats: Sell reason metrics
:param close_reason_stats: Sell reason metrics
:param stake_currency: Stakecurrency used
: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 = [[
t['sell_reason'], t['trades'],
t['close_reason'], t['trades'],
_generate_wins_draws_losses(t['wins'], t['draws'], t['losses']),
t['profit_mean_pct'], t['profit_sum_pct'],
round_coin_value(t['profit_total_abs'], stake_currency, False),
t['profit_total_pct'],
] for t in sell_reason_stats]
] for t in close_reason_stats]
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(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)
if isinstance(table, str) and len(table) > 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')
max_rate = get_column_def(cols, 'max_rate', '0.0')
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')
# If ticker-interval existed use that, else null.
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(
cols, 'close_profit_abs',
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')
# 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,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy,
max_rate, min_rate, close_reason, close_order_status, strategy,
timeframe, open_trade_value, close_profit_abs
)
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_pct} initial_stop_loss_pct,
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{max_rate} max_rate, {min_rate} min_rate, {close_reason} close_reason,
{close_order_status} close_order_status,
{strategy} strategy, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
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:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param db_url: Database to use
:param clean_open_orders: Remove open orders from the database.
Useful for dry-run or if all orders have been reset on the exchange.
:return: None
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param db_url: Database to use
:param clean_open_orders: Remove open orders from the database.
Useful for dry-run or if all orders have been reset on the exchange.
:return: None
"""
kwargs = {}
@ -132,8 +132,9 @@ class Order(_DECL_BASE):
order_filled_date = Column(DateTime, nullable=True)
order_update_date = Column(DateTime, nullable=True)
def __repr__(self):
leverage = Column(Float, nullable=True)
def __repr__(self):
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})')
@ -155,6 +156,7 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
self.leverage = order.get('leverage', self.leverage)
if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -226,6 +228,7 @@ class LocalTrade():
fee_close_currency: str = ''
open_rate: float = 0.0
open_rate_requested: Optional[float] = None
# open_trade_value - calculated via _calc_open_trade_value
open_trade_value: float = 0.0
close_rate: Optional[float] = None
@ -254,11 +257,20 @@ class LocalTrade():
max_rate: float = 0.0
# Lowest price reached
min_rate: float = 0.0
sell_reason: str = ''
sell_order_status: str = ''
close_reason: str = ''
close_order_status: str = ''
strategy: str = ''
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):
for key in kwargs:
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_abs': self.close_profit_abs,
'sell_reason': self.sell_reason,
'sell_order_status': self.sell_order_status,
'close_reason': self.close_reason,
'close_order_status': self.close_order_status,
'stop_loss_abs': self.stop_loss,
'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None,
'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None,
@ -340,6 +352,13 @@ class LocalTrade():
'min_rate': self.min_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,
}
@ -379,6 +398,9 @@ class LocalTrade():
return
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
if not self.stop_loss:
@ -389,7 +411,7 @@ class LocalTrade():
# evaluate if the stop loss needs to be updated
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...")
self._set_new_stoploss(new_loss, stoploss)
else:
@ -403,6 +425,20 @@ class LocalTrade():
f"Trailing stoploss saved us: "
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:
"""
Updates this entity with amount and actual open/close rates.
@ -416,22 +452,24 @@ class LocalTrade():
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
self.open_rate = float(safe_value_fallback(order, 'average', 'price'))
self.amount = float(safe_value_fallback(order, 'filled', 'amount'))
self.recalc_open_trade_value()
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
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:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price'))
payment = "BUY" if self.isShort else "SELL"
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'):
self.stoploss_order_id = None
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:
logger.info(f'{order_type.upper()} is hit for {self}.')
self.close(safe_value_fallback(order, 'average', 'price'))
@ -445,11 +483,11 @@ class LocalTrade():
and marks trade as closed
"""
self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
self.is_open = False
self.sell_order_status = 'closed'
self.close_order_status = 'closed'
self.open_order_id = None
if show_msg:
logger.info(
@ -462,14 +500,14 @@ class LocalTrade():
"""
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_currency = fee_currency
if fee_rate is not None:
self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess
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_currency = fee_currency
if fee_rate is not None:
@ -479,9 +517,9 @@ class LocalTrade():
"""
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
elif side == 'sell':
elif self.is_closing_trade(side):
return self.fee_close_currency is not None
else:
return False
@ -494,9 +532,13 @@ class LocalTrade():
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
buy_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open)
return float(buy_trade + fees)
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = open_trade * Decimal(self.fee_open)
if (self.isShort):
return float(open_trade - fees)
else:
return float(open_trade + fees)
def recalc_open_trade_value(self) -> None:
"""
@ -513,34 +555,47 @@ class LocalTrade():
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
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
"""
if rate is None and not self.close_rate:
return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
close_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
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,
fee: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
: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).
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
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
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}")
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).
:param rate: rate to compare with (optional).
@ -554,7 +609,10 @@ class LocalTrade():
)
if self.open_trade_value == 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}")
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
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
def close_bt_trade(trade):
@ -700,8 +758,8 @@ class Trade(_DECL_BASE, LocalTrade):
max_rate = Column(Float, nullable=True, default=0.0)
# Lowest price reached
min_rate = Column(Float, nullable=True)
sell_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True)
close_reason = Column(String(100), nullable=True)
close_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), 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:
# Create description for sell summarizing the trade
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",
axis=1)
trade_buys = go.Scatter(

View File

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

View File

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

View File

@ -315,11 +315,11 @@ class RPC:
return 'draws'
trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
# Sell reason
sell_reasons = {}
close_reasons = {}
for trade in trades:
if trade.sell_reason not in sell_reasons:
sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1
if trade.close_reason not in close_reasons:
close_reasons[trade.close_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
close_reasons[trade.close_reason][trade_win_loss(trade)] += 1
# Duration
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'
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(
self, stake_currency: str, fiat_display_currency: str,
@ -539,8 +539,8 @@ class RPC:
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_sell_rate(trade.pair, False)
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, sell_reason)
close_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
self._freqtrade.execute_sell(trade, current_rate, close_reason)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:

View File

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

View File

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

View File

@ -185,7 +185,7 @@
"trades = load_backtest_data(backtest_dir)\n",
"\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",
"\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
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:
"""
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 rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
:param close_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime