Backtesting: Offline runs with test data, use argument '--backtesting'
This commit is contained in:
parent
5e0f143a38
commit
c88c3f9b84
@ -6,7 +6,8 @@ import arrow
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exchange import get_ticker_history
|
||||
from freqtrade.exchange import backtesting, get_ticker_history
|
||||
from freqtrade.misc import State, get_state
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
@ -23,6 +24,10 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame
|
||||
.drop('BV', 1) \
|
||||
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
|
||||
.sort_values('date')
|
||||
|
||||
if get_state() == State.BACKTESTING:
|
||||
return df
|
||||
|
||||
return df[df['date'].map(arrow.get) > minimum_date]
|
||||
|
||||
|
||||
@ -69,16 +74,20 @@ def analyze_ticker(pair: str) -> DataFrame:
|
||||
add several TA indicators and buy signal to it
|
||||
:return DataFrame with ticker data and indicator data
|
||||
"""
|
||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||
if get_state() == State.BACKTESTING:
|
||||
minimum_date = backtesting.get_minimum_date(pair)
|
||||
else:
|
||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||
|
||||
data = get_ticker_history(pair, minimum_date)
|
||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||
|
||||
if dataframe.empty:
|
||||
logger.warning('Empty dataframe for pair %s', pair)
|
||||
return dataframe
|
||||
|
||||
dataframe = populate_indicators(dataframe)
|
||||
dataframe = populate_buy_trend(dataframe)
|
||||
|
||||
return dataframe
|
||||
|
||||
|
||||
@ -95,10 +104,11 @@ def get_buy_signal(pair: str) -> bool:
|
||||
|
||||
latest = dataframe.iloc[-1]
|
||||
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
if get_state() != State.BACKTESTING:
|
||||
# Check if dataframe is out of date
|
||||
signal_date = arrow.get(latest['date'])
|
||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||
return False
|
||||
|
||||
signal = latest['buy'] == 1
|
||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
||||
|
@ -4,8 +4,10 @@ from typing import List
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade.exchange.backtesting import Backtesting
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
from freqtrade.misc import State, get_state
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -33,22 +35,24 @@ def init(config: dict) -> None:
|
||||
|
||||
_CONF.update(config)
|
||||
|
||||
if config['dry_run']:
|
||||
if get_state() == State.BACKTESTING:
|
||||
logger.info('Instance is running with backtesting enabled')
|
||||
EXCHANGE = Backtesting(config['exchange'])
|
||||
return
|
||||
elif config['dry_run']:
|
||||
logger.info('Instance is running with dry_run enabled')
|
||||
|
||||
exchange_config = config['exchange']
|
||||
|
||||
# Find matching class for the given exchange name
|
||||
name = exchange_config['name']
|
||||
name = _CONF['exchange']['name']
|
||||
try:
|
||||
exchange_class = Exchanges[name.upper()].value
|
||||
except KeyError:
|
||||
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||
|
||||
EXCHANGE = exchange_class(exchange_config)
|
||||
EXCHANGE = exchange_class(_CONF['exchange'])
|
||||
|
||||
# Check if all pairs are available
|
||||
validate_pairs(config['exchange']['pair_whitelist'])
|
||||
validate_pairs(_CONF['exchange']['pair_whitelist'])
|
||||
|
||||
|
||||
def validate_pairs(pairs: List[str]) -> None:
|
||||
|
220
freqtrade/exchange/backtesting.py
Normal file
220
freqtrade/exchange/backtesting.py
Normal file
@ -0,0 +1,220 @@
|
||||
# pylint: disable=C0103
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from os.path import basename, splitext
|
||||
from typing import List, Optional
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import func, text
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TESTDATA_DIR = os.path.join('freqtrade', 'tests', 'testdata')
|
||||
# TODO: Define a global value for analyze and backtesting
|
||||
TICKER_HISTORY_INTERVAL_H: int = 24
|
||||
|
||||
_ROW_INDEX: int = 0
|
||||
_ROW_INTERVAL: int = 0
|
||||
_LEN_ROWS: int = 0
|
||||
_TESTDATA: dict = {}
|
||||
|
||||
|
||||
class Backtesting(Exchange):
|
||||
@property
|
||||
def sleep_time(self) -> float:
|
||||
return 0
|
||||
|
||||
def __init__(self, config: dict, testdata_dir: Optional[str] = TESTDATA_DIR) -> None:
|
||||
global _ROW_INDEX, _ROW_INTERVAL, _LEN_ROWS, _TESTDATA
|
||||
|
||||
# Disable debug logs for a quicker run
|
||||
# logging.disable(logging.DEBUG)
|
||||
|
||||
# Get pairs from test data directory and inject into config replacing existing ones
|
||||
(files, pairs) = _get_testdata_pairs(testdata_dir)
|
||||
config['pair_whitelist'] = pairs
|
||||
|
||||
# Load the test data for each pair
|
||||
(_TESTDATA, _LEN_ROWS) = _get_testdata(files, pairs)
|
||||
|
||||
# Set first row according to shift time
|
||||
# to have some ticker history available for the first analysis
|
||||
_ROW_INDEX = _initial_row_index(_TESTDATA[pairs[0]]['result'], TICKER_HISTORY_INTERVAL_H)
|
||||
_ROW_INTERVAL = _ROW_INDEX
|
||||
if _ROW_INDEX >= _LEN_ROWS or _ROW_INDEX == 0:
|
||||
raise RuntimeError('Test data is not usable with the current row interval')
|
||||
|
||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||
return 'backtesting'
|
||||
|
||||
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||
return 'backtesting'
|
||||
|
||||
def get_balance(self, currency: str) -> float:
|
||||
return 999.9
|
||||
|
||||
def get_ticker(self, pair: str) -> dict:
|
||||
row = _TESTDATA[pair]['result'][_ROW_INDEX]
|
||||
return {'bid': row['C'], 'ask': row['C'], 'last': row['C']}
|
||||
|
||||
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
|
||||
minimum = _ROW_INDEX - _ROW_INTERVAL
|
||||
maximum = _ROW_INDEX
|
||||
return \
|
||||
{'success': True, 'message': '', 'result': _TESTDATA[pair]['result'][minimum:maximum]}
|
||||
|
||||
def cancel_order(self, order_id: str) -> None:
|
||||
return
|
||||
|
||||
def get_open_orders(self, pair: str) -> List[dict]:
|
||||
return []
|
||||
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
return ''
|
||||
|
||||
def get_markets(self) -> List[str]:
|
||||
return []
|
||||
|
||||
|
||||
def _get_testdata_pairs(directory: str) -> (List[str], List[str]):
|
||||
"""
|
||||
Returns a file and a pair list for a given test data directory.
|
||||
:param directory: Test data directory containing JSON files
|
||||
:return: File list with paths relative to the project directory, pair list e.g. ['BTC_ETC']
|
||||
"""
|
||||
files = sorted(glob.glob(os.path.join(directory, '*.json')))
|
||||
pairs = sorted([splitext(basename(p))[0].replace('-', '_').upper() for p in files])
|
||||
return files, pairs
|
||||
|
||||
|
||||
def _get_testdata(files: List[str], pairs) -> (dict, int):
|
||||
"""
|
||||
Returns test data content from a given file list.
|
||||
:param files: JSON file list with paths relative to the project directory
|
||||
:param pairs: Pair list, e.g. ['BTC_ETC']
|
||||
:return: Test data dict with currency pairs as keys, maximal common testdata length for pairs
|
||||
"""
|
||||
testdata_lengths = [] # Find a common maximum for all testdata pairs
|
||||
testdata = {}
|
||||
for (file, pair) in zip(files, pairs):
|
||||
with open(file) as f:
|
||||
testdata[pair] = json.load(f)
|
||||
testdata_lengths.append(len(testdata[pair]['result']))
|
||||
len_rows = min(testdata_lengths)
|
||||
return testdata, len_rows
|
||||
|
||||
|
||||
def _initial_row_index(ticker_history: dict, ticker_history_interval: float) -> int:
|
||||
"""
|
||||
Calculates the initial row index to have a buffer for the first signal analysis.
|
||||
:param ticker_history: Ticker history of a currency pair
|
||||
:param ticker_history_interval: Time interval used during the ticker analysis
|
||||
:return: Calculated initial row index
|
||||
"""
|
||||
result = 0
|
||||
t0 = arrow.get(ticker_history[0]['T'])
|
||||
t1 = t0.shift(hours=ticker_history_interval)
|
||||
for i, row in enumerate(ticker_history):
|
||||
row_time = arrow.get(row['T'])
|
||||
if row_time >= t1:
|
||||
result = i
|
||||
break
|
||||
return result
|
||||
|
||||
|
||||
def time_step() -> bool:
|
||||
"""
|
||||
Advances in time or rather increases the row counter by 1 (one).
|
||||
:return: Success status - False if the end of a data set is reached
|
||||
"""
|
||||
global _ROW_INDEX
|
||||
|
||||
time = _ROW_INDEX
|
||||
_ROW_INDEX += 1
|
||||
logger.debug('Row: %s/%s', time, _LEN_ROWS)
|
||||
if time >= _LEN_ROWS: # Backtesting complete
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def get_minimum_date(pair: str) -> datetime:
|
||||
"""
|
||||
Subtracts the ticker history interval (e.g. 24 hours) from the current time.
|
||||
:param pair: Pair as str, format: BTC_ETH
|
||||
:return: datetime
|
||||
"""
|
||||
ticker_history = _TESTDATA[pair]['result']
|
||||
minimum_row = _ROW_INDEX - _ROW_INTERVAL
|
||||
minimum_date = ticker_history[minimum_row]['T']
|
||||
return minimum_date
|
||||
|
||||
|
||||
def current_time(pair: str) -> datetime:
|
||||
"""
|
||||
Gets the time stored in the current row of a data set for a given currency pair.
|
||||
:param pair: Pair as str, format: BTC_ETC
|
||||
:return: datetime
|
||||
"""
|
||||
return arrow.get(_TESTDATA[pair]['result'][_ROW_INDEX]['T']).datetime.replace(tzinfo=None)
|
||||
|
||||
|
||||
# TODO: Common statistic methods for telegram and backtesting
|
||||
def print_results() -> None:
|
||||
"""
|
||||
Prints cumulative profit statistics.
|
||||
:return: None
|
||||
"""
|
||||
trades = Trade.query.order_by(Trade.id).all()
|
||||
|
||||
profit_amounts = []
|
||||
profits = []
|
||||
durations = []
|
||||
for trade in trades:
|
||||
if trade.close_date:
|
||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||
if trade.close_profit:
|
||||
profit = trade.close_profit
|
||||
else:
|
||||
# Get current rate
|
||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
||||
|
||||
profit_amounts.append((profit / 100) * trade.stake_amount)
|
||||
profits.append(profit)
|
||||
|
||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||
.filter(Trade.is_open.is_(False)) \
|
||||
.group_by(Trade.pair) \
|
||||
.order_by(text('profit_sum DESC')) \
|
||||
.first()
|
||||
|
||||
if not best_pair:
|
||||
logger.info('No closed trade')
|
||||
return
|
||||
|
||||
bp_pair, bp_rate = best_pair
|
||||
msg = """
|
||||
ROI: {profit_btc:.2f} ({profit:.2f}%)
|
||||
Trade Count: {trade_count}
|
||||
First Trade opened: {first_trade_date}
|
||||
Latest Trade opened: {latest_trade_date}
|
||||
Avg. Duration: {avg_duration}
|
||||
Best Performing: {best_pair}: {best_rate:.2f}%
|
||||
""".format(
|
||||
profit_btc=round(sum(profit_amounts), 8),
|
||||
profit=round(sum(profits), 2),
|
||||
trade_count=len(trades),
|
||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
||||
best_pair=bp_pair,
|
||||
best_rate=round(bp_rate, 2),
|
||||
)
|
||||
logger.info(msg)
|
@ -2,6 +2,7 @@
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
@ -11,7 +12,8 @@ from jsonschema import validate
|
||||
|
||||
from freqtrade import __version__, exchange, persistence
|
||||
from freqtrade.analyze import get_buy_signal
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||
from freqtrade.exchange import backtesting
|
||||
from freqtrade.misc import CONF_SCHEMA, State, get_args, get_state, parse_args, update_state
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import telegram
|
||||
|
||||
@ -106,6 +108,9 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
||||
:return True if bot should sell at current rate
|
||||
"""
|
||||
if get_state() == State.BACKTESTING:
|
||||
current_time = backtesting.current_time(trade.pair)
|
||||
|
||||
current_profit = (current_rate - trade.open_rate) / trade.open_rate
|
||||
|
||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||
@ -186,6 +191,10 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
order_id = exchange.buy(pair, open_rate, amount)
|
||||
|
||||
# Create trade entity and return
|
||||
if get_state() == State.BACKTESTING:
|
||||
current_time = backtesting.current_time(pair)
|
||||
else:
|
||||
current_time = datetime.utcnow()
|
||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||
exchange.EXCHANGE.name.upper(),
|
||||
pair.replace('_', '/'),
|
||||
@ -197,41 +206,48 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||
return Trade(pair=pair,
|
||||
stake_amount=stake_amount,
|
||||
open_rate=open_rate,
|
||||
open_date=datetime.utcnow(),
|
||||
open_date=current_time,
|
||||
amount=amount,
|
||||
exchange=exchange.EXCHANGE.name.upper(),
|
||||
open_order_id=order_id,
|
||||
is_open=True)
|
||||
|
||||
|
||||
def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
def init(config: dict, db_url: Optional[str] = None, argv: Optional[list] = None) -> None:
|
||||
"""
|
||||
Initializes all modules and updates the config
|
||||
:param config: config as dict
|
||||
:param db_url: database connector string for sqlalchemy (Optional)
|
||||
:param argv: (optional) command-line arguments
|
||||
:return: None
|
||||
"""
|
||||
# Parse command-line arguments
|
||||
parse_args(argv)
|
||||
|
||||
# Set initial application state
|
||||
if get_args().backtesting:
|
||||
initial_state = State.BACKTESTING
|
||||
_CONF['telegram']['enabled'] = False
|
||||
else:
|
||||
initial_state = config.get('initial_state', State.STOPPED.name.lower())
|
||||
initial_state = State[initial_state.upper()]
|
||||
update_state(initial_state)
|
||||
|
||||
# Initialize all modules
|
||||
telegram.init(config)
|
||||
persistence.init(config, db_url)
|
||||
exchange.init(config)
|
||||
|
||||
# Set initial application state
|
||||
initial_state = config.get('initial_state')
|
||||
if initial_state:
|
||||
update_state(State[initial_state.upper()])
|
||||
else:
|
||||
update_state(State.STOPPED)
|
||||
|
||||
|
||||
def app(config: dict) -> None:
|
||||
def app(config: dict, argv: Optional[list] = None) -> None:
|
||||
"""
|
||||
Main loop which handles the application state
|
||||
:param config: config as dict
|
||||
:param argv: (optional) command-line arguments
|
||||
:return: None
|
||||
"""
|
||||
logger.info('Starting freqtrade %s', __version__)
|
||||
init(config)
|
||||
init(config, argv=argv)
|
||||
try:
|
||||
old_state = get_state()
|
||||
logger.info('Initial State: %s', old_state)
|
||||
@ -249,6 +265,12 @@ def app(config: dict) -> None:
|
||||
_process()
|
||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||
time.sleep(exchange.EXCHANGE.sleep_time)
|
||||
elif new_state == State.BACKTESTING:
|
||||
_process()
|
||||
# Advance in time without sleeping
|
||||
if not backtesting.time_step():
|
||||
backtesting.print_results()
|
||||
break
|
||||
old_state = new_state
|
||||
except RuntimeError:
|
||||
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||
@ -266,7 +288,7 @@ def main():
|
||||
with open('config.json') as file:
|
||||
_CONF = json.load(file)
|
||||
validate(_CONF, CONF_SCHEMA)
|
||||
app(_CONF)
|
||||
app(_CONF, sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -1,4 +1,6 @@
|
||||
import argparse
|
||||
import enum
|
||||
from typing import Optional
|
||||
|
||||
from wrapt import synchronized
|
||||
|
||||
@ -6,8 +8,12 @@ from wrapt import synchronized
|
||||
class State(enum.Enum):
|
||||
RUNNING = 0
|
||||
STOPPED = 1
|
||||
BACKTESTING = 2
|
||||
|
||||
|
||||
# Command-line arguments
|
||||
_ARGS: argparse.Namespace = None
|
||||
|
||||
# Current application state
|
||||
_STATE = State.STOPPED
|
||||
|
||||
@ -32,6 +38,29 @@ def get_state() -> State:
|
||||
return _STATE
|
||||
|
||||
|
||||
def parse_args(argv: Optional[list] = None) -> None:
|
||||
"""
|
||||
Parses and stores command-line arguments.
|
||||
:param argv: (optional) command-line arguments
|
||||
:return: None
|
||||
"""
|
||||
global _ARGS
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Simple High Frequency Trading Bot for crypto currencies')
|
||||
parser.add_argument('-b', '--backtesting', action='store_true',
|
||||
help='test bot performance on offline data and print results')
|
||||
_ARGS = parser.parse_args(argv[1:] if argv else [])
|
||||
|
||||
|
||||
def get_args():
|
||||
"""
|
||||
Gets the current command-line arguments.
|
||||
:return: arguments
|
||||
"""
|
||||
return _ARGS
|
||||
|
||||
|
||||
# Required json-schema for user specified config
|
||||
CONF_SCHEMA = {
|
||||
'type': 'object',
|
||||
|
@ -8,6 +8,7 @@ from sqlalchemy.orm.session import sessionmaker
|
||||
from sqlalchemy.types import Enum
|
||||
|
||||
from freqtrade import exchange
|
||||
from freqtrade.misc import State, get_state
|
||||
|
||||
_CONF = {}
|
||||
|
||||
@ -25,7 +26,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
||||
"""
|
||||
_CONF.update(config)
|
||||
if not db_url:
|
||||
if _CONF.get('dry_run', False):
|
||||
if get_state() == State.BACKTESTING:
|
||||
db_url = 'sqlite://'
|
||||
elif _CONF.get('dry_run', False):
|
||||
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
||||
else:
|
||||
db_url = 'sqlite:///tradesv2.sqlite'
|
||||
|
206
freqtrade/tests/test_backtesting_internal.py
Normal file
206
freqtrade/tests/test_backtesting_internal.py
Normal file
@ -0,0 +1,206 @@
|
||||
# pragma pylint: disable=missing-docstring,protected-access
|
||||
import os
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
from jsonschema import validate
|
||||
|
||||
from freqtrade.exchange import backtesting
|
||||
from freqtrade.exchange.backtesting import Backtesting
|
||||
from freqtrade.main import create_trade, init
|
||||
from freqtrade.misc import CONF_SCHEMA
|
||||
from freqtrade.persistence import Trade
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def conf():
|
||||
configuration = {
|
||||
'max_open_trades': 3,
|
||||
'stake_currency': 'BTC',
|
||||
'stake_amount': 0.05,
|
||||
'dry_run': True,
|
||||
'minimal_roi': {
|
||||
'60': 0.0,
|
||||
'40': 0.01,
|
||||
'20': 0.02,
|
||||
'0': 0.03
|
||||
},
|
||||
'stoploss': -0.40,
|
||||
'bid_strategy': {
|
||||
'ask_last_balance': 0.0
|
||||
},
|
||||
'exchange': {
|
||||
'name': 'bittrex',
|
||||
'key': 'key',
|
||||
'secret': 'secret',
|
||||
'pair_whitelist': [
|
||||
'BTC_RLC'
|
||||
]
|
||||
},
|
||||
'telegram': {
|
||||
'enabled': False,
|
||||
'token': 'token',
|
||||
'chat_id': 'chat_id'
|
||||
}
|
||||
}
|
||||
validate(configuration, CONF_SCHEMA)
|
||||
return configuration
|
||||
|
||||
|
||||
FILES = [
|
||||
os.path.join('freqtrade', 'tests', 'testdata', 'btc-edg.json'),
|
||||
os.path.join('freqtrade', 'tests', 'testdata', 'btc-etc.json')
|
||||
]
|
||||
PAIRS = ['BTC_EDG', 'BTC_ETC']
|
||||
TESTDATA = {
|
||||
PAIRS[0]: {
|
||||
'success': True,
|
||||
'message': '',
|
||||
'result': [
|
||||
{'O': 0.00014469, 'H': 0.00014469, 'L': 0.00014469, 'C': 0.00014469, 'V': 10.66173857, 'T': '2017-09-05T18:55:00', 'BV': 0.00154264},
|
||||
{'O': 0.00014469, 'H': 0.00014477, 'L': 0.00014469, 'C': 0.00014477, 'V': 410.54795113, 'T': '2017-09-05T19:00:00', 'BV': 0.05942728},
|
||||
{'O': 0.00014477, 'H': 0.00014477, 'L': 0.00014477, 'C': 0.00014477, 'V': 69.10850034, 'T': '2017-09-05T19:05:00', 'BV': 0.01000482},
|
||||
{'O': 0.00014470, 'H': 0.00014474, 'L': 0.00014400, 'C': 0.00014473, 'V': 7612.36224582, 'T': '2017-09-05T19:10:00', 'BV': 1.09730748},
|
||||
]
|
||||
},
|
||||
PAIRS[1]: {
|
||||
'success': True,
|
||||
'message': '',
|
||||
'result': [
|
||||
{'O': 0.00391500, 'H': 0.00392700, 'L': 0.00391500, 'C': 0.00392000, 'V': 29.90264260, 'T': '2017-09-05T18:55:00', 'BV': 0.11712504},
|
||||
{'O': 0.00392680, 'H': 0.00392749, 'L': 0.00391500, 'C': 0.00391500, 'V': 329.35043009, 'T': '2017-09-05T19:00:00', 'BV': 1.29065913},
|
||||
{'O': 0.00391500, 'H': 0.00392733, 'L': 0.00391500, 'C': 0.00392300, 'V': 186.96019741, 'T': '2017-09-05T19:05:00', 'BV': 0.73332203},
|
||||
{'O': 0.00391500, 'H': 0.00391500, 'L': 0.00390007, 'C': 0.00390007, 'V': 298.06457786, 'T': '2017-09-05T19:10:00', 'BV': 1.16560055},
|
||||
{'O': 0.00391490, 'H': 0.00391490, 'L': 0.00389126, 'C': 0.00389126, 'V': 1007.91208513, 'T': '2017-09-05T19:15:00', 'BV': 3.92491826}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def len_rows():
|
||||
ticker_history = TESTDATA[PAIRS[0]]
|
||||
return len(ticker_history['result'])
|
||||
|
||||
|
||||
def _json_load(file):
|
||||
pair = os.path.splitext(os.path.basename(file.name))[0].replace('-', '_').upper()
|
||||
return TESTDATA[pair]
|
||||
|
||||
|
||||
def test_init(conf, mocker):
|
||||
mocker.patch('json.load', side_effect=_json_load)
|
||||
mocker.patch('glob.glob', return_value=FILES)
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_args', backtesting=True)
|
||||
mocker.patch('freqtrade.exchange', init=MagicMock())
|
||||
mocker.patch('freqtrade.main.backtesting.TICKER_HISTORY_INTERVAL_H', 1/6)
|
||||
init(conf)
|
||||
assert backtesting._TESTDATA == TESTDATA
|
||||
assert backtesting._LEN_ROWS == 4
|
||||
# 10 minutes (1/6 hours) should correspond to 2 rows
|
||||
assert backtesting._ROW_INDEX == 2
|
||||
assert backtesting._ROW_INTERVAL == 2
|
||||
|
||||
# Available test data should not cover the interval
|
||||
mocker.patch('freqtrade.main.backtesting.TICKER_HISTORY_INTERVAL_H', 24)
|
||||
with pytest.raises(RuntimeError):
|
||||
assert init(conf)
|
||||
|
||||
|
||||
def test_time_step(len_rows, mocker):
|
||||
mocker.patch('freqtrade.exchange.backtesting._LEN_ROWS', len_rows)
|
||||
for index in range(4):
|
||||
assert backtesting._ROW_INDEX == index
|
||||
assert backtesting.time_step() is True
|
||||
assert backtesting.time_step() is False
|
||||
|
||||
|
||||
def test_returns_minimum_date(mocker):
|
||||
mocker.patch('freqtrade.exchange.backtesting._TESTDATA', TESTDATA)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INDEX', 3)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INTERVAL', 2)
|
||||
assert backtesting.get_minimum_date(PAIRS[0]) == '2017-09-05T19:00:00'
|
||||
|
||||
|
||||
def test_returns_ticker(conf, mocker):
|
||||
mocker.patch('freqtrade.main.get_args', backtesting=True)
|
||||
mocker.patch('freqtrade.exchange.backtesting._get_testdata', return_value=(TESTDATA, 4))
|
||||
mocker.patch('freqtrade.exchange.backtesting.TICKER_HISTORY_INTERVAL_H', 1/6)
|
||||
first_pair_close = 0.00014477
|
||||
backtesting = Backtesting(conf)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INDEX', 2)
|
||||
assert backtesting.get_ticker(PAIRS[0]) == \
|
||||
{'bid': first_pair_close, 'ask': first_pair_close, 'last': first_pair_close}
|
||||
|
||||
|
||||
def test_returns_ticker_history(conf, mocker):
|
||||
mocker.patch('freqtrade.exchange.backtesting._get_testdata', return_value=(TESTDATA, 4))
|
||||
mocker.patch('freqtrade.main.backtesting.TICKER_HISTORY_INTERVAL_H', 1/6)
|
||||
backtesting = Backtesting(conf)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INDEX', 3)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INTERVAL', 2)
|
||||
assert backtesting.get_ticker_history(PAIRS[0]) == \
|
||||
{'success': True,
|
||||
'message': '',
|
||||
'result': [
|
||||
{'O': 0.00014469, 'H': 0.00014477, 'L': 0.00014469, 'C': 0.00014477, 'V': 410.54795113, 'T': '2017-09-05T19:00:00', 'BV': 0.05942728},
|
||||
{'O': 0.00014477, 'H': 0.00014477, 'L': 0.00014477, 'C': 0.00014477, 'V': 69.10850034, 'T': '2017-09-05T19:05:00', 'BV': 0.01000482}
|
||||
]}
|
||||
|
||||
|
||||
def test_returns_current_time(mocker):
|
||||
mocker.patch('freqtrade.exchange.backtesting._TESTDATA', TESTDATA)
|
||||
mocker.patch('freqtrade.exchange.backtesting._ROW_INDEX', 1)
|
||||
assert backtesting.current_time(PAIRS[0]) == \
|
||||
arrow.get('2017-09-05T19:00:00').datetime.replace(tzinfo=None)
|
||||
|
||||
|
||||
def test_results(conf, mocker):
|
||||
current_time = datetime.utcnow()
|
||||
logger_mock = MagicMock()
|
||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||
mocker.patch('freqtrade.main.get_args', backtesting=True)
|
||||
mocker.patch('freqtrade.main.backtesting', init=MagicMock())
|
||||
mocker.patch('freqtrade.main.telegram', init=MagicMock())
|
||||
mocker.patch('freqtrade.main.backtesting.current_time', return_value=current_time)
|
||||
mocker.patch('freqtrade.main.get_buy_signal', return_value=True)
|
||||
mocker.patch('freqtrade.exchange.backtesting.logger', info=logger_mock)
|
||||
mocker.patch.multiple('freqtrade.main.exchange',
|
||||
get_ticker=MagicMock(return_value={
|
||||
'bid': 0.07256061,
|
||||
'ask': 0.072661,
|
||||
'last': 0.07256061
|
||||
}),
|
||||
buy=MagicMock(return_value='mocked_order_id'))
|
||||
init(conf)
|
||||
|
||||
# Create some test data
|
||||
trade = create_trade(15.0)
|
||||
assert trade
|
||||
trade.close_rate = 0.07256061
|
||||
trade.close_profit = 100.00
|
||||
trade.close_date = current_time
|
||||
trade.open_order_id = None
|
||||
trade.is_open = False
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
backtesting.print_results()
|
||||
assert logger_mock.call_count == 1
|
||||
assert '(100.00%)' in logger_mock.call_args_list[-1][0][0]
|
||||
|
||||
# Trade should not be closed yet
|
||||
Trade.session.delete(trade)
|
||||
trade.close_rate = None
|
||||
trade.close_profit = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
Trade.session.add(trade)
|
||||
Trade.session.flush()
|
||||
|
||||
backtesting.print_results()
|
||||
assert logger_mock.call_count == 2
|
||||
assert 'No closed trade' in logger_mock.call_args_list[-1][0][0]
|
Loading…
Reference in New Issue
Block a user