Skip to content

Commit

Permalink
Merge pull request freqtrade#1018 from freqtrade/feat/sell_reason
Browse files Browse the repository at this point in the history
Record sell reason
  • Loading branch information
vertti authored Jul 24, 2018
2 parents ab67822 + 0775a37 commit 0b31905
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 43 deletions.
4 changes: 3 additions & 1 deletion docs/backtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ with filename.open() as file:
data = json.load(file)

columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end"]
"open_rate", "close_rate", "open_at_end", "sell_reason"]
df = pd.DataFrame(data, columns=columns)

df['opents'] = pd.to_datetime(df['opents'],
Expand All @@ -98,6 +98,8 @@ df['closets'] = pd.to_datetime(df['closets'],
)
```

If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it.

#### Exporting trades to file specifying a custom filename

```bash
Expand Down
15 changes: 10 additions & 5 deletions freqtrade/freqtradebot.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from freqtrade.persistence import Trade
from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.state import State
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -53,7 +54,6 @@ def __init__(self, config: Dict[str, Any])-> None:
self.rpc: RPCManager = RPCManager(self)
self.persistence = None
self.exchange = Exchange(self.config)

self._init_modules()

def _init_modules(self) -> None:
Expand Down Expand Up @@ -392,7 +392,9 @@ def execute_buy(self, pair: str, stake_amount: float) -> bool:
open_rate_requested=buy_limit,
open_date=datetime.utcnow(),
exchange=self.exchange.id,
open_order_id=order_id
open_order_id=order_id,
strategy=self.strategy.get_strategy_name(),
ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']]
)
Trade.session.add(trade)
Trade.session.flush()
Expand Down Expand Up @@ -505,8 +507,9 @@ def handle_trade(self, trade: Trade) -> bool:
(buy, sell) = self.strategy.get_signal(self.exchange,
trade.pair, self.strategy.ticker_interval)

if self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
if should_sell.sell_flag:
self.execute_sell(trade, current_rate, should_sell.sell_type)
return True
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False
Expand Down Expand Up @@ -607,17 +610,19 @@ def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
# TODO: figure out how to handle partially complete sell orders
return False

def execute_sell(self, trade: Trade, limit: float) -> None:
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
"""
Executes a limit sell for the given trade and limit
:param trade: Trade instance
:param limit: limit rate for the sell order
:param sellreason: Reason the sell was triggered
:return: None
"""
# Execute sell and update trade record
order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id
trade.close_rate_requested = limit
trade.sell_reason = sell_reason.value

profit_trade = trade.calc_profit(rate=limit)
current_rate = self.exchange.get_ticker(trade.pair)['bid']
Expand Down
49 changes: 37 additions & 12 deletions freqtrade/optimize/backtesting.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver

logger = logging.getLogger(__name__)
Expand All @@ -40,6 +41,7 @@ class BacktestResult(NamedTuple):
open_at_end: bool
open_rate: float
close_rate: float
sell_reason: SellType


class Backtesting(object):
Expand Down Expand Up @@ -120,11 +122,21 @@ def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str
])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")

def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
"""
Generate small table outlining Backtest results
"""
tabular_data = []
headers = ['Sell Reason', 'Count']
for reason, count in results['sell_reason'].value_counts().iteritems():
tabular_data.append([reason.value, count])
return tabulate(tabular_data, headers=headers, tablefmt="pipe")

def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:

records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
t.open_rate, t.close_rate, t.open_at_end)
t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value)
for index, t in results.iterrows()]

if records:
Expand Down Expand Up @@ -153,8 +165,9 @@ def _get_sell_trade_entry(
trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1

buy_signal = sell_row.buy
if self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
sell_row.sell):
sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal,
sell_row.sell)
if sell.sell_flag:

return BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.open),
Expand All @@ -167,7 +180,8 @@ def _get_sell_trade_entry(
close_index=sell_row.Index,
open_at_end=False,
open_rate=buy_row.open,
close_rate=sell_row.open
close_rate=sell_row.open,
sell_reason=sell.sell_type
)
if partial_ticker:
# no sell condition found - trade stil open at end of backtest period
Expand All @@ -183,7 +197,8 @@ def _get_sell_trade_entry(
close_index=sell_row.Index,
open_at_end=True,
open_rate=buy_row.open,
close_rate=sell_row.open
close_rate=sell_row.open,
sell_reason=SellType.FORCE_SELL
)
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs)
Expand Down Expand Up @@ -318,21 +333,31 @@ def start(self) -> None:
self._store_backtest_result(self.config.get('exportfilename'), results)

logger.info(
'\n================================================= '
'BACKTESTING REPORT'
' ==================================================\n'
'\n' + '=' * 49 +
' BACKTESTING REPORT ' +
'=' * 50 + '\n'
'%s',
self._generate_text_table(
data,
results
)
)
# logger.info(
# results[['sell_reason']].groupby('sell_reason').count()
# )

