Compare commits

...

36 Commits

Author SHA1 Message Date
gcarq
b115963a70 Merge branch 'release/0.14.2' 2017-11-16 00:40:44 +01:00
gcarq
2e953a937d version bump 2017-11-16 00:40:36 +01:00
gcarq
4e05691cab check if balance list is empty (fixes #105) 2017-11-16 00:01:47 +01:00
gcarq
b5f58724a0 get_ticker_history: check if result is set (fixes #103) 2017-11-15 23:16:54 +01:00
gcarq
b83309b55d reduce calls_per_second to 1 2017-11-15 23:16:39 +01:00
gcarq
e8101a6da5 default BaseVolume to 0.0 if null 2017-11-14 17:48:19 +01:00
gcarq
dd9cb008fb refresh whitelist based on wallet health (fixes #60)
Refreshs the whitelist in each iteration based on the wallet health,
disabled wallets will be removed from the whitelist automatically.
2017-11-13 21:34:47 +01:00
gcarq
81f7172c4a sanitize get_ticker_history (fixes #100) 2017-11-13 19:54:09 +01:00
Michael Egger
bab59fbacd Merge pull request #99 from gcarq/more_triggers2
Expanding hyperopt
2017-11-13 12:11:15 +01:00
Janne Sinivirta
0f0b10b6cc adjust search spaces 2017-11-13 07:28:56 +02:00
Janne Sinivirta
8e68c5358e clean up prints during hyperopt 2017-11-12 09:44:31 +02:00
Janne Sinivirta
660f01b514 add hilbert transform leadsine trigger 2017-11-12 09:13:54 +02:00
Janne Sinivirta
13537e3ce4 add short ema guard to hyperopt 2017-11-12 08:45:32 +02:00
Janne Sinivirta
2963a90008 add stochastics trigger 2017-11-12 08:38:52 +02:00
Janne Sinivirta
15b20b83fa optimize hyperopt objective function 2017-11-12 08:30:58 +02:00
gcarq
1c3c316e45 reduce calls_per_second 2017-11-11 21:29:35 +01:00
gcarq
517879382b Add argument for dynamic-whitelist handling
If --dynamic-whitelist is passed the whitelist in the config file
is ignored. It gets automatically refreshed every 30 minutes and
currently selects the 20 topmost BaseVolume markets
2017-11-11 19:20:53 +01:00
gcarq
bcd3340a80 implement get_market_summaries 2017-11-11 19:20:16 +01:00
gcarq
12ae1e111e use get_candles from python-bittrex 2017-11-11 17:14:55 +01:00
gcarq
d3b3370f23 Add configurable throttle mechanism 2017-11-11 16:47:19 +01:00
gcarq
8f817a3634 use TTLCache for get_ticker_history 2017-11-11 15:29:31 +01:00
Janne Sinivirta
cf79b15651 use discrete values for filters 2017-11-11 11:50:10 +02:00
Janne Sinivirta
a4284351e3 fix green_candle 2017-11-11 11:22:12 +02:00
Janne Sinivirta
906caf329b remove two unused or poorly performing indicators 2017-11-11 11:22:12 +02:00
Janne Sinivirta
3db13fae13 add green_candle guard 2017-11-11 11:22:12 +02:00
Janne Sinivirta
274972f7af make faststoch trigger use crossed_above helper 2017-11-11 11:22:11 +02:00
Janne Sinivirta
83fd27e031 add sar reversal as trigger 2017-11-11 11:22:11 +02:00
gcarq
3126dcfcea drop sleep_time and use python-bittrex request delay 2017-11-10 23:39:49 +01:00
Michael Egger
72aec6c320 Merge pull request #96 from gcarq/feature/add-argparse
add argparse and implement basic arguments
2017-11-10 18:04:03 +01:00
gcarq
b709ccbf53 enhance logging messages 2017-11-10 17:56:03 +01:00
gcarq
7e99b13742 add missing commands to README 2017-11-10 17:27:19 +01:00
gcarq
8b464033ff add missing commands to README 2017-11-10 17:26:52 +01:00
gcarq
93c525a8fa Merge branch 'master' into develop 2017-11-10 17:18:21 +01:00
gcarq
54b15c1556 update README 2017-11-10 17:17:51 +01:00
gcarq
029f32af63 Merge tag '0.14.1' into develop
0.14.1
2017-11-09 23:53:14 +01:00
gcarq
e01c85bb3a add argparse and implement basic arguments 2017-11-08 22:43:47 +01:00
18 changed files with 379 additions and 99 deletions

View File

@@ -22,8 +22,11 @@ Persistence is achieved through sqlite.
* /status [table]: Lists all open trades
* /count: Displays number of open trades
* /profit: Lists cumulative profit from all finished trades
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /forcesell <trade_id>|all: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair
* /balance: Show account balance per currency
* /help: Show help message
* /version: Show version
### Config
`minimal_roi` is a JSON object where the key is a duration
@@ -112,14 +115,14 @@ filesystem):
```
$ cd ~/.freq
$ touch tradesv2.sqlite
$ touch tradesv3.sqlite
$ docker run -d \
--name freqtrade \
-v ~/.freq/config.json:/freqtrade/config.json \
-v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \
-v ~/.freq/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \
freqtrade
```
If you are using `dry_run=True` you need to bind `tradesv2.dry_run.sqlite` instead of `tradesv2.sqlite`.
If you are using `dry_run=True` it's not necessary to mount `tradesv3.sqlite`.
You can then use the following commands to monitor and manage your container:

View File

@@ -34,5 +34,8 @@
"token": "token",
"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

View File

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

View File

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

View File

@@ -1,14 +1,14 @@
import logging
from typing import List, Dict
import requests
from bittrex.bittrex import Bittrex as _Bittrex
from bittrex.bittrex import Bittrex as _Bittrex, API_V2_0, API_V1_1
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_API_V2: _Bittrex = None
_EXCHANGE_CONF: dict = {}
@@ -18,22 +18,23 @@ class Bittrex(Exchange):
"""
# Base URL and API endpoints
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'
@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:
global _API, _EXCHANGE_CONF
global _API, _API_V2, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
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
@@ -81,13 +82,17 @@ class Bittrex(Exchange):
raise RuntimeError('{message} params=({pair})'.format(
message=data['message'],
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 {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'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:
interval = 'oneMin'
elif tick_interval == 5:
@@ -95,10 +100,18 @@ class Bittrex(Exchange):
else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = requests.get(self.TICKER_METHOD, params={
'marketName': pair.replace('_', '-'),
'tickInterval': interval,
}).json()
data = _API_V2.get_candles(pair.replace('_', '-'), interval)
# These sanity check are necessary because bittrex cannot keep their API stable.
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']:
raise RuntimeError('{message} params=({pair})'.format(
message=data['message'],
@@ -139,3 +152,20 @@ class Bittrex(Exchange):
if not data['success']:
raise RuntimeError('{message}'.format(message=data['message']))
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
"""
@property
@abstractmethod
def sleep_time(self) -> float:
"""
Sleep time in seconds for the main loop to avoid API rate limits.
:return: float
"""
@abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str:
"""
@@ -82,7 +74,7 @@ class Exchange(ABC):
"""
@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.
:param pair: Pair as str, format: BTC_ETC
@@ -139,3 +131,41 @@ class Exchange(ABC):
Returns all available markets.
: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 traceback
from datetime import datetime
from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM
from typing import Dict, Optional, List
import requests
from cachetools import cached, TTLCache
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.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger = logging.getLogger('freqtrade')
_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,
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
"""
state_changed = False
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
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if len(trades) < _CONF['max_open_trades']:
@@ -42,7 +72,10 @@ def _process() -> bool:
Trade.session.add(trade)
state_changed = True
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:
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.open_order_id is None:
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 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
: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'])
# Check if stake_amount is fulfilled
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)
@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:
"""
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
:return: None
"""
logger.info('Starting freqtrade %s', __version__)
global _CONF
with open('config.json') as file:
_CONF = json.load(file)
args = build_arg_parser().parse_args()
# 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 ...')
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)
old_state = get_state()
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
old_state = None
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
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:
time.sleep(1)
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(exchange.get_sleep_time())
throttle(
_process,
min_secs=_CONF['internals'].get('process_throttle_secs', 10),
dynamic_whitelist=args.dynamic_whitelist,
)
old_state = new_state

View File

@@ -1,7 +1,15 @@
import argparse
import enum
import logging
from typing import Any, Callable
import time
from wrapt import synchronized
from freqtrade import __version__
logger = logging.getLogger(__name__)
class State(enum.Enum):
RUNNING = 0
@@ -32,6 +40,57 @@ def get_state() -> 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
CONF_SCHEMA = {
'type': 'object',
@@ -71,6 +130,12 @@ CONF_SCHEMA = {
'required': ['enabled', 'token', 'chat_id']
},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',
'properties': {
'process_throttle_secs': {'type': 'number'}
}
}
},
'definitions': {
'exchange': {

View File

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

View File

@@ -273,18 +273,21 @@ def _balance(bot: Bot, update: Update) -> None:
Handler for /balance
Returns current account balance per crypto
"""
output = ""
balances = exchange.get_balances()
output = ''
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:
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
continue
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
""".format(**currency)
send_msg(output)

View File

@@ -37,7 +37,8 @@ def default_conf():
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT"
"BTC_SWT",
"BTC_BCC"
]
},
"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
def limit_buy_order():
return {

View File

@@ -8,7 +8,9 @@ from freqtrade.exchange import validate_pairs
def test_validate_pairs(default_conf, mocker):
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.dict('freqtrade.exchange._CONF', default_conf)
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
# 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):
print(params)
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['uptrend_short_ema']['enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10'])
if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['cci']['enabled']:
conditions.append(dataframe['cci'] < params['cci']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
prev_fastd = dataframe['fastd'].shift(1)
# TRIGGERS
triggers = {
'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)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'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']))
@@ -72,13 +75,16 @@ def test_hyperopt(backtest_conf, backdata, mocker):
results = backtest(backtest_conf, backdata, mocker)
result = format_results(results)
print(result)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5)
profit_loss = exp(-total_profit**3 / 10**11)
trade_loss = 1 - 0.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
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 {
'loss': trade_loss + profit_loss,
@@ -89,32 +95,36 @@ def test_hyperopt(backtest_conf, backdata, mocker):
space = {
'mfi': hp.choice('mfi', [
{'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', [
{'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', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
]),
'cci': hp.choice('cci', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'rsi': hp.choice('rsi', [
{'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', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
@@ -125,10 +135,13 @@ def test_hyperopt(backtest_conf, backdata, mocker):
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
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('Best parameters {}'.format(best))
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
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'))
init(default_conf, create_engine('sqlite://'))
@@ -41,7 +42,7 @@ def test_process_trade_creation(default_conf, ticker, mocker):
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
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',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, create_engine('sqlite://'))
result = _process()
@@ -56,7 +58,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker):
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()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
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',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(side_effect=RuntimeError))
init(default_conf, create_engine('sqlite://'))
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]
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.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_wallet_health=health,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
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
python-telegram-bot==8.1.1
arrow==0.10.0
cachetools==2.0.1
requests==2.18.4
urllib3==1.22
wrapt==1.10.11

View File

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