Merge branch 'release/0.14.2'

This commit is contained in:
gcarq 2017-11-16 00:40:44 +01:00
commit b115963a70
17 changed files with 372 additions and 95 deletions

View File

@ -34,5 +34,8 @@
"token": "token", "token": "token",
"chat_id": "chat_id" "chat_id": "chat_id"
}, },
"initial_state": "running" "initial_state": "running",
"internals": {
"process_throttle_secs": 5
}
} }

View File

@ -1,3 +1,3 @@
__version__ = '0.14.1' __version__ = '0.14.2'
from . import main from . import main

View File

@ -8,8 +8,6 @@ from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history from freqtrade.exchange import get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,9 +39,7 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
dataframe['cci'] = ta.CCI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
dataframe['mom'] = ta.MOM(dataframe)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
@ -53,6 +49,9 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist'] dataframe['macdhist'] = macd['macdhist']
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
return dataframe return dataframe
@ -80,13 +79,12 @@ def analyze_ticker(pair: str) -> DataFrame:
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
data = get_ticker_history(pair) ticker_hist = get_ticker_history(pair)
dataframe = parse_ticker_dataframe(data) if not ticker_hist:
logger.warning('Empty ticker history for pair %s', pair)
if dataframe.empty: return DataFrame()
logger.warning('Empty dataframe for pair %s', pair)
return dataframe
dataframe = parse_ticker_dataframe(ticker_hist)
dataframe = populate_indicators(dataframe) dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe) dataframe = populate_buy_trend(dataframe)
return dataframe return dataframe
@ -99,7 +97,6 @@ def get_buy_signal(pair: str) -> bool:
:return: True if pair is good for buying, False otherwise :return: True if pair is good for buying, False otherwise
""" """
dataframe = analyze_ticker(pair) dataframe = analyze_ticker(pair)
if dataframe.empty: if dataframe.empty:
return False return False

View File

@ -4,6 +4,7 @@ from random import randint
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import arrow import arrow
from cachetools import cached, TTLCache
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
@ -127,7 +128,8 @@ def get_ticker(pair: str) -> dict:
return _API.get_ticker(pair) return _API.get_ticker(pair)
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List: @cached(TTLCache(maxsize=100, ttl=30))
def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval) return _API.get_ticker_history(pair, tick_interval)
@ -157,13 +159,17 @@ def get_markets() -> List[str]:
return _API.get_markets() return _API.get_markets()
def get_market_summaries() -> List[Dict]:
return _API.get_market_summaries()
def get_name() -> str: def get_name() -> str:
return _API.name return _API.name
def get_sleep_time() -> float:
return _API.sleep_time
def get_fee() -> float: def get_fee() -> float:
return _API.fee return _API.fee
def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health()

View File