logger.info(
'\n=============================================== '
'LEFT OPEN TRADES REPORT'
' ===============================================\n'
'%s',
'\n' +
' SELL READON STATS '.center(119, '=') +
'\n%s \n',
self._generate_text_table_sell_reason(data, results)

)

logger.info(
'\n' +
' LEFT OPEN TRADES REPORT '.center(119, '=') +
'\n%s',
self._generate_text_table(
data,
results.loc[results.open_at_end]
Expand Down
12 changes: 10 additions & 2 deletions freqtrade/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ def check_migrate(engine) -> None:
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
max_rate = get_column_def(cols, 'max_rate', '0.0')
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
ticker_interval = get_column_def(cols, 'ticker_interval', 'null')

# Schema migration necessary
engine.execute(f"alter table trades rename to {table_back_name}")
Expand All @@ -101,7 +104,8 @@ def check_migrate(engine) -> None:
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, max_rate
stop_loss, initial_stop_loss, max_rate, sell_reason, strategy,
ticker_interval
)
select id, lower(exchange),
case
Expand All @@ -116,7 +120,8 @@ def check_migrate(engine) -> None:
{close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id,
{stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{max_rate} max_rate
{max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy,
{ticker_interval} ticker_interval
from {table_back_name}
""")

Expand Down Expand Up @@ -172,6 +177,9 @@ class Trade(_DECL_BASE):
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
sell_reason = Column(String, nullable=True)
strategy = Column(String, nullable=True)
ticker_interval = Column(Integer, nullable=True)

def __repr__(self):
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
Expand Down
3 changes: 2 additions & 1 deletion freqtrade/rpc/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade
from freqtrade.state import State
from freqtrade.strategy.interface import SellType

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -344,7 +345,7 @@ def _exec_forcesell(trade: Trade) -> None:

# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
self._freqtrade.execute_sell(trade, current_rate)
self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL)
# ---- EOF def _exec_forcesell ----

if self._freqtrade.state != State.RUNNING:
Expand Down
57 changes: 43 additions & 14 deletions freqtrade/strategy/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from abc import ABC, abstractmethod
from datetime import datetime
from enum import Enum
from typing import Dict, List, Tuple
from typing import Dict, List, NamedTuple, Tuple

import arrow
from pandas import DataFrame
Expand All @@ -27,6 +27,26 @@ class SignalType(Enum):
SELL = "sell"


class SellType(Enum):
"""
Enum to distinguish between sell reasons
"""
ROI = "roi"
STOP_LOSS = "stop_loss"
TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell"
NONE = ""


class SellCheckTuple(NamedTuple):
"""
NamedTuple for Sell type + reason
"""
sell_flag: bool
sell_type: SellType


class IStrategy(ABC):
"""
Interface for freqtrade strategies
Expand Down Expand Up @@ -69,6 +89,12 @@ def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
:return: DataFrame with sell column
"""

def get_strategy_name(self) -> str:
"""
Returns strategy class name
"""
return self.__class__.__name__

def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
Expand Down Expand Up @@ -137,40 +163,42 @@ def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool
)
return buy, sell

def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
sell: bool) -> SellCheckTuple:
"""
This function evaluate if on the condition required to trigger a sell has been reached
if the threshold is reached and updates the trade record.
:return: True if trade should be sold, False otherwise
"""
current_profit = trade.calc_profit_percent(rate)
if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
current_profit=current_profit):
return True
stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
current_profit=current_profit)
if stoplossflag.sell_flag:
return stoplossflag

experimental = self.config.get('experimental', {})

if buy and experimental.get('ignore_roi_if_buy_signal', False):
logger.debug('Buy signal still active - not selling.')
return False
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)

# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..')
return True
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)

if experimental.get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..')
if trade.calc_profit(rate=rate) <= 0:
return False
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..')
return True
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)

return False
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)

def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
current_profit: float) -> bool:
current_profit: float) -> SellCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
Expand All @@ -182,8 +210,9 @@ def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: dat

# evaluate if the stoploss was hit
if self.stoploss is not None and trade.stop_loss >= current_rate:

selltype = SellType.STOP_LOSS
if trailing_stop:
selltype = SellType.TRAILING_STOP_LOSS
logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, "
Expand All @@ -192,7 +221,7 @@ def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: dat
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")

logger.debug('Stop loss hit.')
return True
return SellCheckTuple(sell_flag=True, sell_type=selltype)

# update the stop loss afterwards, after all by definition it's supposed to be hanging
if trailing_stop:
Expand All @@ -209,7 +238,7 @@ def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: dat

trade.adjust_stop_loss(current_rate, stop_loss_value)

return False
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)

def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
"""
Expand Down
Loading

0 comments on commit 0b31905

Please sign in to comment.