Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
baudbox 2017-11-23 20:34:23 +00:00
commit f1730eee3a
18 changed files with 236 additions and 152 deletions

View File

@ -6,10 +6,10 @@
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
"20": 0.02 "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.40, "stoploss": -0.10,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -1,4 +1,16 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.14.3' __version__ = '0.14.3'
from . import main
class DependencyException(BaseException):
"""
Indicates that a assumed dependency is not met.
This could happen when there is currently not enough money on the account.
"""
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
"""

View File

@ -1,9 +1,9 @@
""" """
Functions to analyze ticker data with indicators and produce buy and sell signals Functions to analyze ticker data with indicators and produce buy and sell signals
""" """
from enum import Enum
import logging import logging
from datetime import timedelta from datetime import timedelta
from enum import Enum
import arrow import arrow
import talib.abstract as ta import talib.abstract as ta
@ -61,6 +61,10 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
hilbert = ta.HT_SINE(dataframe) hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine'] dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine'] dataframe['htleadsine'] = hilbert['leadsine']
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
return dataframe return dataframe
@ -71,14 +75,21 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
(dataframe['tema'] <= dataframe['blower']) & (
(dataframe['rsi'] < 37) & (dataframe['rsi'] < 35) &
(dataframe['fastd'] < 48) & (dataframe['fastd'] < 35) &
(dataframe['adx'] > 31), (dataframe['adx'] > 30) &
(dataframe['plus_di'] > 0.5)
) |
(
(dataframe['adx'] > 65) &
(dataframe['plus_di'] > 0.5)
),
'buy'] = 1 'buy'] = 1
return dataframe return dataframe
def populate_sell_trend(dataframe: DataFrame) -> DataFrame: def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
""" """
Based on TA indicators, populates the sell signal for the given dataframe Based on TA indicators, populates the sell signal for the given dataframe
@ -86,9 +97,19 @@ def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
:return: DataFrame with buy column :return: DataFrame with buy column
""" """
dataframe.loc[ dataframe.loc[
(crossed_above(dataframe['rsi'], 70)), (
(
(crossed_above(dataframe['rsi'], 70)) |
(crossed_above(dataframe['fastd'], 70))
) &
(dataframe['adx'] > 10) &
(dataframe['minus_di'] > 0)
) |
(
(dataframe['adx'] > 70) &
(dataframe['minus_di'] > 0.5)
),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
@ -107,9 +128,6 @@ def analyze_ticker(pair: str) -> DataFrame:
dataframe = populate_indicators(dataframe) dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe) dataframe = populate_buy_trend(dataframe)
dataframe = populate_sell_trend(dataframe) dataframe = populate_sell_trend(dataframe)
# TODO: buy_price and sell_price are only used by the plotter, should probably be moved there
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
return dataframe return dataframe
@ -119,7 +137,12 @@ def get_signal(pair: str, signal: SignalType) -> bool:
:param pair: pair in format BTC_ANT or BTC-ANT :param pair: pair in format BTC_ANT or BTC-ANT
:return: True if pair is good for buying, False otherwise :return: True if pair is good for buying, False otherwise
""" """
dataframe = analyze_ticker(pair) try:
dataframe = analyze_ticker(pair)
except ValueError as ex:
logger.warning('Unable to analyze ticker for pair %s: %s', pair, str(ex))
return False
if dataframe.empty: if dataframe.empty:
return False return False

View File

@ -9,6 +9,7 @@ import arrow
import requests import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
@ -51,7 +52,7 @@ def init(config: dict) -> None:
try: try:
exchange_class = Exchanges[name.upper()].value exchange_class = Exchanges[name.upper()].value
except KeyError: except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name)) raise OperationalException('Exchange {} is not supported'.format(name))
_API = exchange_class(exchange_config) _API = exchange_class(exchange_config)
@ -62,7 +63,7 @@ def init(config: dict) -> None:
def validate_pairs(pairs: List[str]) -> None: def validate_pairs(pairs: List[str]) -> None:
""" """
Checks if all given pairs are tradable on the current exchange. Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available. Raises OperationalException if one pair is not available.
:param pairs: list of pairs :param pairs: list of pairs
:return: None :return: None
""" """
@ -75,11 +76,12 @@ def validate_pairs(pairs: List[str]) -> None:
stake_cur = _CONF['stake_currency'] stake_cur = _CONF['stake_currency']
for pair in pairs: for pair in pairs:
if not pair.startswith(stake_cur): if not pair.startswith(stake_cur):
raise RuntimeError( raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
) )
if pair not in markets: if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower())) raise OperationalException(
'Pair {} is not available at {}'.format(pair, _API.name.lower()))
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:

View File

@ -4,6 +4,7 @@ from typing import List, Dict
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1 from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
from requests.exceptions import ContentDecodingError from requests.exceptions import ContentDecodingError
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -46,7 +47,7 @@ class Bittrex(Exchange):
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate) data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'], message=data['message'],
pair=pair, pair=pair,
rate=rate, rate=rate,
@ -56,7 +57,7 @@ class Bittrex(Exchange):
def sell(self, pair: str, rate: float, amount: float) -> str: def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate) data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format( raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'], message=data['message'],
pair=pair, pair=pair,
rate=rate, rate=rate,
@ -66,7 +67,7 @@ class Bittrex(Exchange):
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency) data = _API.get_balance(currency)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({currency})'.format( raise OperationalException('{message} params=({currency})'.format(
message=data['message'], message=data['message'],
currency=currency)) currency=currency))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
@ -74,13 +75,13 @@ class Bittrex(Exchange):
def get_balances(self): def get_balances(self):
data = _API.get_balances() data = _API.get_balances()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return data['result'] return data['result']
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> dict:
data = _API.get_ticker(pair.replace('_', '-')) data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise OperationalException('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
@ -121,7 +122,7 @@ class Bittrex(Exchange):
pair=pair)) pair=pair))
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise OperationalException('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
@ -130,7 +131,7 @@ class Bittrex(Exchange):
def get_order(self, order_id: str) -> Dict: def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id) data = _API.get_order(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format( raise OperationalException('{message} params=({order_id})'.format(
message=data['message'], message=data['message'],
order_id=order_id)) order_id=order_id))
data = data['result'] data = data['result']
@ -148,7 +149,7 @@ class Bittrex(Exchange):
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id) data = _API.cancel(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format( raise OperationalException('{message} params=({order_id})'.format(
message=data['message'], message=data['message'],
order_id=order_id)) order_id=order_id))
@ -158,19 +159,19 @@ class Bittrex(Exchange):
def get_markets(self) -> List[str]: def get_markets(self) -> List[str]:
data = _API.get_markets() data = _API.get_markets()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']] return [m['MarketName'].replace('-', '_') for m in data['result']]
def get_market_summaries(self) -> List[Dict]: def get_market_summaries(self) -> List[Dict]:
data = _API.get_market_summaries() data = _API.get_market_summaries()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return data['result'] return data['result']
def get_wallet_health(self) -> List[Dict]: def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health() data = _API_V2.get_wallet_health()
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise OperationalException('{message}'.format(message=data['message']))
return [{ return [{
'Currency': entry['Health']['Currency'], 'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'], 'IsActive': entry['Health']['IsActive'],

View File

@ -6,16 +6,16 @@ import sys
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from signal import signal, SIGINT, SIGABRT, SIGTERM
from typing import Dict, Optional, List from typing import Dict, Optional, List
import requests import requests
from cachetools import cached, TTLCache from cachetools import cached, TTLCache
from freqtrade import __version__, exchange, persistence, rpc from freqtrade import __version__, exchange, persistence, rpc, DependencyException, \
OperationalException
from freqtrade.analyze import get_signal, SignalType from freqtrade.analyze import get_signal, SignalType
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \ from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
load_config, FreqtradeException load_config
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -67,16 +67,13 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
try: try:
# Create entity and execute trade # Create entity and execute trade
trade = create_trade(float(_CONF['stake_amount'])) state_changed = create_trade(float(_CONF['stake_amount']))
if trade: if not state_changed:
Trade.session.add(trade)
state_changed = True
else:
logger.info( logger.info(
'Checked all whitelisted currencies. ' 'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...' 'Found no suitable entry positions for buying. Will keep looking ...'
) )
except FreqtradeException as e: except DependencyException as e:
logger.warning('Unable to create trade: %s', e) logger.warning('Unable to create trade: %s', e)
for trade in trades: for trade in trades:
@ -97,12 +94,12 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
error error
) )
time.sleep(30) time.sleep(30)
except RuntimeError: except OperationalException:
rpc.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format( rpc.send_msg('*Status:* Got OperationalException:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(), traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.' hint='Issue `/start` if you think it is safe to restart.'
)) ))
logger.exception('Got RuntimeError. Stopping trader ...') logger.exception('Got OperationalException. Stopping trader ...')
update_state(State.STOPPED) update_state(State.STOPPED)
return state_changed return state_changed
@ -126,6 +123,7 @@ def execute_sell(trade: Trade, limit: float) -> None:
limit, limit,
fmt_exp_profit fmt_exp_profit
)) ))
Trade.session.flush()
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool: def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
@ -172,11 +170,12 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float) -> Optional[Trade]: def create_trade(stake_amount: float) -> bool:
""" """
Checks the implemented trading indicator(s) for a randomly picked pair, Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend :param stake_amount: amount of btc to spend
:return: True if a trade object has been created and persisted, False otherwise
""" """
logger.info( logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...', 'Checking buy signals to create a new trade with stake_amount: %f ...',
@ -185,7 +184,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled # Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise FreqtradeException( raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
) )
@ -195,7 +194,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
whitelist.remove(trade.pair) whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
raise FreqtradeException('No pair in whitelist') raise DependencyException('No pair in whitelist')
# Pick pair based on StochRSI buy signals # Pick pair based on StochRSI buy signals
for _pair in whitelist: for _pair in whitelist:
@ -203,12 +202,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
pair = _pair pair = _pair
break break
else: else:
return None return False
# Calculate amount and subtract fee # Calculate amount
fee = exchange.get_fee()
buy_limit = get_target_bid(exchange.get_ticker(pair)) buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = (1 - fee) * stake_amount / buy_limit amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount) order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return # Create trade entity and return
@ -219,14 +217,19 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
buy_limit buy_limit
)) ))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair, trade = Trade(
stake_amount=stake_amount, pair=pair,
amount=amount, stake_amount=stake_amount,
fee=fee * 2, amount=amount,
open_rate=buy_limit, fee=exchange.get_fee() * 2,
open_date=datetime.utcnow(), open_rate=buy_limit,
exchange=exchange.get_name().upper(), open_date=datetime.utcnow(),
open_order_id=order_id) exchange=exchange.get_name().upper(),
open_order_id=order_id
)
Trade.session.add(trade)
Trade.session.flush()
return True
def init(config: dict, db_url: Optional[str] = None) -> None: def init(config: dict, db_url: Optional[str] = None) -> None:
@ -248,10 +251,6 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
else: else:
update_state(State.STOPPED) update_state(State.STOPPED)
# Register signal handlers
for sig in (SIGINT, SIGTERM, SIGABRT):
signal(sig, cleanup)
@cached(TTLCache(maxsize=1, ttl=1800)) @cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]: def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
@ -270,7 +269,7 @@ def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolum
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]] return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
def cleanup(*args, **kwargs) -> None: def cleanup() -> None:
""" """
Cleanup the application state und finish all pending tasks Cleanup the application state und finish all pending tasks
:return: None :return: None
@ -283,7 +282,7 @@ def cleanup(*args, **kwargs) -> None:
exit(0) exit(0)
def main(): def main() -> None:
""" """
Loads and validates the config and handles the main loop Loads and validates the config and handles the main loop
:return: None :return: None
@ -311,24 +310,32 @@ def main():
# Initialize all modules and start main loop # Initialize all modules and start main loop
if args.dynamic_whitelist: if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)') logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
init(_CONF)
old_state = None
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED: try:
time.sleep(1) init(_CONF)
elif new_state == State.RUNNING: old_state = None
throttle( while True:
_process, new_state = get_state()
min_secs=_CONF['internals'].get('process_throttle_secs', 10), # Log state transition
dynamic_whitelist=args.dynamic_whitelist, if new_state != old_state:
) rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
old_state = new_state logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
)
old_state = new_state
except KeyboardInterrupt:
logger.info('Got SIGINT, aborting ...')
except BaseException:
logger.exception('Got fatal exception!')
finally:
cleanup()
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -15,10 +15,6 @@ from freqtrade import __version__
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FreqtradeException(BaseException):
pass
class State(enum.Enum): class State(enum.Enum):
RUNNING = 0 RUNNING = 0
STOPPED = 1 STOPPED = 1
@ -150,6 +146,12 @@ def build_subcommands(parser: argparse.ArgumentParser) -> None:
type=int, type=int,
metavar='INT', metavar='INT',
) )
backtest.add_argument(
'--limit-max-trades',
help='uses max_open_trades from config to simulate real world limitations',
action='store_true',
dest='limit_max_trades',
)
def start_backtesting(args) -> None: def start_backtesting(args) -> None:
@ -165,6 +167,7 @@ def start_backtesting(args) -> None:
'BACKTEST_LIVE': 'true' if args.live else '', 'BACKTEST_LIVE': 'true' if args.live else '',
'BACKTEST_CONFIG': args.config, 'BACKTEST_CONFIG': args.config,
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval), 'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
'BACKTEST_LIMIT_MAX_TRADES': 'true' if args.limit_max_trades else '',
}) })
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py') path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
pytest.main(['-s', path]) pytest.main(['-s', path])

View File

@ -2,11 +2,11 @@ import logging
import re import re
from datetime import timedelta from datetime import timedelta
from typing import Callable, Any from typing import Callable, Any
from pandas import DataFrame
from tabulate import tabulate
import arrow import arrow
from pandas import DataFrame
from sqlalchemy import and_, func, text from sqlalchemy import and_, func, text
from tabulate import tabulate
from telegram import ParseMode, Bot, Update from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater

View File

@ -23,7 +23,7 @@ def default_conf():
"20": 0.02, "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.05, "stoploss": -0.10,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },
@ -54,6 +54,7 @@ def default_conf():
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
def backtest_conf(): def backtest_conf():
return { return {
"max_open_trades": 3,
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.01, "stake_amount": 0.01,
"minimal_roi": { "minimal_roi": {
@ -62,7 +63,7 @@ def backtest_conf():
"20": 0.02, "20": 0.02,
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.05 "stoploss": -0.10
} }

View File

@ -1,5 +1,6 @@
# pragma pylint: disable=missing-docstring,W0621 # pragma pylint: disable=missing-docstring,W0621
import json import json
import arrow import arrow
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame

View File

@ -83,24 +83,46 @@ def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currenc
return tabulate(tabular_data, headers=headers) return tabulate(tabular_data, headers=headers)
def backtest(backtest_conf, processed, mocker): def backtest(config: Dict, processed, mocker, max_open_trades=0):
"""
Implements backtesting functionality
:param config: config to use
:param processed: a processed dictionary with format {pair, data}
:param mocker: mocker instance
:param max_open_trades: maximum number of concurrent trades (default: 0, disabled)
:return: DataFrame
"""
trades = [] trades = []
trade_count_lock = {}
exchange._API = Bittrex({'key': '', 'secret': ''}) exchange._API = Bittrex({'key': '', 'secret': ''})
mocker.patch.dict('freqtrade.main._CONF', backtest_conf) mocker.patch.dict('freqtrade.main._CONF', config)
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'] = 0 pair_data['buy'], pair_data['sell'] = 0, 0
pair_data['sell'] = 0
ticker = populate_sell_trend(populate_buy_trend(pair_data)) ticker = populate_sell_trend(populate_buy_trend(pair_data))
# for each buy point # for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True): for row in ticker[ticker.buy == 1].itertuples(index=True):
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( trade = Trade(
open_rate=row.close, open_rate=row.close,
open_date=row.date, open_date=row.date,
amount=backtest_conf['stake_amount'], amount=config['stake_amount'],
fee=exchange.get_fee() * 2 fee=exchange.get_fee() * 2
) )
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True): 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: if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
current_profit = trade.calc_profit(row2.close) current_profit = trade.calc_profit(row2.close)
@ -110,6 +132,13 @@ def backtest(backtest_conf, processed, mocker):
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
def get_max_open_trades(config):
if not os.environ.get('BACKTEST_LIMIT_MAX_TRADES'):
return 0
print('Using max_open_trades: {} ...'.format(config['max_open_trades']))
return config['max_open_trades']
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set") @pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
def test_backtest(backtest_conf, mocker): def test_backtest(backtest_conf, mocker):
print('') print('')
@ -147,8 +176,6 @@ def test_backtest(backtest_conf, mocker):
)) ))
# Execute backtest and print results # Execute backtest and print results
results = backtest(config, preprocess(data), mocker) results = backtest(config, preprocess(data), mocker, get_max_open_trades(config))
print('====================== BACKTESTING REPORT ======================================\n\n' print('====================== BACKTESTING REPORT ======================================\n\n')
'NOTE: This Report doesn\'t respect the limits of max_open_trades, \n'
' so the projected values should be taken with a grain of salt.\n')
print(generate_text_table(data, results, config['stake_currency'])) print(generate_text_table(data, results, config['stake_currency']))

View File

@ -3,6 +3,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade import OperationalException
from freqtrade.exchange import validate_pairs from freqtrade.exchange import validate_pairs
@ -21,7 +22,7 @@ def test_validate_pairs_not_available(default_conf, mocker):
api_mock.get_markets = MagicMock(return_value=[]) api_mock.get_markets = MagicMock(return_value=[])
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(RuntimeError, match=r'not available'): with pytest.raises(OperationalException, match=r'not available'):
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])
@ -31,5 +32,5 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
default_conf['stake_currency'] = 'ETH' default_conf['stake_currency'] = 'ETH'
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
with pytest.raises(RuntimeError, match=r'not compatible'): with pytest.raises(OperationalException, match=r'not compatible'):
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -6,11 +6,12 @@ import pytest
import requests import requests
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade.exchange import Exchanges from freqtrade import DependencyException, OperationalException
from freqtrade.analyze import SignalType from freqtrade.analyze import SignalType
from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, init, \ from freqtrade.main import create_trade, handle_trade, init, \
get_target_bid, _process get_target_bid, _process
from freqtrade.misc import get_state, State, FreqtradeException from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -40,7 +41,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.072661 assert trade.open_rate == 0.072661
assert trade.amount == 0.6864067381401302 assert trade.amount == 0.6881270557795791
def test_process_exchange_failures(default_conf, ticker, health, mocker): def test_process_exchange_failures(default_conf, ticker, health, mocker):
@ -59,7 +60,7 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
assert sleep_mock.has_calls() assert sleep_mock.has_calls()
def test_process_runtime_error(default_conf, ticker, health, mocker): def test_process_operational_exception(default_conf, ticker, health, mocker):
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=msg_mock)
@ -68,14 +69,14 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health, get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError)) buy=MagicMock(side_effect=OperationalException))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
result = _process() result = _process()
assert result is False assert result is False
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0] assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker): def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
@ -114,9 +115,9 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) create_trade(15.0)
Trade.session.add(trade)
Trade.session.flush() trade = Trade.query.first()
assert trade is not None assert trade is not None
assert trade.stake_amount == 15.0 assert trade.stake_amount == 15.0
assert trade.is_open assert trade.is_open
@ -132,6 +133,21 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
assert whitelist == default_conf['exchange']['pair_whitelist'] assert whitelist == default_conf['exchange']['pair_whitelist']
def test_create_trade_minimal_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
buy_mock = mocker.patch('freqtrade.main.exchange.buy', MagicMock(return_value='mocked_limit_buy'))
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
min_stake_amount = 0.0005
create_trade(min_stake_amount)
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
assert rate * amount >= min_stake_amount
def test_create_trade_no_stake_amount(default_conf, ticker, mocker): def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
@ -141,7 +157,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount']) create_trade(default_conf['stake_amount'])
@ -154,7 +170,7 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(FreqtradeException, match=r'.*No pair in whitelist.*'): with pytest.raises(DependencyException, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf) conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = [] conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', conf)
@ -175,13 +191,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell')) sell=MagicMock(return_value='mocked_limit_sell'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) create_trade(15.0)
trade.update(limit_buy_order)
Trade.session.add(trade) trade = Trade.query.first()
Trade.session.flush()
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
trade.update(limit_buy_order)
assert trade.is_open is True
handle_trade(trade) handle_trade(trade)
assert trade.open_order_id == 'mocked_limit_sell' assert trade.open_order_id == 'mocked_limit_sell'
@ -204,15 +221,14 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it # Create trade and sell it
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
trade = create_trade(15.0) create_trade(15.0)
Trade.session.add(trade)
trade.update(limit_buy_order) trade = Trade.query.first()
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
trade.update(limit_buy_order)
trade.update(limit_sell_order) trade.update(limit_sell_order)
trade = Trade.query.filter(Trade.is_open.is_(False)).first() assert trade.is_open is False
assert trade
with pytest.raises(ValueError, match=r'.*closed trade.*'): with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade) handle_trade(trade)

View File

@ -109,6 +109,7 @@ def test_start_backtesting(mocker):
live=True, live=True,
loglevel=20, loglevel=20,
ticker_interval=1, ticker_interval=1,
limit_max_trades=True,
) )
start_backtesting(args) start_backtesting(args)
assert env_mock == { assert env_mock == {
@ -116,6 +117,7 @@ def test_start_backtesting(mocker):
'BACKTEST_LIVE': 'true', 'BACKTEST_LIVE': 'true',
'BACKTEST_CONFIG': 'config.json', 'BACKTEST_CONFIG': 'config.json',
'BACKTEST_TICKER_INTERVAL': '1', 'BACKTEST_TICKER_INTERVAL': '1',
'BACKTEST_LIMIT_MAX_TRADES': 'true',
} }
assert pytest_mock.call_count == 1 assert pytest_mock.call_count == 1

View File

@ -1,7 +1,6 @@
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
from unittest.mock import MagicMock
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock
from freqtrade.rpc import init, cleanup, send_msg from freqtrade.rpc import init, cleanup, send_msg

View File

@ -101,11 +101,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
trade = create_trade(15.0) create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update) _status(bot=MagicMock(), update=update)
@ -141,10 +137,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
trade = create_trade(15.0) create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
_status_table(bot=MagicMock(), update=update) _status_table(bot=MagicMock(), update=update)
@ -177,8 +170,8 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
trade = create_trade(15.0) create_trade(15.0)
assert trade trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
@ -193,8 +186,6 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -216,11 +207,10 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
trade = create_trade(15.0) create_trade(15.0)
assert trade
Trade.session.add(trade) trade = Trade.query.first()
Trade.session.flush() assert trade
update.message.text = '/forcesell 1' update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update) _forcesell(bot=MagicMock(), update=update)
@ -245,8 +235,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
# Create some test data # Create some test data
for _ in range(4): for _ in range(4):
Trade.session.add(create_trade(15.0)) create_trade(15.0)
Trade.session.flush()
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
@ -309,7 +298,8 @@ def test_performance_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
trade = create_trade(15.0) create_trade(15.0)
trade = Trade.query.first()
assert trade assert trade
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@ -320,8 +310,6 @@ def test_performance_handle(
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_performance(bot=MagicMock(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
@ -351,9 +339,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
update_state(State.RUNNING) update_state(State.RUNNING)
# Create some test data # Create some test data
Trade.session.add(create_trade(15.0)) create_trade(15.0)
Trade.session.flush()
msg_mock.reset_mock() msg_mock.reset_mock()
_count(bot=MagicMock(), update=update) _count(bot=MagicMock(), update=update)

View File

@ -19,12 +19,12 @@
# limitations under the License. # limitations under the License.
# #
import sys
import warnings
from datetime import datetime, timedelta
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import warnings
import sys
from datetime import datetime, timedelta
from pandas.core.base import PandasObject from pandas.core.base import PandasObject
# ============================================= # =============================================

View File

@ -18,6 +18,9 @@ def plot_analyzed_dataframe(pair: str) -> None:
exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) exchange._API = exchange.Bittrex({'key': '', 'secret': ''})
dataframe = analyze.analyze_ticker(pair) dataframe = analyze.analyze_ticker(pair)
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.loc[dataframe['sell'] == 1, 'sell_price'] = dataframe['close']
# Two subplots sharing x axis # Two subplots sharing x axis
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold') fig.suptitle(pair, fontsize=14, fontweight='bold')