@ -1,14 +1,14 @@
import logging import logging
from typing import List, Dict from typing import List, Dict
import requests from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
from bittrex.bittrex import Bittrex as _Bittrex
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_API: _Bittrex = None _API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {} _EXCHANGE_CONF: dict = {}
@ -18,22 +18,23 @@ class Bittrex(Exchange):
""" """
# Base URL and API endpoints # Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com' BASE_URL: str = 'https://www.bittrex.com'
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
@property
def sleep_time(self) -> float:
""" Sleep time to avoid rate limits, used in the main loop """
return 25
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
global _API, _EXCHANGE_CONF global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config) _EXCHANGE_CONF.update(config)
_API = _Bittrex( _API = _Bittrex(
api_key=_EXCHANGE_CONF['key'], api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'], api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=5, calls_per_second=1,
api_version=API_V1_1,
)
_API_V2 = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=1,
api_version=API_V2_0,
) )
@property @property
@ -81,13 +82,17 @@ class Bittrex(Exchange):
raise RuntimeError('{message} params=({pair})'.format( raise RuntimeError('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
pair=pair)) pair=pair))
if not data['result']['Bid'] or not data['result']['Ask'] or not data['result']['Last']:
raise RuntimeError('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return { return {
'bid': float(data['result']['Bid']), 'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']), 'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']), 'last': float(data['result']['Last']),
} }
def get_ticker_history(self, pair: str, tick_interval: int): def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1: if tick_interval == 1:
interval = 'oneMin' interval = 'oneMin'
elif tick_interval == 5: elif tick_interval == 5:
@ -95,10 +100,18 @@ class Bittrex(Exchange):
else: else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval)) raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = requests.get(self.TICKER_METHOD, params={ data = _API_V2.get_candles(pair.replace('_', '-'), interval)
'marketName': pair.replace('_', '-'),
'tickInterval': interval, # These sanity check are necessary because bittrex cannot keep their API stable.
}).json() if not data.get('result'):
return []
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
for tick in data['result']:
if prop not in tick.keys():
logger.warning('Required property %s not present in response', prop)
return []
if not data['success']: if not data['success']:
raise RuntimeError('{message} params=({pair})'.format( raise RuntimeError('{message} params=({pair})'.format(
message=data['message'], message=data['message'],
@ -139,3 +152,20 @@ class Bittrex(Exchange):
if not data['success']: if not data['success']:
raise RuntimeError('{message}'.format(message=data['message'])) raise RuntimeError('{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]:
data = _API.get_market_summaries()
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
return data['result']
def get_wallet_health(self) -> List[Dict]:
data = _API_V2.get_wallet_health()
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
return [{
'Currency': entry['Health']['Currency'],
'IsActive': entry['Health']['IsActive'],
'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']]

View File

@ -18,14 +18,6 @@ class Exchange(ABC):
:return: percentage in float :return: percentage in float
""" """
@property
@abstractmethod
def sleep_time(self) -> float:
"""
Sleep time in seconds for the main loop to avoid API rate limits.
:return: float
"""
@abstractmethod @abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
""" """
@ -82,7 +74,7 @@ class Exchange(ABC):
""" """
@abstractmethod @abstractmethod
def get_ticker_history(self, pair: str, tick_interval: int) -> List: def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
""" """
Gets ticker history for given pair. Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC :param pair: Pair as str, format: BTC_ETC
@ -139,3 +131,41 @@ class Exchange(ABC):
Returns all available markets. Returns all available markets.
:return: List of all available pairs :return: List of all available pairs
""" """
@abstractmethod
def get_market_summaries(self) -> List[Dict]:
"""
Returns a 24h market summary for all available markets
:return: list, format: [
{
'MarketName': str,
'High': float,
'Low': float,
'Volume': float,
'Last': float,
'TimeStamp': datetime,
'BaseVolume': float,
'Bid': float,
'Ask': float,
'OpenBuyOrders': int,
'OpenSellOrders': int,
'PrevDay': float,
'Created': datetime
},
...
]
"""
@abstractmethod
def get_wallet_health(self) -> List[Dict]:
"""
Returns a list of all wallet health information
:return: list, format: [
{
'Currency': str,
'IsActive': bool,
'LastChecked': str,
'Notice': str
},
...
"""

View File

@ -5,33 +5,63 @@ import logging
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM from signal import signal, SIGINT, SIGABRT, SIGTERM
from typing import Dict, Optional, List
import requests import requests
from cachetools import cached, TTLCache
from jsonschema import validate from jsonschema import validate
from freqtrade import __version__, exchange, persistence from freqtrade import __version__, exchange, persistence
from freqtrade.analyze import get_buy_signal from freqtrade.analyze import get_buy_signal
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import telegram from freqtrade.rpc import telegram
logging.basicConfig(level=logging.DEBUG, logger = logging.getLogger('freqtrade')
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
def _process() -> bool: def refresh_whitelist(whitelist: Optional[List[str]] = None) -> None:
"""
Check wallet health and remove pair from whitelist if necessary
:param whitelist: a new whitelist (optional)
:return: None
"""
whitelist = whitelist or _CONF['exchange']['pair_whitelist']
sanitized_whitelist = []
health = exchange.get_wallet_health()
for status in health:
pair = '{}_{}'.format(_CONF['stake_currency'], status['Currency'])
if pair not in whitelist:
continue
if status['IsActive']:
sanitized_whitelist.append(pair)
else:
logger.info(
'Ignoring %s from whitelist (reason: %s).',
pair, status.get('Notice') or 'wallet is not active'
)
if _CONF['exchange']['pair_whitelist'] != sanitized_whitelist:
logger.debug('Using refreshed pair whitelist: %s ...', sanitized_whitelist)
_CONF['exchange']['pair_whitelist'] = sanitized_whitelist
def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
:param: dynamic_whitelist: True is a dynamic whitelist should be generated (optional)
:return: True if a trade has been created or closed, False otherwise :return: True if a trade has been created or closed, False otherwise
""" """
state_changed = False state_changed = False
try: try:
# Refresh whitelist based on wallet maintenance
refresh_whitelist(
gen_pair_whitelist(_CONF['stake_currency']) if dynamic_whitelist else None
)
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']: if len(trades) < _CONF['max_open_trades']:
@ -42,7 +72,10 @@ def _process() -> bool:
Trade.session.add(trade) Trade.session.add(trade)
state_changed = True state_changed = True
else: else:
logging.info('Got no buy signal...') logger.info(
'Checked all whitelisted currencies. '
'Found no suitable entry positions for buying. Will keep looking ...'
)
except ValueError: except ValueError:
logger.exception('Unable to create trade') logger.exception('Unable to create trade')
@ -85,7 +118,10 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
and trade.close_rate is not None \ and trade.close_rate is not None \
and trade.open_order_id is None: and trade.open_order_id is None:
trade.is_open = False trade.is_open = False
logger.info('No open orders found and trade is fulfilled. Marking %s as closed ...', trade) logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
trade
)
return True return True
return False return False
@ -163,7 +199,10 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
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
""" """
logger.info('Creating new trade with stake_amount: %f ...', stake_amount) logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
stake_amount
)
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:
@ -237,6 +276,23 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
signal(sig, cleanup) signal(sig, cleanup)
@cached(TTLCache(maxsize=1, ttl=1800))
def gen_pair_whitelist(base_currency: str, topn: int = 20, key: str = 'BaseVolume') -> List[str]:
"""
Updates the whitelist with with a dynamically generated list
:param base_currency: base currency as str
:param topn: maximum number of returned results
:param key: sort key (defaults to 'BaseVolume')
:return: List of pairs
"""
summaries = sorted(
(s for s in exchange.get_market_summaries() if s['MarketName'].startswith(base_currency)),
key=lambda s: s.get(key) or 0.0,
reverse=True
)
return [s['MarketName'].replace('-', '_') for s in summaries[:topn]]
def cleanup(*args, **kwargs) -> None: def cleanup(*args, **kwargs) -> None:
""" """
Cleanup the application state und finish all pending tasks Cleanup the application state und finish all pending tasks
@ -255,32 +311,49 @@ def main():
Loads and validates the config and handles the main loop Loads and validates the config and handles the main loop
:return: None :return: None
""" """
logger.info('Starting freqtrade %s', __version__)
global _CONF global _CONF
with open('config.json') as file: args = build_arg_parser().parse_args()
_CONF = json.load(file)
# Initialize logger
logging.basicConfig(
level=args.loglevel,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
)
logger.info(
'Starting freqtrade %s (loglevel=%s)',
__version__,
logging.getLevelName(args.loglevel)
)
# Load and validate configuration
with open(args.config) as file:
_CONF = json.load(file)
if 'internals' not in _CONF:
_CONF['internals'] = {}
logger.info('Validating configuration ...') logger.info('Validating configuration ...')
validate(_CONF, CONF_SCHEMA) validate(_CONF, CONF_SCHEMA)
# Initialize all modules and start main loop
if args.dynamic_whitelist:
logger.info('Using dynamically generated whitelist. (--dynamic-whitelist detected)')
init(_CONF) init(_CONF)
old_state = get_state() old_state = None
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True: while True:
new_state = get_state() new_state = get_state()
# Log state transition # Log state transition
if new_state != old_state: if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower())) telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logging.info('Changing state to: %s', new_state.name) logger.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED: if new_state == State.STOPPED:
time.sleep(1) time.sleep(1)
elif new_state == State.RUNNING: elif new_state == State.RUNNING:
_process() throttle(
# We need to sleep here because otherwise we would run into bittrex rate limit _process,
time.sleep(exchange.get_sleep_time()) min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
)
old_state = new_state old_state = new_state

View File

@ -1,7 +1,15 @@
import argparse
import enum import enum
import logging
from typing import Any, Callable
import time
from wrapt import synchronized from wrapt import synchronized
from freqtrade import __version__
logger = logging.getLogger(__name__)
class State(enum.Enum): class State(enum.Enum):
RUNNING = 0 RUNNING = 0
@ -32,6 +40,57 @@ def get_state() -> State:
return _STATE return _STATE
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
"""
Throttles the given callable that it
takes at least `min_secs` to finish execution.
:param func: Any callable
:param min_secs: minimum execution time in seconds
:return: Any
"""
start = time.time()
result = func(*args, **kwargs)
end = time.time()
duration = max(min_secs - (end - start), 0.0)
logger.debug('Throttling %s for %.2f seconds', func.__name__, duration)
time.sleep(duration)
return result
def build_arg_parser() -> argparse.ArgumentParser:
""" Builds and returns an ArgumentParser instance """
parser = argparse.ArgumentParser(
description='Simple High Frequency Trading Bot for crypto currencies'
)
parser.add_argument(
'-c', '--config',
help='specify configuration file (default: config.json)',
dest='config',
default='config.json',
type=str,
metavar='PATH',
)
parser.add_argument(
'-v', '--verbose',
help='be verbose',
action='store_const',
dest='loglevel',
const=logging.DEBUG,
default=logging.INFO,
)
parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
)
parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist based on 24h BaseVolume',
action='store_true',
)
return parser
# Required json-schema for user specified config # Required json-schema for user specified config
CONF_SCHEMA = { CONF_SCHEMA = {
'type': 'object', 'type': 'object',
@ -71,6 +130,12 @@ CONF_SCHEMA = {
'required': ['enabled', 'token', 'chat_id'] 'required': ['enabled', 'token', 'chat_id']
}, },
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'}
}
}
}, },
'definitions': { 'definitions': {
'exchange': { 'exchange': {

View File

@ -11,8 +11,6 @@ from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}

View File

@ -273,18 +273,21 @@ def _balance(bot: Bot, update: Update) -> None:
Handler for /balance Handler for /balance
Returns current account balance per crypto Returns current account balance per crypto
""" """
output = "" output = ''
balances = exchange.get_balances() balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
output = '`All balances are zero.`'
for currency in balances: for currency in balances:
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
continue
output += """*Currency*: {Currency} output += """*Currency*: {Currency}
*Available*: {Available} *Available*: {Available}
*Balance*: {Balance} *Balance*: {Balance}
*Pending*: {Pending} *Pending*: {Pending}
""".format(**currency) """.format(**currency)
send_msg(output) send_msg(output)

View File

@ -37,7 +37,8 @@ def default_conf():
"BTC_ETH", "BTC_ETH",
"BTC_TKN", "BTC_TKN",
"BTC_TRST", "BTC_TRST",
"BTC_SWT" "BTC_SWT",
"BTC_BCC"
] ]
}, },
"telegram": { "telegram": {
@ -90,6 +91,36 @@ def ticker():
}) })
@pytest.fixture
def health():
return MagicMock(return_value=[{
'Currency': 'BTC',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'ETH',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'TRST',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'SWT',
'IsActive': True,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}, {
'Currency': 'BCC',
'IsActive': False,
'LastChecked': '2017-11-13T20:15:00.00',
'Notice': None
}])
@pytest.fixture @pytest.fixture
def limit_buy_order(): def limit_buy_order():
return { return {

View File

@ -8,7 +8,9 @@ from freqtrade.exchange import validate_pairs
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.get_markets = MagicMock(return_value=['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']) api_mock.get_markets = MagicMock(return_value=[
'BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT', 'BTC_BCC',
])
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)
validate_pairs(default_conf['exchange']['pair_whitelist']) validate_pairs(default_conf['exchange']['pair_whitelist'])

View File

@ -15,41 +15,44 @@ from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data # set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1200 TARGET_TRADES = 1300
TOTAL_TRIES = 4
current_tries = 0
def buy_strategy_generator(params): def buy_strategy_generator(params):
print(params)
def populate_buy_trend(dataframe: DataFrame) -> DataFrame: def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']: if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100']) conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['uptrend_short_ema']['enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10'])
if params['mfi']['enabled']: if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value']) conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']: if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value']) conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']: if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value']) conditions.append(dataframe['adx'] > params['adx']['value'])
if params['cci']['enabled']:
conditions.append(dataframe['cci'] < params['cci']['value'])
if params['rsi']['enabled']: if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value']) conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']: if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar']) conditions.append(dataframe['close'] > dataframe['sar'])
if params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if params['uptrend_sma']['enabled']: if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1) prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma) conditions.append(dataframe['sma'] > prevsma)
prev_fastd = dataframe['fastd'].shift(1)
# TRIGGERS # TRIGGERS
triggers = { triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'], 'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10), 'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)), 'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])), 'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])), 'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
} }
conditions.append(triggers.get(params['trigger']['type'])) conditions.append(triggers.get(params['trigger']['type']))
@ -72,13 +75,16 @@ def test_hyperopt(backtest_conf, backdata, mocker):
results = backtest(backtest_conf, backdata, mocker) results = backtest(backtest_conf, backdata, mocker)
result = format_results(results) result = format_results(results)
print(result)
total_profit = results.profit.sum() * 1000 total_profit = results.profit.sum() * 1000
trade_count = len(results.index) trade_count = len(results.index)
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5) trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
profit_loss = exp(-total_profit**3 / 10**11) profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
global current_tries
current_tries += 1
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
return { return {
'loss': trade_loss + profit_loss, 'loss': trade_loss + profit_loss,
@ -89,32 +95,36 @@ def test_hyperopt(backtest_conf, backdata, mocker):
space = { space = {
'mfi': hp.choice('mfi', [ 'mfi': hp.choice('mfi', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)} {'enabled': True, 'value': hp.quniform('mfi-value', 5, 25, 1)}
]), ]),
'fastd': hp.choice('fastd', [ 'fastd': hp.choice('fastd', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)} {'enabled': True, 'value': hp.quniform('fastd-value', 10, 50, 1)}
]), ]),
'adx': hp.choice('adx', [ 'adx': hp.choice('adx', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)} {'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'cci': hp.choice('cci', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
]), ]),
'rsi': hp.choice('rsi', [ 'rsi': hp.choice('rsi', [
{'enabled': False}, {'enabled': False},
{'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)} {'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]), ]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [ 'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False}, {'enabled': False},
{'enabled': True} {'enabled': True}
]), ]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [ 'over_sar': hp.choice('over_sar', [
{'enabled': False}, {'enabled': False},
{'enabled': True} {'enabled': True}
]), ]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [ 'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False}, {'enabled': False},
{'enabled': True} {'enabled': True}
@ -125,10 +135,13 @@ def test_hyperopt(backtest_conf, backdata, mocker):
{'type': 'ao_cross_zero'}, {'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'}, {'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'}, {'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]), ]),
} }
trials = Trials() trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials) best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=TOTAL_TRIES, trials=trials)
print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================') print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
print('Best parameters {}'.format(best)) print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss')) newlist = sorted(trials.results, key=itemgetter('loss'))

