Merge branch 'release/0.14.3'
This commit is contained in:
commit
e9dbdc9247
@ -1,3 +1,10 @@
|
|||||||
|
[MASTER]
|
||||||
|
extension-pkg-whitelist=numpy,talib
|
||||||
|
|
||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=logger
|
good-names=logger
|
||||||
ignore=vendor
|
ignore=vendor
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
ignored-modules=numpy,talib
|
||||||
|
|
||||||
|
37
README.md
37
README.md
@ -137,6 +137,43 @@ $ docker start freqtrade
|
|||||||
You do not need to rebuild the image for configuration
|
You do not need to rebuild the image for configuration
|
||||||
changes, it will suffice to edit `config.json` and restart the container.
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
```
|
||||||
|
usage: freqtrade [-h] [-c PATH] [-v] [--version] [--dynamic-whitelist]
|
||||||
|
{backtesting} ...
|
||||||
|
|
||||||
|
Simple High Frequency Trading Bot for crypto currencies
|
||||||
|
|
||||||
|
positional arguments:
|
||||||
|
{backtesting}
|
||||||
|
backtesting backtesting module
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
specify configuration file (default: config.json)
|
||||||
|
-v, --verbose be verbose
|
||||||
|
--version show program's version number and exit
|
||||||
|
--dynamic-whitelist dynamically generate and update whitelist based on 24h
|
||||||
|
BaseVolume
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backtesting
|
||||||
|
|
||||||
|
Backtesting also uses the config specified via `-c/--config`.
|
||||||
|
|
||||||
|
```
|
||||||
|
usage: freqtrade backtesting [-h] [-l] [-i INT]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
-l, --live using live data
|
||||||
|
-i INT, --ticker-interval INT
|
||||||
|
specify ticker interval in minutes (default: 5)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Execute tests
|
### Execute tests
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
__version__ = '0.14.2'
|
""" FreqTrade bot """
|
||||||
|
__version__ = '0.14.3'
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
@ -6,10 +10,15 @@ import talib.abstract as ta
|
|||||||
from pandas import DataFrame, to_datetime
|
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, crossed_above
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SignalType(Enum):
|
||||||
|
""" Enum to distinguish between buy and sell signals """
|
||||||
|
BUY = "buy"
|
||||||
|
SELL = "sell"
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -57,18 +66,28 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators, populates the buy trend for the given dataframe
|
Based on TA indicators, populates the buy signal for the given dataframe
|
||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.ix[
|
dataframe.loc[
|
||||||
(dataframe['close'] < dataframe['sma']) &
|
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
(dataframe['mfi'] < 25) &
|
(dataframe['rsi'] < 37) &
|
||||||
(dataframe['fastd'] < 25) &
|
(dataframe['fastd'] < 48) &
|
||||||
(dataframe['adx'] > 30),
|
(dataframe['adx'] > 31),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Based on TA indicators, populates the sell signal for the given dataframe
|
||||||
|
:param dataframe: DataFrame
|
||||||
|
:return: DataFrame with buy column
|
||||||
|
"""
|
||||||
|
dataframe.loc[
|
||||||
|
(crossed_above(dataframe['rsi'], 70)),
|
||||||
|
'sell'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@ -87,12 +106,16 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
dataframe = parse_ticker_dataframe(ticker_hist)
|
dataframe = parse_ticker_dataframe(ticker_hist)
|
||||||
dataframe = populate_indicators(dataframe)
|
dataframe = populate_indicators(dataframe)
|
||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_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
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair: str) -> bool:
|
def get_signal(pair: str, signal: SignalType) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based several technical analysis indicators
|
Calculates current signal based several technical analysis indicators
|
||||||
: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
|
||||||
"""
|
"""
|
||||||
@ -107,6 +130,6 @@ def get_buy_signal(pair: str) -> bool:
|
|||||||
if signal_date < arrow.now() - timedelta(minutes=10):
|
if signal_date < arrow.now() - timedelta(minutes=10):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
signal = latest['buy'] == 1
|
result = latest[signal.value] == 1
|
||||||
logger.debug('buy_trigger: %s (pair=%s, signal=%s)', latest['date'], pair, signal)
|
logger.debug('%s_trigger: %s (pair=%s, signal=%s)', signal.value, latest['date'], pair, result)
|
||||||
return signal
|
return result
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
# pragma pylint: disable=W0603
|
||||||
|
""" Cryptocurrency Exchanges support """
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
|
|
||||||
from freqtrade.exchange.bittrex import Bittrex
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
@ -63,7 +66,12 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
markets = _API.get_markets()
|
try:
|
||||||
|
markets = _API.get_markets()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
||||||
|
return
|
||||||
|
|
||||||
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):
|
||||||
@ -77,7 +85,7 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
@ -95,7 +103,7 @@ def buy(pair: str, rate: float, amount: float) -> str:
|
|||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
global _DRY_RUN_OPEN_ORDERS
|
global _DRY_RUN_OPEN_ORDERS
|
||||||
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6))
|
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
|
||||||
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
_DRY_RUN_OPEN_ORDERS[order_id] = {
|
||||||
'pair': pair,
|
'pair': pair,
|
||||||
'rate': rate,
|
'rate': rate,
|
||||||
|
@ -2,6 +2,7 @@ import logging
|
|||||||
from typing import List, Dict
|
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 freqtrade.exchange.interface import Exchange
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
@ -82,9 +83,13 @@ 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(
|
if not data.get('result') \
|
||||||
message=data['message'],
|
or not data['result'].get('Bid') \
|
||||||
|
or not data['result'].get('Ask') \
|
||||||
|
or not data['result'].get('Last'):
|
||||||
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
pair=pair))
|
pair=pair))
|
||||||
return {
|
return {
|
||||||
'bid': float(data['result']['Bid']),
|
'bid': float(data['result']['Bid']),
|
||||||
@ -104,13 +109,16 @@ class Bittrex(Exchange):
|
|||||||
|
|
||||||
# These sanity check are necessary because bittrex cannot keep their API stable.
|
# These sanity check are necessary because bittrex cannot keep their API stable.
|
||||||
if not data.get('result'):
|
if not data.get('result'):
|
||||||
return []
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
|
message='Got invalid response from bittrex',
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
for prop in ['C', 'V', 'O', 'H', 'L', 'T']:
|
||||||
for tick in data['result']:
|
for tick in data['result']:
|
||||||
if prop not in tick.keys():
|
if prop not in tick.keys():
|
||||||
logger.warning('Required property %s not present in response', prop)
|
raise ContentDecodingError('{message} params=({pair})'.format(
|
||||||
return []
|
message='Required property {} not present in response'.format(prop),
|
||||||
|
pair=pair))
|
||||||
|
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{message} params=({pair})'.format(
|
raise RuntimeError('{message} params=({pair})'.format(
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -10,13 +11,12 @@ from typing import Dict, Optional, List
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cachetools import cached, TTLCache
|
from cachetools import cached, TTLCache
|
||||||
from jsonschema import validate
|
|
||||||
|
|
||||||
from freqtrade import __version__, exchange, persistence
|
from freqtrade import __version__, exchange, persistence, rpc
|
||||||
from freqtrade.analyze import get_buy_signal
|
from freqtrade.analyze import get_signal, SignalType
|
||||||
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state, build_arg_parser, throttle
|
from freqtrade.misc import State, get_state, update_state, parse_args, throttle, \
|
||||||
|
load_config, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
|
||||||
|
|
||||||
logger = logging.getLogger('freqtrade')
|
logger = logging.getLogger('freqtrade')
|
||||||
|
|
||||||
@ -76,8 +76,8 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
'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 ValueError:
|
except FreqtradeException as e:
|
||||||
logger.exception('Unable to create trade')
|
logger.warning('Unable to create trade: %s', e)
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Get order details for actual price per unit
|
# Get order details for actual price per unit
|
||||||
@ -86,17 +86,19 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
logger.info('Got open order for %s', trade)
|
logger.info('Got open order for %s', trade)
|
||||||
trade.update(exchange.get_order(trade.open_order_id))
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
|
|
||||||
if not close_trade_if_fulfilled(trade):
|
if trade.is_open and trade.open_order_id is None:
|
||||||
# Check if we can sell our current pair
|
# Check if we can sell our current pair
|
||||||
state_changed = handle_trade(trade) or state_changed
|
state_changed = handle_trade(trade) or state_changed
|
||||||
|
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
|
||||||
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
|
logger.warning(
|
||||||
logger.exception(msg)
|
'Got %s in _process(), retrying in 30 seconds...',
|
||||||
|
error
|
||||||
|
)
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
|
rpc.send_msg('*Status:* Got RuntimeError:\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.'
|
||||||
))
|
))
|
||||||
@ -105,27 +107,6 @@ def _process(dynamic_whitelist: Optional[bool] = False) -> bool:
|
|||||||
return state_changed
|
return state_changed
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
|
||||||
"""
|
|
||||||
Checks if the trade is closable, and if so it is being closed.
|
|
||||||
:param trade: Trade
|
|
||||||
:return: True if trade has been closed else False
|
|
||||||
"""
|
|
||||||
# If we don't have an open order and the close rate is already set,
|
|
||||||
# we can close this trade.
|
|
||||||
if trade.close_profit is not None \
|
|
||||||
and trade.close_date is not None \
|
|
||||||
and trade.close_rate is not None \
|
|
||||||
and trade.open_order_id is None:
|
|
||||||
trade.is_open = False
|
|
||||||
logger.info(
|
|
||||||
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
|
||||||
trade
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, limit: float) -> None:
|
def execute_sell(trade: Trade, limit: float) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a limit sell for the given trade and limit
|
Executes a limit sell for the given trade and limit
|
||||||
@ -138,20 +119,18 @@ def execute_sell(trade: Trade, limit: float) -> None:
|
|||||||
trade.open_order_id = order_id
|
trade.open_order_id = order_id
|
||||||
|
|
||||||
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
||||||
message = '*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format(
|
||||||
trade.exchange,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
limit,
|
limit,
|
||||||
fmt_exp_profit
|
fmt_exp_profit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
|
|
||||||
|
|
||||||
def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -> bool:
|
||||||
"""
|
"""
|
||||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
Based an earlier trade and current price and ROI configuration, decides whether bot should sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = trade.calc_profit(current_rate)
|
current_profit = trade.calc_profit(current_rate)
|
||||||
@ -159,9 +138,9 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
|||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
# Check if time matches and current rate is above threshold
|
||||||
|
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||||
# Check if time matches and current rate is above threshold
|
|
||||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
|
||||||
if time_diff > float(duration) and current_profit > threshold:
|
if time_diff > float(duration) and current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -179,7 +158,7 @@ def handle_trade(trade: Trade) -> bool:
|
|||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
if min_roi_reached(trade, current_rate, datetime.utcnow()) or get_signal(trade.pair, SignalType.SELL):
|
||||||
execute_sell(trade, current_rate)
|
execute_sell(trade, current_rate)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
@ -206,7 +185,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 ValueError(
|
raise FreqtradeException(
|
||||||
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -216,11 +195,11 @@ 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 ValueError('No pair in whitelist')
|
raise FreqtradeException('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:
|
||||||
if get_buy_signal(_pair):
|
if get_signal(_pair, SignalType.BUY):
|
||||||
pair = _pair
|
pair = _pair
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -233,14 +212,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
|
|
||||||
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
|
||||||
message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
rpc.send_msg('*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
|
||||||
exchange.get_name().upper(),
|
exchange.get_name().upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
buy_limit
|
buy_limit
|
||||||
)
|
))
|
||||||
logger.info(message)
|
|
||||||
telegram.send_msg(message)
|
|
||||||
# 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,
|
return Trade(pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
@ -260,7 +237,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Initialize all modules
|
# Initialize all modules
|
||||||
telegram.init(config)
|
rpc.init(config)
|
||||||
persistence.init(config, db_url)
|
persistence.init(config, db_url)
|
||||||
exchange.init(config)
|
exchange.init(config)
|
||||||
|
|
||||||
@ -298,11 +275,11 @@ def cleanup(*args, **kwargs) -> None:
|
|||||||
Cleanup the application state und finish all pending tasks
|
Cleanup the application state und finish all pending tasks
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
telegram.send_msg('*Status:* `Stopping trader...`')
|
rpc.send_msg('*Status:* `Stopping trader...`')
|
||||||
logger.info('Stopping trader and cleaning up modules...')
|
logger.info('Stopping trader and cleaning up modules...')
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
persistence.cleanup()
|
persistence.cleanup()
|
||||||
telegram.cleanup()
|
rpc.cleanup()
|
||||||
exit(0)
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
@ -312,7 +289,9 @@ def main():
|
|||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _CONF
|
global _CONF
|
||||||
args = build_arg_parser().parse_args()
|
args = parse_args(sys.argv[1:])
|
||||||
|
if not args:
|
||||||
|
exit(0)
|
||||||
|
|
||||||
# Initialize logger
|
# Initialize logger
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
@ -327,12 +306,7 @@ def main():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Load and validate configuration
|
# Load and validate configuration
|
||||||
with open(args.config) as file:
|
_CONF = load_config(args.config)
|
||||||
_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
|
# Initialize all modules and start main loop
|
||||||
if args.dynamic_whitelist:
|
if args.dynamic_whitelist:
|
||||||
@ -343,7 +317,7 @@ def main():
|
|||||||
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()))
|
rpc.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
|
||||||
logger.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:
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import enum
|
import enum
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Callable
|
import os
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
from typing import Any, Callable, List, Dict
|
||||||
|
|
||||||
|
from jsonschema import validate, Draft4Validator
|
||||||
|
from jsonschema.exceptions import best_match, ValidationError
|
||||||
from wrapt import synchronized
|
from wrapt import synchronized
|
||||||
|
|
||||||
from freqtrade import __version__
|
from freqtrade import __version__
|
||||||
@ -11,6 +15,10 @@ 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
|
||||||
@ -40,6 +48,27 @@ def get_state() -> State:
|
|||||||
return _STATE
|
return _STATE
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(path: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Loads a config file from the given path
|
||||||
|
:param path: path as str
|
||||||
|
:return: configuration as dictionary
|
||||||
|
"""
|
||||||
|
with open(path) as file:
|
||||||
|
conf = json.load(file)
|
||||||
|
if 'internals' not in conf:
|
||||||
|
conf['internals'] = {}
|
||||||
|
logger.info('Validating configuration ...')
|
||||||
|
try:
|
||||||
|
validate(conf, CONF_SCHEMA)
|
||||||
|
return conf
|
||||||
|
except ValidationError:
|
||||||
|
logger.fatal('Configuration is not valid! See config.json.example')
|
||||||
|
raise ValidationError(
|
||||||
|
best_match(Draft4Validator(CONF_SCHEMA).iter_errors(conf)).message
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
||||||
"""
|
"""
|
||||||
Throttles the given callable that it
|
Throttles the given callable that it
|
||||||
@ -57,8 +86,11 @@ def throttle(func: Callable[..., Any], min_secs: float, *args, **kwargs) -> Any:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def build_arg_parser() -> argparse.ArgumentParser:
|
def parse_args(args: List[str]):
|
||||||
""" Builds and returns an ArgumentParser instance """
|
"""
|
||||||
|
Parses given arguments and returns an argparse Namespace instance.
|
||||||
|
Returns None if a sub command has been selected and executed.
|
||||||
|
"""
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description='Simple High Frequency Trading Bot for crypto currencies'
|
description='Simple High Frequency Trading Bot for crypto currencies'
|
||||||
)
|
)
|
||||||
@ -88,7 +120,54 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|||||||
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
help='dynamically generate and update whitelist based on 24h BaseVolume',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
)
|
)
|
||||||
return parser
|
build_subcommands(parser)
|
||||||
|
parsed_args = parser.parse_args(args)
|
||||||
|
|
||||||
|
# No subcommand as been selected
|
||||||
|
if not hasattr(parsed_args, 'func'):
|
||||||
|
return parsed_args
|
||||||
|
|
||||||
|
parsed_args.func(parsed_args)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def build_subcommands(parser: argparse.ArgumentParser) -> None:
|
||||||
|
""" Builds and attaches all subcommands """
|
||||||
|
subparsers = parser.add_subparsers(dest='subparser')
|
||||||
|
backtest = subparsers.add_parser('backtesting', help='backtesting module')
|
||||||
|
backtest.set_defaults(func=start_backtesting)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-l', '--live',
|
||||||
|
action='store_true',
|
||||||
|
dest='live',
|
||||||
|
help='using live data',
|
||||||
|
)
|
||||||
|
backtest.add_argument(
|
||||||
|
'-i', '--ticker-interval',
|
||||||
|
help='specify ticker interval in minutes (default: 5)',
|
||||||
|
dest='ticker_interval',
|
||||||
|
default=5,
|
||||||
|
type=int,
|
||||||
|
metavar='INT',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def start_backtesting(args) -> None:
|
||||||
|
"""
|
||||||
|
Exports all args as environment variables and starts backtesting via pytest.
|
||||||
|
:param args: arguments namespace
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
os.environ.update({
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true' if args.live else '',
|
||||||
|
'BACKTEST_CONFIG': args.config,
|
||||||
|
'BACKTEST_TICKER_INTERVAL': str(args.ticker_interval),
|
||||||
|
})
|
||||||
|
path = os.path.join(os.path.dirname(__file__), 'tests', 'test_backtesting.py')
|
||||||
|
pytest.main(['-s', path])
|
||||||
|
|
||||||
|
|
||||||
# Required json-schema for user specified config
|
# Required json-schema for user specified config
|
||||||
@ -146,7 +225,10 @@ CONF_SCHEMA = {
|
|||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
'type': 'array',
|
'type': 'array',
|
||||||
'items': {'type': 'string'},
|
'items': {
|
||||||
|
'type': 'string',
|
||||||
|
'pattern': '^[0-9A-Z]+_[0-9A-Z]+$'
|
||||||
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -85,20 +85,27 @@ class Trade(_DECL_BASE):
|
|||||||
if not order['closed']:
|
if not order['closed']:
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug('Updating trade (id=%d) ...', self.id)
|
logger.info('Updating trade (id=%d) ...', self.id)
|
||||||
if order['type'] == 'LIMIT_BUY':
|
if order['type'] == 'LIMIT_BUY':
|
||||||
# Update open rate and actual amount
|
# Update open rate and actual amount
|
||||||
self.open_rate = order['rate']
|
self.open_rate = order['rate']
|
||||||
self.amount = order['amount']
|
self.amount = order['amount']
|
||||||
|
logger.info('LIMIT_BUY has been fulfilled for %s.', self)
|
||||||
elif order['type'] == 'LIMIT_SELL':
|
elif order['type'] == 'LIMIT_SELL':
|
||||||
# Set close rate and set actual profit
|
# Set close rate and set actual profit
|
||||||
self.close_rate = order['rate']
|
self.close_rate = order['rate']
|
||||||
self.close_profit = self.calc_profit()
|
self.close_profit = self.calc_profit()
|
||||||
self.close_date = datetime.utcnow()
|
self.close_date = datetime.utcnow()
|
||||||
|
self.is_open = False
|
||||||
|
logger.info(
|
||||||
|
'Marking %s as closed as the trade is fulfilled and found no open orders for it.',
|
||||||
|
self
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError('Unknown order type: {}'.format(order['type']))
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
|
|
||||||
self.open_order_id = None
|
self.open_order_id = None
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
def calc_profit(self, rate: Optional[float] = None) -> float:
|
def calc_profit(self, rate: Optional[float] = None) -> float:
|
||||||
"""
|
"""
|
||||||
|
@ -1 +1,42 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
from . import telegram
|
from . import telegram
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
REGISTERED_MODULES = []
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes all enabled rpc modules
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if config['telegram'].get('enabled', False):
|
||||||
|
logger.info('Enabling rpc.telegram ...')
|
||||||
|
REGISTERED_MODULES.append('telegram')
|
||||||
|
telegram.init(config)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup() -> None:
|
||||||
|
"""
|
||||||
|
Stops all enabled rpc modules
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
logger.debug('Cleaning up rpc.telegram ...')
|
||||||
|
telegram.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def send_msg(msg: str) -> None:
|
||||||
|
"""
|
||||||
|
Send given markdown message to all registered rpc modules
|
||||||
|
:param msg: message
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
logger.info(msg)
|
||||||
|
if 'telegram' in REGISTERED_MODULES:
|
||||||
|
telegram.send_msg(msg)
|
||||||
|
@ -8,7 +8,7 @@ from tabulate import tabulate
|
|||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import and_, func, text
|
from sqlalchemy import and_, func, text
|
||||||
from telegram import ParseMode, Bot, Update
|
from telegram import ParseMode, Bot, Update
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError, TelegramError
|
||||||
from telegram.ext import CommandHandler, Updater
|
from telegram.ext import CommandHandler, Updater
|
||||||
|
|
||||||
from freqtrade import exchange, __version__
|
from freqtrade import exchange, __version__
|
||||||
@ -57,7 +57,7 @@ def init(config: dict) -> None:
|
|||||||
_UPDATER.dispatcher.add_handler(handle)
|
_UPDATER.dispatcher.add_handler(handle)
|
||||||
_UPDATER.start_polling(
|
_UPDATER.start_polling(
|
||||||
clean=True,
|
clean=True,
|
||||||
bootstrap_retries=3,
|
bootstrap_retries=-1,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
read_latency=60,
|
read_latency=60,
|
||||||
)
|
)
|
||||||
@ -475,13 +475,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
|||||||
return
|
return
|
||||||
|
|
||||||
bot = bot or _UPDATER.bot
|
bot = bot or _UPDATER.bot
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
try:
|
||||||
except NetworkError as error:
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
# Sometimes the telegram server resets the current connection,
|
except NetworkError as network_err:
|
||||||
# if this is the case we send the message again.
|
# Sometimes the telegram server resets the current connection,
|
||||||
logger.warning(
|
# if this is the case we send the message again.
|
||||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
logger.warning(
|
||||||
error.message
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
)
|
network_err.message
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
)
|
||||||
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
|
except TelegramError as telegram_err:
|
||||||
|
logger.warning('Got TelegramError: %s! Giving up on that message.', telegram_err.message)
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtesting_data(ticker_interval: int = 5):
|
||||||
|
path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
result = {}
|
||||||
|
pairs = [
|
||||||
|
'BTC_BCC', 'BTC_ETH', 'BTC_DASH', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_VTC', 'BTC_WAVES', 'BTC_LSK', 'BTC_XLM', 'BTC_OK',
|
||||||
|
]
|
||||||
|
for pair in pairs:
|
||||||
|
with open('{abspath}/testdata/{pair}-{ticker_interval}.json'.format(
|
||||||
|
abspath=path,
|
||||||
|
pair=pair,
|
||||||
|
ticker_interval=ticker_interval,
|
||||||
|
)) as tickerdata:
|
||||||
|
result[pair] = json.load(tickerdata)
|
||||||
|
return result
|
@ -1,5 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -55,6 +54,8 @@ def default_conf():
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def backtest_conf():
|
def backtest_conf():
|
||||||
return {
|
return {
|
||||||
|
"stake_currency": "BTC",
|
||||||
|
"stake_amount": 0.01,
|
||||||
"minimal_roi": {
|
"minimal_roi": {
|
||||||
"40": 0.0,
|
"40": 0.0,
|
||||||
"30": 0.01,
|
"30": 0.01,
|
||||||
@ -65,16 +66,6 @@ def backtest_conf():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
|
||||||
def backdata():
|
|
||||||
result = {}
|
|
||||||
for pair in ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']:
|
|
||||||
with open('freqtrade/tests/testdata/' + pair + '.json') as data_file:
|
|
||||||
result[pair] = json.load(data_file)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def update():
|
def update():
|
||||||
_update = Update(0)
|
_update = Update(0)
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0621
|
||||||
from datetime import datetime
|
|
||||||
import json
|
import json
|
||||||
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
get_buy_signal
|
get_signal, SignalType, populate_sell_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
with open('freqtrade/tests/testdata/BTC_ETH-1.json') as data_file:
|
||||||
return parse_ticker_dataframe(json.load(data_file))
|
return parse_ticker_dataframe(json.load(data_file))
|
||||||
|
|
||||||
|
|
||||||
@ -20,20 +20,34 @@ def test_dataframe_correct_columns(result):
|
|||||||
|
|
||||||
|
|
||||||
def test_dataframe_correct_length(result):
|
def test_dataframe_correct_length(result):
|
||||||
assert len(result.index) == 5751
|
assert len(result.index) == 14382
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
assert 'buy_price' in dataframe.columns
|
|
||||||
|
|
||||||
|
def test_populates_sell_trend(result):
|
||||||
|
dataframe = populate_sell_trend(populate_indicators(result))
|
||||||
|
assert 'sell' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert get_buy_signal('BTC-ETH')
|
assert get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert not get_buy_signal('BTC-ETH')
|
assert not get_signal('BTC-ETH', SignalType.BUY)
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_latest_sell_signal(mocker):
|
||||||
|
selldf = DataFrame([{'sell': 1, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
|
||||||
|
selldf = DataFrame([{'sell': 0, 'date': arrow.utcnow()}])
|
||||||
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=selldf)
|
||||||
|
assert not get_signal('BTC-ETH', SignalType.SELL)
|
||||||
|
@ -1,65 +1,154 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
import pytest
|
|
||||||
import arrow
|
import arrow
|
||||||
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import analyze_ticker
|
from freqtrade.analyze import parse_ticker_dataframe, populate_indicators, \
|
||||||
|
populate_buy_trend, populate_sell_trend
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.main import min_roi_reached
|
||||||
|
from freqtrade.misc import load_config
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def format_results(results):
|
def format_results(results: DataFrame):
|
||||||
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
return ('Made {:6d} buys. Average profit {: 5.2f}%. '
|
||||||
len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
|
'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format(
|
||||||
|
len(results.index),
|
||||||
|
results.profit.mean() * 100.0,
|
||||||
|
results.profit.sum(),
|
||||||
|
results.duration.mean() * 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_pair_results(pair, results):
|
def preprocess(backdata) -> Dict[str, DataFrame]:
|
||||||
print('For currency {}:'.format(pair))
|
processed = {}
|
||||||
print(format_results(results[results.currency == pair]))
|
for pair, pair_data in backdata.items():
|
||||||
|
processed[pair] = populate_indicators(parse_ticker_dataframe(pair_data))
|
||||||
|
return processed
|
||||||
|
|
||||||
|
|
||||||
def backtest(backtest_conf, backdata, mocker):
|
def get_timeframe(data: Dict[str, Dict]) -> Tuple[arrow.Arrow, arrow.Arrow]:
|
||||||
|
"""
|
||||||
|
Get the maximum timeframe for the given backtest data
|
||||||
|
:param data: dictionary with backtesting data
|
||||||
|
:return: tuple containing min_date, max_date
|
||||||
|
"""
|
||||||
|
min_date, max_date = None, None
|
||||||
|
for values in data.values():
|
||||||
|
sorted_values = sorted(values, key=lambda d: arrow.get(d['T']))
|
||||||
|
if not min_date or sorted_values[0]['T'] < min_date:
|
||||||
|
min_date = sorted_values[0]['T']
|
||||||
|
if not max_date or sorted_values[-1]['T'] > max_date:
|
||||||
|
max_date = sorted_values[-1]['T']
|
||||||
|
return arrow.get(min_date), arrow.get(max_date)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_text_table(data: Dict[str, Dict], results: DataFrame, stake_currency) -> str:
|
||||||
|
"""
|
||||||
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
|
:return: pretty printed table with tabulate as str
|
||||||
|
"""
|
||||||
|
tabular_data = []
|
||||||
|
headers = ['pair', 'buy count', 'avg profit', 'total profit', 'avg duration']
|
||||||
|
for pair in data:
|
||||||
|
result = results[results.currency == pair]
|
||||||
|
tabular_data.append([
|
||||||
|
pair,
|
||||||
|
len(result.index),
|
||||||
|
'{:.2f}%'.format(result.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(result.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(result.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
|
||||||
|
# Append Total
|
||||||
|
tabular_data.append([
|
||||||
|
'TOTAL',
|
||||||
|
len(results.index),
|
||||||
|
'{:.2f}%'.format(results.profit.mean() * 100.0),
|
||||||
|
'{:.08f} {}'.format(results.profit.sum(), stake_currency),
|
||||||
|
'{:.2f}'.format(results.duration.mean() * 5),
|
||||||
|
])
|
||||||
|
return tabulate(tabular_data, headers=headers)
|
||||||
|
|
||||||
|
|
||||||
|
def backtest(backtest_conf, processed, mocker):
|
||||||
trades = []
|
trades = []
|
||||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
|
||||||
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
for pair, pair_data in processed.items():
|
||||||
for pair, pair_data in backdata.items():
|
pair_data['buy'] = 0
|
||||||
mocked_history.return_value = pair_data
|
pair_data['sell'] = 0
|
||||||
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
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):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
open_rate=row.close,
|
open_rate=row.close,
|
||||||
open_date=row.date,
|
open_date=row.date,
|
||||||
amount=1,
|
amount=backtest_conf['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:].itertuples(index=True):
|
||||||
if should_sell(trade, row2.close, row2.date):
|
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)
|
||||||
|
|
||||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
trades.append((pair, current_profit, row2.Index - row.Index))
|
||||||
break
|
break
|
||||||
labels = ['currency', 'profit', 'duration']
|
labels = ['currency', 'profit', 'duration']
|
||||||
results = DataFrame.from_records(trades, columns=labels)
|
return DataFrame.from_records(trades, columns=labels)
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST'), reason="BACKTEST not set")
|
||||||
def test_backtest(backtest_conf, backdata, mocker, report=True):
|
def test_backtest(backtest_conf, mocker):
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
print('')
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
# Load configuration file based on env variable
|
||||||
for pair in backdata:
|
conf_path = os.environ.get('BACKTEST_CONFIG')
|
||||||
print_pair_results(pair, results)
|
if conf_path:
|
||||||
print('TOTAL OVER ALL TRADES:')
|
print('Using config: {} ...'.format(conf_path))
|
||||||
print(format_results(results))
|
config = load_config(conf_path)
|
||||||
|
else:
|
||||||
|
config = backtest_conf
|
||||||
|
|
||||||
|
# Parse ticker interval
|
||||||
|
ticker_interval = int(os.environ.get('BACKTEST_TICKER_INTERVAL') or 5)
|
||||||
|
print('Using ticker_interval: {} ...'.format(ticker_interval))
|
||||||
|
|
||||||
|
data = {}
|
||||||
|
if os.environ.get('BACKTEST_LIVE'):
|
||||||
|
print('Downloading data for all pairs in whitelist ...')
|
||||||
|
for pair in config['exchange']['pair_whitelist']:
|
||||||
|
data[pair] = exchange.get_ticker_history(pair, ticker_interval)
|
||||||
|
else:
|
||||||
|
print('Using local backtesting data (ignoring whitelist in given config)...')
|
||||||
|
data = load_backtesting_data(ticker_interval)
|
||||||
|
|
||||||
|
print('Using stake_currency: {} ...\nUsing stake_amount: {} ...'.format(
|
||||||
|
config['stake_currency'], config['stake_amount']
|
||||||
|
))
|
||||||
|
|
||||||
|
# Print timeframe
|
||||||
|
min_date, max_date = get_timeframe(data)
|
||||||
|
print('Measuring data from {} up to {} ...'.format(
|
||||||
|
min_date.isoformat(), max_date.isoformat()
|
||||||
|
))
|
||||||
|
|
||||||
|
# Execute backtest and print results
|
||||||
|
results = backtest(config, preprocess(data), mocker)
|
||||||
|
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']))
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -33,4 +33,3 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
|||||||
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(RuntimeError, match=r'not compatible'):
|
||||||
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
validate_pairs(default_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,W0212
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
@ -9,16 +9,22 @@ import pytest
|
|||||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
|
from freqtrade.tests import load_backtesting_data
|
||||||
from freqtrade.tests.test_backtesting import backtest, format_results
|
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||||
|
from freqtrade.tests.test_backtesting import preprocess
|
||||||
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
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 = 1300
|
TARGET_TRADES = 1100
|
||||||
TOTAL_TRIES = 4
|
TOTAL_TRIES = 4
|
||||||
|
# pylint: disable=C0103
|
||||||
current_tries = 0
|
current_tries = 0
|
||||||
|
|
||||||
|
|
||||||
def buy_strategy_generator(params):
|
def buy_strategy_generator(params):
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
@ -59,32 +65,36 @@ def buy_strategy_generator(params):
|
|||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
reduce(lambda x, y: x & y, conditions),
|
reduce(lambda x, y: x & y, conditions),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_hyperopt(backtest_conf, backdata, mocker):
|
def test_hyperopt(backtest_conf, mocker):
|
||||||
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
mocked_buy_trend = mocker.patch('freqtrade.tests.test_backtesting.populate_buy_trend')
|
||||||
|
|
||||||
|
backdata = load_backtesting_data()
|
||||||
|
processed = preprocess(backdata)
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
|
||||||
def optimizer(params):
|
def optimizer(params):
|
||||||
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
||||||
|
|
||||||
results = backtest(backtest_conf, backdata, mocker)
|
results = backtest(backtest_conf, processed, mocker)
|
||||||
|
|
||||||
result = format_results(results)
|
result = format_results(results)
|
||||||
|
|
||||||
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.4 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2)
|
||||||
profit_loss = max(0, 1 - total_profit / 15000) # max profit 15000
|
profit_loss = max(0, 1 - total_profit / 10000) # max profit 10000
|
||||||
|
|
||||||
|
# pylint: disable=W0603
|
||||||
global current_tries
|
global current_tries
|
||||||
current_tries += 1
|
current_tries += 1
|
||||||
print('{}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
print('{:5d}/{}: {}'.format(current_tries, TOTAL_TRIES, result))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'loss': trade_loss + profit_loss,
|
'loss': trade_loss + profit_loss,
|
||||||
@ -146,3 +156,8 @@ def test_hyperopt(backtest_conf, backdata, mocker):
|
|||||||
print('Best parameters {}'.format(best))
|
print('Best parameters {}'.format(best))
|
||||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||||
print('Result: {}'.format(newlist[0]['result']))
|
print('Result: {}'.format(newlist[0]['result']))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# for profiling with cProfile and line_profiler
|
||||||
|
pytest.main([__file__, '-s'])
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
import copy
|
import copy
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
@ -7,16 +7,17 @@ import requests
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
|
||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
from freqtrade.analyze import SignalType
|
||||||
|
from freqtrade.main import create_trade, handle_trade, init, \
|
||||||
get_target_bid, _process
|
get_target_bid, _process
|
||||||
from freqtrade.misc import get_state, State
|
from freqtrade.misc import get_state, State, FreqtradeException
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_creation(default_conf, ticker, health, 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.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: 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,
|
||||||
@ -25,7 +26,7 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
|||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
|
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
@ -44,8 +45,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
|
|||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, health, 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.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
@ -61,8 +62,8 @@ def test_process_exchange_failures(default_conf, ticker, health, mocker):
|
|||||||
def test_process_runtime_error(default_conf, ticker, health, 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.rpc', init=MagicMock(), send_msg=msg_mock)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: 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,
|
||||||
@ -79,8 +80,9 @@ def test_process_runtime_error(default_conf, ticker, health, mocker):
|
|||||||
|
|
||||||
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):
|
||||||
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.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal',
|
||||||
|
side_effect=lambda *args: False if args[1] == SignalType.SELL else 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,
|
||||||
@ -90,7 +92,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
|
|||||||
init(default_conf, create_engine('sqlite://'))
|
init(default_conf, create_engine('sqlite://'))
|
||||||
|
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
assert len(trades) == 0
|
assert not trades
|
||||||
result = _process()
|
result = _process()
|
||||||
assert result is True
|
assert result is True
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
@ -102,8 +104,8 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, health, m
|
|||||||
|
|
||||||
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
@ -132,27 +134,27 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
|
|||||||
|
|
||||||
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_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
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(ValueError, match=r'.*stake amount.*'):
|
with pytest.raises(FreqtradeException, match=r'.*stake amount.*'):
|
||||||
create_trade(default_conf['stake_amount'])
|
create_trade(default_conf['stake_amount'])
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
def test_create_trade_no_pairs(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_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value='mocked_limit_buy'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
|
|
||||||
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
|
with pytest.raises(FreqtradeException, 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)
|
||||||
@ -161,8 +163,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker):
|
|||||||
|
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -182,7 +184,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
|
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
assert trade.open_order_id == 'mocked_limit_sell'
|
assert trade.open_order_id == 'mocked_limit_sell'
|
||||||
assert close_trade_if_fulfilled(trade) is False
|
|
||||||
|
|
||||||
# Simulate fulfilled LIMIT_SELL order for trade
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
@ -194,8 +195,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
|
|||||||
|
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.rpc', init=MagicMock(), send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
@ -204,20 +205,15 @@ 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)
|
trade = create_trade(15.0)
|
||||||
trade.update(limit_buy_order)
|
|
||||||
trade.update(limit_sell_order)
|
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
trade.update(limit_buy_order)
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
# Simulate that there is no open order
|
trade.update(limit_sell_order)
|
||||||
trade.open_order_id = None
|
trade = Trade.query.filter(Trade.is_open.is_(False)).first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
closed = close_trade_if_fulfilled(trade)
|
|
||||||
assert closed
|
|
||||||
assert not trade.is_open
|
|
||||||
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
with pytest.raises(ValueError, match=r'.*closed trade.*'):
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
|
|
||||||
|
@ -1,7 +1,15 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring,C0103
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
from argparse import Namespace
|
||||||
|
from copy import deepcopy
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.misc import throttle
|
import pytest
|
||||||
|
from jsonschema import ValidationError
|
||||||
|
|
||||||
|
from freqtrade.misc import throttle, parse_args, start_backtesting, load_config
|
||||||
|
|
||||||
|
|
||||||
def test_throttle():
|
def test_throttle():
|
||||||
@ -18,3 +26,124 @@ def test_throttle():
|
|||||||
|
|
||||||
result = throttle(func, -1)
|
result = throttle(func, -1)
|
||||||
assert result == 42
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_defaults():
|
||||||
|
args = parse_args([])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == 'config.json'
|
||||||
|
assert args.dynamic_whitelist is False
|
||||||
|
assert args.loglevel == 20
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['-c'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_config():
|
||||||
|
args = parse_args(['-c', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
args = parse_args(['--config', '/dev/null'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.config == '/dev/null'
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_verbose():
|
||||||
|
args = parse_args(['-v'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.loglevel == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_dynamic_whitelist():
|
||||||
|
args = parse_args(['--dynamic-whitelist'])
|
||||||
|
assert args is not None
|
||||||
|
assert args.dynamic_whitelist is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['backtesting'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'config.json'
|
||||||
|
assert call_args.live is False
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_invalid():
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval'])
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit, match=r'2'):
|
||||||
|
parse_args(['--ticker-interval', 'abc'])
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_args_backtesting_custom(mocker):
|
||||||
|
backtesting_mock = mocker.patch('freqtrade.misc.start_backtesting', MagicMock())
|
||||||
|
args = parse_args(['-c', 'test_conf.json', 'backtesting', '--live', '--ticker-interval', '1'])
|
||||||
|
assert args is None
|
||||||
|
assert backtesting_mock.call_count == 1
|
||||||
|
|
||||||
|
call_args = backtesting_mock.call_args[0][0]
|
||||||
|
assert call_args.config == 'test_conf.json'
|
||||||
|
assert call_args.live is True
|
||||||
|
assert call_args.loglevel == 20
|
||||||
|
assert call_args.subparser == 'backtesting'
|
||||||
|
assert call_args.func is not None
|
||||||
|
assert call_args.ticker_interval == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_backtesting(mocker):
|
||||||
|
pytest_mock = mocker.patch('pytest.main', MagicMock())
|
||||||
|
env_mock = mocker.patch('os.environ', {})
|
||||||
|
args = Namespace(
|
||||||
|
config='config.json',
|
||||||
|
live=True,
|
||||||
|
loglevel=20,
|
||||||
|
ticker_interval=1,
|
||||||
|
)
|
||||||
|
start_backtesting(args)
|
||||||
|
assert env_mock == {
|
||||||
|
'BACKTEST': 'true',
|
||||||
|
'BACKTEST_LIVE': 'true',
|
||||||
|
'BACKTEST_CONFIG': 'config.json',
|
||||||
|
'BACKTEST_TICKER_INTERVAL': '1',
|
||||||
|
}
|
||||||
|
assert pytest_mock.call_count == 1
|
||||||
|
|
||||||
|
main_call_args = pytest_mock.call_args[0][0]
|
||||||
|
assert main_call_args[0] == '-s'
|
||||||
|
assert main_call_args[1].endswith(os.path.join('freqtrade', 'tests', 'test_backtesting.py'))
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config(default_conf, mocker):
|
||||||
|
file_mock = mocker.patch('freqtrade.misc.open', mocker.mock_open(
|
||||||
|
read_data=json.dumps(default_conf)
|
||||||
|
))
|
||||||
|
validated_conf = load_config('somefile')
|
||||||
|
assert file_mock.call_count == 1
|
||||||
|
assert validated_conf.items() >= default_conf.items()
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_invalid_pair(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['exchange']['pair_whitelist'].append('BTC-ETH')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*does not match.*'):
|
||||||
|
load_config('somefile')
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_missing_attributes(default_conf, mocker):
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf.pop('exchange')
|
||||||
|
mocker.patch('freqtrade.misc.open', mocker.mock_open(read_data=json.dumps(conf)))
|
||||||
|
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
|
||||||
|
load_config('somefile')
|
||||||
|
58
freqtrade/tests/test_rpc.py
Normal file
58
freqtrade/tests/test_rpc.py
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
|
from freqtrade.rpc import init, cleanup, send_msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_enabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
init(default_conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
assert 'telegram' in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_init_telegram_disabled(default_conf, mocker):
|
||||||
|
module_list = []
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', module_list)
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.init', MagicMock())
|
||||||
|
|
||||||
|
conf = deepcopy(default_conf)
|
||||||
|
conf['telegram']['enabled'] = False
|
||||||
|
init(conf)
|
||||||
|
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
assert 'telegram' not in module_list
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_cleanup_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.cleanup', MagicMock())
|
||||||
|
cleanup()
|
||||||
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_enabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', ['telegram'])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_telegram_disabled(mocker):
|
||||||
|
mocker.patch('freqtrade.rpc.REGISTERED_MODULES', [])
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.send_msg', MagicMock())
|
||||||
|
send_msg('test')
|
||||||
|
assert telegram_mock.call_count == 0
|
@ -1,10 +1,9 @@
|
|||||||
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
|
# pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from telegram import Update, Message, Chat
|
from telegram import Update, Message, Chat
|
||||||
from telegram.error import NetworkError
|
from telegram.error import NetworkError
|
||||||
@ -14,10 +13,8 @@ from freqtrade.main import init, create_trade
|
|||||||
from freqtrade.misc import update_state, State, get_state
|
from freqtrade.misc import update_state, State, get_state
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
from freqtrade.rpc import telegram
|
||||||
from freqtrade.rpc.telegram import (
|
from freqtrade.rpc.telegram import authorized_only, is_enabled, send_msg, _status, _status_table, \
|
||||||
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
|
_profit, _forcesell, _performance, _count, _start, _stop, _balance, _version, _help
|
||||||
authorized_only, _help, is_enabled, send_msg,
|
|
||||||
_version)
|
|
||||||
|
|
||||||
|
|
||||||
def test_is_enabled(default_conf, mocker):
|
def test_is_enabled(default_conf, mocker):
|
||||||
@ -79,9 +76,10 @@ def test_authorized_only_exception(default_conf, mocker):
|
|||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, mocker):
|
def test_status_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -111,16 +109,17 @@ def test_status_handle(default_conf, update, ticker, mocker):
|
|||||||
# 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)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_status_table_handle(default_conf, update, ticker, mocker):
|
def test_status_table_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -155,14 +154,15 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
assert int(fields[0]) == 1
|
assert int(fields[0]) == 1
|
||||||
assert fields[1] == 'BTC_ETH'
|
assert fields[1] == 'BTC_ETH'
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -184,7 +184,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
_profit(bot=MagicMock(), update=update)
|
_profit(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
@ -204,12 +204,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
|
|||||||
|
|
||||||
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
@ -225,19 +225,19 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
|
|||||||
update.message.text = '/forcesell 1'
|
update.message.text = '/forcesell 1'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=MagicMock())
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker)
|
get_ticker=ticker)
|
||||||
@ -247,22 +247,21 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
|
|||||||
for _ in range(4):
|
for _ in range(4):
|
||||||
Trade.session.add(create_trade(15.0))
|
Trade.session.add(create_trade(15.0))
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
rpc_mock.reset_mock()
|
||||||
msg_mock.reset_mock()
|
|
||||||
|
|
||||||
update.message.text = '/forcesell all'
|
update.message.text = '/forcesell all'
|
||||||
_forcesell(bot=MagicMock(), update=update)
|
_forcesell(bot=MagicMock(), update=update)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert rpc_mock.call_count == 4
|
||||||
for args in msg_mock.call_args_list:
|
for args in rpc_mock.call_args_list:
|
||||||
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
assert '0.07256061 (profit: ~-0.64%)' in args[0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
def test_forcesell_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -297,9 +296,10 @@ def test_forcesell_handle_invalid(default_conf, update, mocker):
|
|||||||
def test_performance_handle(
|
def test_performance_handle(
|
||||||
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
|
||||||
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -324,17 +324,17 @@ def test_performance_handle(
|
|||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
_performance(bot=MagicMock(), update=update)
|
_performance(bot=MagicMock(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 1
|
||||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
assert 'Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, mocker):
|
def test_count_handle(default_conf, update, ticker, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.main.telegram',
|
'freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -365,9 +365,9 @@ def test_count_handle(default_conf, update, ticker, mocker):
|
|||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker):
|
def test_performance_handle_invalid(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -385,7 +385,7 @@ def test_performance_handle_invalid(default_conf, update, mocker):
|
|||||||
def test_start_handle(default_conf, update, mocker):
|
def test_start_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -403,7 +403,7 @@ def test_start_handle(default_conf, update, mocker):
|
|||||||
def test_start_handle_already_running(default_conf, update, mocker):
|
def test_start_handle_already_running(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -422,7 +422,7 @@ def test_start_handle_already_running(default_conf, update, mocker):
|
|||||||
def test_stop_handle(default_conf, update, mocker):
|
def test_stop_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -441,7 +441,7 @@ def test_stop_handle(default_conf, update, mocker):
|
|||||||
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
def test_stop_handle_already_stopped(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -473,7 +473,7 @@ def test_balance_handle(default_conf, update, mocker):
|
|||||||
}]
|
}]
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -489,7 +489,7 @@ def test_balance_handle(default_conf, update, mocker):
|
|||||||
def test_help_handle(default_conf, update, mocker):
|
def test_help_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -502,7 +502,7 @@ def test_help_handle(default_conf, update, mocker):
|
|||||||
def test_version_handle(default_conf, update, mocker):
|
def test_version_handle(default_conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock(),
|
init=MagicMock(),
|
||||||
send_msg=msg_mock)
|
send_msg=msg_mock)
|
||||||
@ -514,12 +514,12 @@ def test_version_handle(default_conf, update, mocker):
|
|||||||
|
|
||||||
def test_send_msg(default_conf, mocker):
|
def test_send_msg(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
send_msg('test', bot)
|
send_msg('test', bot)
|
||||||
assert len(bot.method_calls) == 0
|
assert not bot.method_calls
|
||||||
bot.reset_mock()
|
bot.reset_mock()
|
||||||
|
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
@ -529,14 +529,13 @@ def test_send_msg(default_conf, mocker):
|
|||||||
|
|
||||||
def test_send_msg_network_error(default_conf, mocker):
|
def test_send_msg_network_error(default_conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram',
|
mocker.patch.multiple('freqtrade.rpc.telegram',
|
||||||
_CONF=default_conf,
|
_CONF=default_conf,
|
||||||
init=MagicMock())
|
init=MagicMock())
|
||||||
default_conf['telegram']['enabled'] = True
|
default_conf['telegram']['enabled'] = True
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
|
||||||
with pytest.raises(NetworkError, match=r'Oh snap'):
|
send_msg('test', bot)
|
||||||
send_msg('test', bot)
|
|
||||||
|
|
||||||
# Bot should've tried to send it twice
|
# Bot should've tried to send it twice
|
||||||
assert len(bot.method_calls) == 2
|
assert len(bot.method_calls) == 2
|
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_BCC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_DASH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_ETH-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_LSK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_OK-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_POWR-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_VTC-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_WAVES-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-1.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
1
freqtrade/tests/testdata/BTC_XLM-5.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-edg.json
vendored
1
freqtrade/tests/testdata/btc-edg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-etc.json
vendored
1
freqtrade/tests/testdata/btc-etc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-eth.json
vendored
1
freqtrade/tests/testdata/btc-eth.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-ltc.json
vendored
1
freqtrade/tests/testdata/btc-ltc.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-mtl.json
vendored
1
freqtrade/tests/testdata/btc-mtl.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-neo.json
vendored
1
freqtrade/tests/testdata/btc-neo.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-omg.json
vendored
1
freqtrade/tests/testdata/btc-omg.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pay.json
vendored
1
freqtrade/tests/testdata/btc-pay.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-pivx.json
vendored
1
freqtrade/tests/testdata/btc-pivx.json
vendored
File diff suppressed because one or more lines are too long
1
freqtrade/tests/testdata/btc-qtum.json
vendored
1
freqtrade/tests/testdata/btc-qtum.json
vendored
File diff suppressed because one or more lines are too long
@ -7,8 +7,13 @@ from os import path
|
|||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex
|
from freqtrade.exchange import Bittrex
|
||||||
|
|
||||||
PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
|
PAIRS = [
|
||||||
TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
|
'BTC_BCC', 'BTC_ETH', 'BTC_MER', 'BTC_POWR', 'BTC_ETC',
|
||||||
|
'BTC_OK', 'BTC_NEO', 'BTC_EMC2', 'BTC_DASH', 'BTC_LSK',
|
||||||
|
'BTC_LTC', 'BTC_XZC', 'BTC_OMG', 'BTC_STRAT', 'BTC_XRP',
|
||||||
|
'BTC_QTUM', 'BTC_WAVES', 'BTC_VTC', 'BTC_XLM', 'BTC_MCO'
|
||||||
|
]
|
||||||
|
TICKER_INTERVAL = 5 # ticker interval in minutes (currently implemented: 1 and 5)
|
||||||
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
OUTPUT_DIR = path.dirname(path.realpath(__file__))
|
||||||
|
|
||||||
# Init Bittrex exchange
|
# Init Bittrex exchange
|
||||||
@ -16,8 +21,8 @@ exchange._API = Bittrex({'key': '', 'secret': ''})
|
|||||||
|
|
||||||
for pair in PAIRS:
|
for pair in PAIRS:
|
||||||
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
|
||||||
filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
|
filename = path.join(OUTPUT_DIR, '{}-{}.json'.format(
|
||||||
pair.lower(),
|
pair,
|
||||||
TICKER_INTERVAL,
|
TICKER_INTERVAL,
|
||||||
))
|
))
|
||||||
with open(filename, 'w') as fp:
|
with open(filename, 'w') as fp:
|
||||||
|
Loading…
Reference in New Issue
Block a user