stable/freqtrade/analyze.py

232 lines
8.6 KiB
Python
Raw Normal View History

2017-11-18 07:34:32 +00:00
"""
Functions to analyze ticker data with indicators and produce buy and sell signals
"""
2018-03-25 19:37:14 +00:00
import logging
from datetime import datetime
2018-02-04 08:28:02 +00:00
from enum import Enum
from typing import Dict, List, Tuple
2018-03-17 21:44:47 +00:00
2018-03-02 15:22:00 +00:00
import arrow
from pandas import DataFrame, to_datetime
2018-03-17 21:44:47 +00:00
from freqtrade import constants
2018-06-17 10:41:33 +00:00
from freqtrade.exchange import Exchange
2018-02-04 08:28:02 +00:00
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import IStrategy
2017-11-25 02:28:52 +00:00
2018-03-25 19:37:14 +00:00
logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
Analyses the trend for the given ticker history
:param ticker: See exchange.get_ticker_history
:return: DataFrame
"""
cols = ['date', 'open', 'high', 'low', 'close', 'volume']
frame = DataFrame(ticker, columns=cols)
frame['date'] = to_datetime(frame['date'],
unit='ms',
utc=True,
infer_datetime_format=True)
# group by index and aggregate results to eliminate duplicate ticks
frame = frame.groupby(by='date', as_index=False, sort=True).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'max',
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
2017-11-14 17:06:03 +00:00
class SignalType(Enum):
"""
2018-02-04 08:28:02 +00:00
Enum to distinguish between buy and sell signals
2017-11-14 18:28:31 +00:00
"""
2018-02-04 08:28:02 +00:00
BUY = "buy"
SELL = "sell"
2017-10-06 10:22:04 +00:00
2018-02-04 08:28:02 +00:00
class Analyze(object):
2017-05-24 19:52:41 +00:00
"""
2018-02-04 08:28:02 +00:00
Analyze class contains everything the bot need to determine if the situation is good for
buying or selling.
2017-05-24 19:52:41 +00:00
"""
def __init__(self, config: dict, strategy: IStrategy) -> None:
2018-02-04 08:28:02 +00:00
"""
Init Analyze
:param config: Bot configuration (use the one from Configuration())
"""
self.config = config
self.strategy = strategy
2018-02-04 08:28:02 +00:00
def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame:
"""
Parses the given ticker history and returns a populated DataFrame
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
dataframe = parse_ticker_dataframe(ticker_history)
dataframe = self.strategy.populate_indicators(dataframe)
dataframe = self.strategy.populate_buy_trend(dataframe)
dataframe = self.strategy.populate_sell_trend(dataframe)
2018-02-04 08:28:02 +00:00
return dataframe
2018-06-17 10:41:33 +00:00
def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]:
2018-02-04 08:28:02 +00:00
"""
Calculates current signal based several technical analysis indicators
:param pair: pair in format ANT/BTC
2018-02-07 04:22:17 +00:00
:param interval: Interval to use (in min)
2018-02-04 08:28:02 +00:00
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
"""
2018-06-17 10:41:33 +00:00
ticker_hist = exchange.get_ticker_history(pair, interval)
2018-02-04 08:28:02 +00:00
if not ticker_hist:
2018-03-25 19:37:14 +00:00
logger.warning('Empty ticker history for pair %s', pair)
return False, False
2018-02-04 08:28:02 +00:00
try:
dataframe = self.analyze_ticker(ticker_hist)
except ValueError as error:
2018-03-25 19:37:14 +00:00
logger.warning(
2018-02-04 08:28:02 +00:00
'Unable to analyze ticker for pair %s: %s',
pair,
str(error)
)
return False, False
2018-02-04 08:28:02 +00:00
except Exception as error:
2018-03-25 19:37:14 +00:00
logger.exception(
2018-02-04 08:28:02 +00:00
'Unexpected error when analyzing ticker for pair %s: %s',
pair,
str(error)
)
return False, False
2018-02-04 08:28:02 +00:00
if dataframe.empty:
2018-03-25 19:37:14 +00:00
logger.warning('Empty dataframe for pair %s', pair)
return False, False
2018-02-04 08:28:02 +00:00
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
signal_date = arrow.get(latest['date'])
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
2018-03-25 19:37:14 +00:00
logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old',
pair,
(arrow.utcnow() - signal_date).seconds // 60
)
return False, False
2018-02-04 08:28:02 +00:00
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
2018-03-25 19:37:14 +00:00
logger.debug(
2018-02-04 08:28:02 +00:00
'trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'],
pair,
str(buy),
str(sell)
)
return buy, sell
2018-02-04 08:28:02 +00:00
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool:
"""
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)
2018-07-07 18:17:53 +00:00
if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date,
current_profit=current_profit):
return True
experimental = self.config.get('experimental', {})
if buy and experimental.get('ignore_roi_if_buy_signal', False):
2018-06-22 18:10:05 +00:00
logger.debug('Buy signal still active - not selling.')
return False
2018-02-04 08:28:02 +00:00
# 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):
2018-03-25 19:37:14 +00:00
logger.debug('Required profit reached. Selling..')
2018-02-04 08:28:02 +00:00
return True
if experimental.get('sell_profit_only', False):
2018-03-25 19:37:14 +00:00
logger.debug('Checking if trade is profitable..')
2018-02-04 08:28:02 +00:00
if trade.calc_profit(rate=rate) <= 0:
return False
if sell and not buy and experimental.get('use_sell_signal', False):
2018-03-25 19:37:14 +00:00
logger.debug('Sell signal received. Selling..')
2018-02-04 08:28:02 +00:00
return True
return False
2018-07-07 18:17:53 +00:00
def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime,
current_profit: float) -> bool:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
"""
trailing_stop = self.config.get('trailing_stop', False)
2018-06-27 04:38:49 +00:00
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
if trailing_stop:
logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, "
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
logger.debug('Stop loss hit.')
return True
# update the stop loss afterwards, after all by definition it's supposed to be hanging
if trailing_stop:
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.strategy.stoploss
if 'trailing_stop_positive' in self.config and current_profit > 0:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
f"since we have profit {current_profit}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
return False
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:
2018-02-04 08:28:02 +00:00
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell
:return True if bot should sell at current rate
"""
# Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
2018-06-02 12:10:15 +00:00
for duration, threshold in self.strategy.minimal_roi.items():
if time_diff <= duration:
return False
if current_profit > threshold:
return True
2018-02-04 08:28:02 +00:00
return False
2018-02-07 04:22:17 +00:00
def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]:
"""
Creates a dataframe and populates indicators for given ticker data
"""
return {pair: self.strategy.populate_indicators(parse_ticker_dataframe(pair_data))
2018-03-02 15:22:00 +00:00
for pair, pair_data in tickerdata.items()}