View File

@ -13,13 +13,14 @@ from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_process_trade_creation(default_conf, ticker, mocker): def test_process_trade_creation(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy')) buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
@ -41,7 +42,7 @@ def test_process_trade_creation(default_conf, ticker, mocker):
assert trade.amount == 0.6864067381401302 assert trade.amount == 0.6864067381401302
def test_process_exchange_failures(default_conf, ticker, mocker): def test_process_exchange_failures(default_conf, ticker, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
@ -49,6 +50,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException)) buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
result = _process() result = _process()
@ -56,7 +58,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
assert sleep_mock.has_calls() assert sleep_mock.has_calls()
def test_process_runtime_error(default_conf, ticker, mocker): def test_process_runtime_error(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.main.telegram', init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
@ -64,6 +66,7 @@ def test_process_runtime_error(default_conf, ticker, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError)) buy=MagicMock(side_effect=RuntimeError))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
@ -74,13 +77,14 @@ def test_process_runtime_error(default_conf, ticker, mocker):
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0] assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker): def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=ticker, get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order)) get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))

View File

@ -0,0 +1,20 @@
# pragma pylint: disable=missing-docstring
import time
from freqtrade.misc import throttle
def test_throttle():
def func():
return 42
start = time.time()
result = throttle(func, 0.1)
end = time.time()
assert result == 42
assert end - start > 0.1
result = throttle(func, -1)
assert result == 42

View File

@ -2,6 +2,7 @@
SQLAlchemy==1.1.14 SQLAlchemy==1.1.14
python-telegram-bot==8.1.1 python-telegram-bot==8.1.1
arrow==0.10.0 arrow==0.10.0
cachetools==2.0.1
requests==2.18.4 requests==2.18.4
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11

View File

@ -34,6 +34,7 @@ setup(name='freqtrade',
'jsonschema', 'jsonschema',
'TA-Lib', 'TA-Lib',
'tabulate', 'tabulate',
'cachetools',
], ],
dependency_links=[ dependency_links=[
"git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex" "git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"