181 lines
6.7 KiB
Python
181 lines
6.7 KiB
Python
# pragma pylint: disable=missing-docstring,W0212
|
|
|
|
|
|
import logging
|
|
from typing import Tuple, Dict
|
|
|
|
import arrow
|
|
from pandas import DataFrame
|
|
from tabulate import tabulate
|
|
|
|
from freqtrade import exchange
|
|
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
|
from freqtrade.exchange import Bittrex
|
|
from freqtrade.main import min_roi_reached
|
|
from freqtrade.misc import load_config
|
|
from freqtrade.optimize import load_data, preprocess
|
|
from freqtrade.persistence import Trade
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
|
"""
|
|
Get the maximum timeframe for the given backtest data
|
|
:param data: dictionary with backtesting data
|
|
:return: tuple containing min_date, max_date
|
|
"""
|
|
min_date, max_date = None, None
|
|
for values in data.values():
|
|
sorted_values = sorted(values, key=lambda d: arrow.get(d['T']))
|
|
if not min_date or sorted_values[0]['T'] < min_date:
|
|
min_date = sorted_values[0]['T']
|
|
if not max_date or sorted_values[-1]['T'] > max_date:
|
|
max_date = sorted_values[-1]['T']
|
|
return arrow.get(min_date), arrow.get(max_date)
|
|
|
|
|
|
def generate_text_table(
|
|
data: Dict[str, Dict], results: DataFrame, stake_currency, ticker_interval) -> str:
|
|
"""
|
|
Generates and returns a text table for the given backtest data and the results dataframe
|
|
:return: pretty printed table with tabulate as str
|
|
"""
|
|
floatfmt = ('s', 'd', '.2f', '.8f', '.1f')
|
|
tabular_data = []
|
|
headers = ['pair', 'buy count', 'avg profit %',
|
|
'total profit ' + stake_currency, 'avg duration']
|
|
for pair in data:
|
|
result = results[results.currency == pair]
|
|
tabular_data.append([
|
|
pair,
|
|
len(result.index),
|
|
result.profit_percent.mean() * 100.0,
|
|
result.profit_BTC.sum(),
|
|
result.duration.mean() * ticker_interval,
|
|
])
|
|
|
|
# Append Total
|
|
tabular_data.append([
|
|
'TOTAL',
|
|
len(results.index),
|
|
results.profit_percent.mean() * 100.0,
|
|
results.profit_BTC.sum(),
|
|
results.duration.mean() * ticker_interval,
|
|
])
|
|
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
|
|
|
|
|
|
def backtest(stake_amount: float, processed: Dict[str, DataFrame],
|
|
max_open_trades: int = 0, realistic: bool = True) -> DataFrame:
|
|
"""
|
|
Implements backtesting functionality
|
|
:param stake_amount: btc amount to use for each trade
|
|
:param processed: a processed dictionary with format {pair, data}
|
|
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
|
|
:param realistic: do we try to simulate realistic trades? (default: True)
|
|
:return: DataFrame
|
|
"""
|
|
trades = []
|
|
trade_count_lock: dict = {}
|
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
for pair, pair_data in processed.items():
|
|
pair_data['buy'], pair_data['sell'] = 0, 0
|
|
ticker = populate_sell_trend(populate_buy_trend(pair_data))
|
|
# for each buy point
|
|
lock_pair_until = None
|
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
|
if realistic:
|
|
if lock_pair_until is not None and row.Index <= lock_pair_until:
|
|
continue
|
|
if max_open_trades > 0:
|
|
# Check if max_open_trades has already been reached for the given date
|
|
if not trade_count_lock.get(row.date, 0) < max_open_trades:
|
|
continue
|
|
|
|
if max_open_trades > 0:
|
|
# Increase lock
|
|
trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1
|
|
|
|
trade = Trade(
|
|
open_rate=row.close,
|
|
open_date=row.date,
|
|
stake_amount=stake_amount,
|
|
amount=stake_amount / row.open,
|
|
fee=exchange.get_fee()
|
|
)
|
|
|
|
# calculate win/lose forwards from buy point
|
|
for row2 in ticker[row.Index + 1:].itertuples(index=True):
|
|
if max_open_trades > 0:
|
|
# Increase trade_count_lock for every iteration
|
|
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
|
|
|
|
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
|
|
current_profit_percent = trade.calc_profit_percent(rate=row2.close)
|
|
current_profit_btc = trade.calc_profit(rate=row2.close)
|
|
lock_pair_until = row2.Index
|
|
|
|
trades.append(
|
|
(
|
|
pair,
|
|
current_profit_percent,
|
|
current_profit_btc,
|
|
row2.Index - row.Index
|
|
)
|
|
)
|
|
break
|
|
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
|
|
return DataFrame.from_records(trades, columns=labels)
|
|
|
|
|
|
def start(args):
|
|
# Initialize logger
|
|
logging.basicConfig(
|
|
level=args.loglevel,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
)
|
|
|
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
|
|
|
logger.info('Using config: %s ...', args.config)
|
|
config = load_config(args.config)
|
|
|
|
logger.info('Using ticker_interval: %s ...', args.ticker_interval)
|
|
|
|
data = {}
|
|
pairs = config['exchange']['pair_whitelist']
|
|
if args.live:
|
|
logger.info('Downloading data for all pairs in whitelist ...')
|
|
for pair in pairs:
|
|
data[pair] = exchange.get_ticker_history(pair, args.ticker_interval)
|
|
else:
|
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
|
data = load_data(pairs=pairs, ticker_interval=args.ticker_interval,
|
|
refresh_pairs=args.refresh_pairs)
|
|
|
|
logger.info('Using stake_currency: %s ...', config['stake_currency'])
|
|
logger.info('Using stake_amount: %s ...', config['stake_amount'])
|
|
|
|
# Print timeframe
|
|
min_date, max_date = get_timeframe(data)
|
|
logger.info('Measuring data from %s up to %s ...', min_date.isoformat(), max_date.isoformat())
|
|
|
|
max_open_trades = 0
|
|
if args.realistic_simulation:
|
|
logger.info('Using max_open_trades: %s ...', config['max_open_trades'])
|
|
max_open_trades = config['max_open_trades']
|
|
|
|
# Monkey patch config
|
|
from freqtrade import main
|
|
main._CONF = config
|
|
|
|
# Execute backtest and print results
|
|
results = backtest(
|
|
config['stake_amount'], preprocess(data), max_open_trades, args.realistic_simulation
|
|
)
|
|
logger.info(
|
|
'\n====================== BACKTESTING REPORT ================================\n%s',
|
|
generate_text_table(data, results, config['stake_currency'], args.ticker_interval)
|
|
)
|