stable/freqtrade/analyze.py

223 lines
8.3 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
2018-02-04 08:28:02 +00:00
from datetime import datetime, timedelta
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
2018-05-31 18:55:45 +00:00
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
2017-05-24 19:52:41 +00:00
2017-11-25 02:28:52 +00:00
2018-03-25 19:37:14 +00:00
logger = logging.getLogger(__name__)
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
"""
2018-02-04 08:28:02 +00:00
def __init__(self, config: dict) -> None:
"""
Init Analyze
:param config: Bot configuration (use the one from Configuration())
"""
self.config = config
2018-05-31 18:55:45 +00:00
self.strategy: IStrategy = StrategyResolver(self.config).strategy
2018-02-04 08:28:02 +00:00
@staticmethod
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',
})
2018-06-07 10:12:44 +00:00
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
2018-02-04 08:28:02 +00:00
return frame
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
return self.strategy.populate_indicators(dataframe=dataframe)
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
return self.strategy.populate_buy_trend(dataframe=dataframe)
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
return self.strategy.populate_sell_trend(dataframe=dataframe)
def get_ticker_interval(self) -> str:
"""
Return ticker interval to use
:return: Ticker interval value to use
"""
2018-06-02 12:10:15 +00:00
return self.strategy.ticker_interval
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 = self.parse_ticker_dataframe(ticker_history)
dataframe = self.populate_indicators(dataframe)
dataframe = self.populate_buy_trend(dataframe)
dataframe = self.populate_sell_trend(dataframe)
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() - timedelta(minutes=(interval_minutes + 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
"""
2018-06-22 18:10:05 +00:00
if buy and self.config.get('experimental', {}).get('ignore_roi_if_buy_signal', False):
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_rate=rate, 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
# Experimental: Check if the trade is profitable before selling it (avoid selling at loss)
if self.config.get('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 self.config.get('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
def min_roi_reached(self, trade: Trade, current_rate: float, current_time: datetime) -> bool:
"""
Based an earlier trade and current price and ROI configuration, decides whether bot should
sell
:return True if bot should sell at current rate
"""
current_profit = trade.calc_profit_percent(current_rate)
2018-06-02 12:10:15 +00:00
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
2018-03-25 19:37:14 +00:00
logger.debug('Stop loss hit.')
2018-02-04 08:28:02 +00:00
return True
# 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.populate_indicators(self.parse_ticker_dataframe(pair_data))
2018-03-02 15:22:00 +00:00
for pair, pair_data in tickerdata.items()}