stable/freqtrade/edge/__init__.py

358 lines
14 KiB
Python
Raw Normal View History

2018-09-24 17:22:30 +00:00
# pragma pylint: disable=W0603
""" Edge positioning package """
2018-09-21 15:41:31 +00:00
import logging
2018-09-24 17:22:30 +00:00
from typing import Any, Dict
2018-09-21 15:41:31 +00:00
import arrow
2018-09-23 02:51:53 +00:00
2018-09-24 17:22:30 +00:00
from pandas import DataFrame
2018-09-23 02:51:53 +00:00
import pandas as pd
2018-09-21 15:41:31 +00:00
import freqtrade.optimize as optimize
2018-09-23 02:51:53 +00:00
from freqtrade.optimize.backtesting import BacktestResult
2018-09-21 15:41:31 +00:00
from freqtrade.arguments import Arguments
from freqtrade.exchange import Exchange
from freqtrade.strategy.interface import SellType
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
from freqtrade.optimize.backtesting import Backtesting
2018-09-23 02:51:53 +00:00
import numpy as np
import utils_find_1st as utf1st
2018-09-21 15:41:31 +00:00
logger = logging.getLogger(__name__)
2018-09-24 17:22:30 +00:00
2018-09-21 15:41:31 +00:00
class Edge():
config: Dict = {}
2018-09-24 17:22:30 +00:00
def __init__(self, config: Dict[str, Any], exchange=None) -> None:
2018-09-21 15:41:31 +00:00
"""
constructor
"""
self.config = config
self.strategy: IStrategy = StrategyResolver(self.config).strategy
self.ticker_interval = self.strategy.ticker_interval
self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe
self.get_timeframe = Backtesting.get_timeframe
self.populate_buy_trend = self.strategy.populate_buy_trend
self.populate_sell_trend = self.strategy.populate_sell_trend
2018-09-24 17:22:30 +00:00
self.edge_config = self.config.get('edge', {})
2018-09-21 15:41:31 +00:00
self._last_updated = None
2018-09-26 14:03:51 +00:00
self._cached_pairs : list = []
self._total_capital = self.edge_config['total_capital_in_stake_currency']
self._allowed_risk = self.edge_config['allowed_risk']
2018-09-21 15:41:31 +00:00
###
#
###
if exchange is None:
self.config['exchange']['secret'] = ''
self.config['exchange']['password'] = ''
self.config['exchange']['uid'] = ''
self.config['dry_run'] = True
self.exchange = Exchange(self.config)
else:
self.exchange = exchange
self.fee = self.exchange.get_fee()
def calculate(self) -> bool:
pairs = self.config['exchange']['pair_whitelist']
heartbeat = self.config['edge']['process_throttle_secs']
if ((self._last_updated is not None) and (self._last_updated + heartbeat > arrow.utcnow().timestamp)):
return False
2018-09-26 14:03:51 +00:00
data: Dict[str, Any] = {}
2018-09-21 15:41:31 +00:00
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
logger.info('Using local backtesting data (using whitelist in given config) ...')
2018-09-24 17:22:30 +00:00
#TODO: add "timerange" to Edge config
2018-09-21 15:41:31 +00:00
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange
)
if not data:
logger.critical("No data found. Edge is stopped ...")
2018-09-26 14:03:51 +00:00
return False
2018-09-24 17:22:30 +00:00
2018-09-21 15:41:31 +00:00
preprocessed = self.tickerdata_to_dataframe(data)
# Print timeframe
min_date, max_date = self.get_timeframe(preprocessed)
logger.info(
'Measuring data from %s up to %s (%s days) ...',
min_date.isoformat(),
max_date.isoformat(),
(max_date - min_date).days
)
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
stoploss_range_min = float(self.edge_config.get('stoploss_range_min', -0.01))
stoploss_range_max = float(self.edge_config.get('stoploss_range_max', -0.05))
stoploss_range_step = float(self.edge_config.get('stoploss_range_step', -0.001))
stoploss_range = np.arange(stoploss_range_min, stoploss_range_max, stoploss_range_step)
2018-09-21 15:41:31 +00:00
########################### Call out BSlap Loop instead of Original BT code
2018-09-26 13:20:53 +00:00
trades: list = []
2018-09-21 15:41:31 +00:00
for pair, pair_data in preprocessed.items():
2018-09-24 17:22:30 +00:00
# Sorting dataframe by date and reset index
2018-09-21 15:41:31 +00:00
pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True)
ticker_data = self.populate_sell_trend(
self.populate_buy_trend(pair_data))[headers].copy()
2018-09-26 13:20:53 +00:00
trades += self._find_trades_for_stoploss_range(ticker_data, pair, stoploss_range)
2018-09-21 15:41:31 +00:00
2018-09-26 13:20:53 +00:00
# Switch List of Trade Dicts (trades) to Dataframe
2018-09-21 15:41:31 +00:00
# Fill missing, calculable columns, profit, duration , abs etc.
2018-09-26 13:20:53 +00:00
trades_df = DataFrame(trades)
2018-09-21 15:41:31 +00:00
2018-09-26 13:20:53 +00:00
if len(trades_df) > 0: # Only post process a frame if it has a record
trades_df = self._fill_calculable_fields(trades_df)
2018-09-21 15:41:31 +00:00
else:
2018-09-26 13:20:53 +00:00
trades_df = []
trades_df = DataFrame.from_records(trades_df, columns=BacktestResult._fields)
2018-09-24 17:22:30 +00:00
2018-09-26 13:20:53 +00:00
self._cached_pairs = self._process_expectancy(trades_df)
2018-09-21 15:41:31 +00:00
self._last_updated = arrow.utcnow().timestamp
return True
2018-09-24 17:22:30 +00:00
2018-09-26 13:20:53 +00:00
def stake_amount(self, pair: str) -> str:
info = [x for x in self._cached_pairs if x[0] == pair][0]
stoploss = info[1]
allowed_capital_at_risk = round(self._total_capital * self._allowed_risk, 5)
position_size = abs(round((allowed_capital_at_risk / stoploss), 5))
return position_size
def stoploss(self, pair: str) -> float:
info = [x for x in self._cached_pairs if x[0] == pair][0]
return info[1]
2018-09-26 14:03:51 +00:00
def sort_pairs(self, pairs) -> list:
2018-09-21 15:41:31 +00:00
if len(self._cached_pairs) == 0:
self.calculate()
edge_sorted_pairs = [x[0] for x in self._cached_pairs]
return [x for _, x in sorted(zip(edge_sorted_pairs,pairs), key=lambda pair: pair[0])]
2018-09-24 17:22:30 +00:00
2018-09-26 13:20:53 +00:00
def _fill_calculable_fields(self, result: DataFrame):
2018-09-21 15:41:31 +00:00
"""
2018-09-26 13:20:53 +00:00
The result frame contains a number of columns that are calculable
2018-09-21 15:41:31 +00:00
from othe columns. These are left blank till all rows are added,
to be populated in single vector calls.
Columns to be populated are:
- Profit
- trade duration
- profit abs
2018-09-26 13:20:53 +00:00
:param result Dataframe
:return: result Dataframe
2018-09-21 15:41:31 +00:00
"""
# stake and fees
# stake = 0.015
# 0.05% is 0.0005
# fee = 0.001
stake = self.config.get('stake_amount')
fee = self.fee
open_fee = fee / 2
close_fee = fee / 2
2018-09-26 13:20:53 +00:00
result['trade_duration'] = result['close_time'] - result['open_time']
result['trade_duration'] = result['trade_duration'].map(lambda x: int(x.total_seconds() / 60))
2018-09-21 15:41:31 +00:00
## Spends, Takes, Profit, Absolute Profit
2018-09-26 13:20:53 +00:00
2018-09-21 15:41:31 +00:00
# Buy Price
2018-09-26 13:20:53 +00:00
result['buy_vol'] = stake / result['open_rate'] # How many target are we buying
result['buy_fee'] = stake * open_fee
result['buy_spend'] = stake + result['buy_fee'] # How much we're spending
2018-09-21 15:41:31 +00:00
# Sell price
2018-09-26 13:20:53 +00:00
result['sell_sum'] = result['buy_vol'] * result['close_rate']
result['sell_fee'] = result['sell_sum'] * close_fee
result['sell_take'] = result['sell_sum'] - result['sell_fee']
2018-09-21 15:41:31 +00:00
# profit_percent
2018-09-26 13:20:53 +00:00
result['profit_percent'] = (result['sell_take'] - result['buy_spend']) \
/ result['buy_spend']
2018-09-21 15:41:31 +00:00
# Absolute profit
2018-09-26 13:20:53 +00:00
result['profit_abs'] = result['sell_take'] - result['buy_spend']
2018-09-21 15:41:31 +00:00
2018-09-26 13:20:53 +00:00
return result
2018-09-21 15:41:31 +00:00
2018-09-26 14:03:51 +00:00
def _process_expectancy(self, results: DataFrame) -> list:
2018-09-21 15:41:31 +00:00
"""
2018-09-24 17:22:30 +00:00
This is a temporary version of edge positioning calculation.
The function will be eventually moved to a plugin called Edge in order to calculate necessary WR, RRR and
other indictaors related to money management periodically (each X minutes) and keep it in a storage.
The calulation will be done per pair and per strategy.
2018-09-21 15:41:31 +00:00
"""
# Removing pairs having less than min_trades_number
min_trades_number = self.edge_config.get('min_trade_number', 15)
2018-09-21 15:41:31 +00:00
results = results.groupby('pair').filter(lambda x: len(x) > min_trades_number)
###################################
2018-09-24 17:22:30 +00:00
# Removing outliers (Only Pumps) from the dataset
2018-09-21 15:41:31 +00:00
# The method to detect outliers is to calculate standard deviation
# Then every value more than (standard deviation + 2*average) is out (pump)
#
# Calculating standard deviation of profits
std = results[["profit_abs"]].std()
#
# Calculating average of profits
avg = results[["profit_abs"]].mean()
#
# Removing Pumps
if self.edge_config.get('remove_pumps', True):
results = results[results.profit_abs < float(avg + 2*std)]
2018-09-21 15:41:31 +00:00
##########################################################################
# Removing trades having a duration more than X minutes (set in config)
max_trade_duration = self.edge_config.get('max_trade_duration_minute', 1440)
2018-09-21 15:41:31 +00:00
results = results[results.trade_duration < max_trade_duration]
#######################################################################
# Win Rate is the number of profitable trades
# Divided by number of trades
def winrate(x):
x = x[x > 0].count() / x.count()
return x
#############################
# Risk Reward Ratio
# 1 / ((loss money / losing trades) / (gained money / winning trades))
def risk_reward_ratio(x):
x = abs(1/ ((x[x<0].sum() / x[x < 0].count()) / (x[x > 0].sum() / x[x > 0].count())))
return x
##############################
# Required Risk Reward
# (1/(winrate - 1)
def required_risk_reward(x):
x = (1/(x[x > 0].count()/x.count()) -1)
return x
##############################
2018-09-24 17:22:30 +00:00
def delta(x):
x = (abs(1/ ((x[x < 0].sum() / x[x < 0].count()) / (x[x > 0].sum() / x[x > 0].count())))) - (1/(x[x > 0].count()/x.count()) -1)
return x
# Expectancy
# Tells you the interest percentage you should hope
# E.x. if expectancy is 0.35, on $1 trade you should expect a target of $1.35
def expectancy(x):
average_win = float(x[x > 0].sum() / x[x > 0].count())
average_loss = float(abs(x[x < 0].sum() / x[x < 0].count()))
winrate = float(x[x > 0].count()/x.count())
x = ((1 + average_win/average_loss) * winrate) - 1
2018-09-21 15:41:31 +00:00
return x
##############################
2018-09-24 17:22:30 +00:00
2018-09-21 15:41:31 +00:00
final = results.groupby(['pair', 'stoploss'])['profit_abs'].\
2018-09-24 17:22:30 +00:00
agg([winrate, risk_reward_ratio, required_risk_reward, expectancy, delta]).\
reset_index().sort_values(by=['expectancy', 'stoploss'], ascending=False)\
.groupby('pair').first().sort_values(by=['expectancy'], ascending=False)
2018-09-24 17:22:30 +00:00
# Returning an array of pairs in order of "expectancy"
2018-09-21 15:41:31 +00:00
return final.reset_index().values
2018-09-26 13:20:53 +00:00
def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range):
buy_column = ticker_data['buy'].values
sell_column = ticker_data['sell'].values
date_column = ticker_data['date'].values
ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values
result: list = []
for stoploss in stoploss_range:
result += self._detect_stop_and_sell_points(buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair)
return result
def _detect_stop_and_sell_points(self, buy_column, sell_column, date_column, ohlc_columns, stoploss, pair, start_point=0):
result: list = []
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
#open_trade_index = np.argmax(buy_column == 1)
# return empty if we don't find trade entry (i.e. buy==1)
if open_trade_index == -1:
return []
stop_price_percentage = stoploss + 1
open_price = ohlc_columns[open_trade_index + 1, 0]
stop_price = (open_price * stop_price_percentage)
# Searching for the index where stoploss is hit
stop_index = utf1st.find_1st(ohlc_columns[open_trade_index + 1:, 2], stop_price, utf1st.cmp_smaller)
# If we don't find it then we assume stop_index will be far in future (infinite number)
if stop_index == -1:
stop_index = float('inf')
#stop_index = np.argmax((ohlc_columns[open_trade_index + 1:, 2] < stop_price) == True)
# Searching for the index where sell is hit
sell_index = utf1st.find_1st(sell_column[open_trade_index + 1:], 1, utf1st.cmp_equal)
# If we don't find it then we assume sell_index will be far in future (infinite number)
if sell_index == -1:
sell_index = float('inf')
#sell_index = np.argmax(sell_column[open_trade_index + 1:] == 1)
# Check if we don't find any stop or sell point (in that case trade remains open)
# It is not interesting for Edge to consider it so we simply ignore the trade
# And stop iterating as the party is over
if stop_index == sell_index == float('inf'):
return []
if stop_index <= sell_index:
exit_index = open_trade_index + stop_index + 1
exit_type = SellType.STOP_LOSS
exit_price = stop_price
elif stop_index > sell_index:
exit_index = open_trade_index + sell_index + 1
exit_type = SellType.SELL_SIGNAL
exit_price = ohlc_columns[open_trade_index + sell_index + 1, 0]
2018-09-26 14:03:51 +00:00
trade = {'pair': pair,
'stoploss': stoploss,
'profit_percent': '',
'profit_abs': '',
'open_time': date_column[open_trade_index],
'close_time': date_column[exit_index],
'open_index': start_point + open_trade_index + 1,
'close_index': start_point + exit_index,
'trade_duration': '',
'open_rate': round(open_price, 15),
'close_rate': round(exit_price, 15),
'exit_type': exit_type
}
2018-09-26 13:20:53 +00:00
result.append(trade)
return result + self._detect_stop_and_sell_points(
buy_column[exit_index:],
sell_column[exit_index:],
date_column[exit_index:],
ohlc_columns[exit_index:],
stoploss,
pair,
(start_point + exit_index)
)