Merge branch 'release/0.14.0'

This commit is contained in:
gcarq 2017-11-09 20:56:07 +01:00
commit 349a91bd92
34 changed files with 1330 additions and 633 deletions

View File

@ -1,2 +1,4 @@
[run] [run]
omit = freqtrade/tests/* omit =
freqtrade/tests/*
freqtrade/vendor/*

View File

@ -1,2 +1,3 @@
[BASIC] [BASIC]
good-names=logger good-names=logger
ignore=vendor

View File

@ -4,6 +4,9 @@ os:
language: python language: python
python: python:
- 3.6 - 3.6
env:
- BACKTEST=
- BACKTEST=true
addons: addons:
apt: apt:
packages: packages:

View File

@ -19,7 +19,8 @@ Persistence is achieved through sqlite.
### Telegram RPC commands: ### Telegram RPC commands:
* /start: Starts the trader * /start: Starts the trader
* /stop: Stops the trader * /stop: Stops the trader
* /status: Lists all open trades * /status [table]: Lists all open trades
* /count: Displays number of open trades
* /profit: Lists cumulative profit from all finished trades * /profit: Lists cumulative profit from all finished trades
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`). * /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
* /performance: Show performance of each finished trade grouped by pair * /performance: Show performance of each finished trade grouped by pair

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
from freqtrade.main import main from freqtrade.main import main
main() main()

View File

@ -1,3 +1,3 @@
__version__ = '0.13.0' __version__ = '0.14.0'
from . import main from . import main

View File

@ -17,16 +17,17 @@ logger = logging.getLogger(__name__)
def parse_ticker_dataframe(ticker: list) -> DataFrame: def parse_ticker_dataframe(ticker: list) -> DataFrame:
""" """
Analyses the trend for the given pair Analyses the trend for the given ticker history
:param pair: pair as str in format BTC_ETH or BTC-ETH :param ticker: See exchange.get_ticker_history
:return: DataFrame :return: DataFrame
""" """
df = DataFrame(ticker) \ columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'}
frame = DataFrame(ticker) \
.drop('BV', 1) \ .drop('BV', 1) \
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) .rename(columns=columns)
df['date'] = to_datetime(df['date'], utc=True, infer_datetime_format=True) frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True)
df.sort_values('date', inplace=True) frame.sort_values('date', inplace=True)
return df return frame
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame) -> DataFrame:
@ -81,9 +82,8 @@ def analyze_ticker(pair: str) -> DataFrame:
add several TA indicators and buy signal to it add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data :return DataFrame with ticker data and indicator data
""" """
minimum_date = arrow.utcnow().shift(hours=-24) data = get_ticker_history(pair)
data = get_ticker_history(pair, minimum_date) dataframe = parse_ticker_dataframe(data)
dataframe = parse_ticker_dataframe(data['result'])
if dataframe.empty: if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair) logger.warning('Empty dataframe for pair %s', pair)
@ -117,19 +117,20 @@ def get_buy_signal(pair: str) -> bool:
return signal return signal
def plot_dataframe(dataframe: DataFrame, pair: str) -> None: def plot_analyzed_dataframe(pair: str) -> None:
""" """
Plots the given dataframe Calls analyze() and plots the returned dataframe
:param dataframe: DataFrame
:param pair: pair as str :param pair: pair as str
:return: None :return: None
""" """
import matplotlib import matplotlib
matplotlib.use("Qt5Agg") matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# Init Bittrex to use public API
exchange._API = Bittrex({'key': '', 'secret': ''})
dataframe = analyze_ticker(pair)
# Two subplots sharing x axis # Two subplots sharing x axis
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold') fig.suptitle(pair, fontsize=14, fontweight='bold')
@ -161,9 +162,6 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
if __name__ == '__main__': if __name__ == '__main__':
# Install PYQT5==5.9 manually if you want to test this helper function # Install PYQT5==5.9 manually if you want to test this helper function
while True: while True:
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''}) for p in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
test_pair = 'BTC_ETH' plot_analyzed_dataframe(p)
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)
plot_dataframe(analyze_ticker(test_pair), test_pair)
time.sleep(60) time.sleep(60)

View File

@ -1,6 +1,7 @@
import enum import enum
import logging import logging
from typing import List from random import randint
from typing import List, Dict, Any, Optional
import arrow import arrow
@ -10,9 +11,12 @@ from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Current selected exchange # Current selected exchange
EXCHANGE: Exchange = None _API: Exchange = None
_CONF: dict = {} _CONF: dict = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
class Exchanges(enum.Enum): class Exchanges(enum.Enum):
""" """
@ -29,7 +33,7 @@ def init(config: dict) -> None:
:param config: config to use :param config: config to use
:return: None :return: None
""" """
global _CONF, EXCHANGE global _CONF, _API
_CONF.update(config) _CONF.update(config)
@ -45,7 +49,7 @@ def init(config: dict) -> None:
except KeyError: except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name)) raise RuntimeError('Exchange {} is not supported'.format(name))
EXCHANGE = exchange_class(exchange_config) _API = exchange_class(exchange_config)
# Check if all pairs are available # Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist']) validate_pairs(config['exchange']['pair_whitelist'])
@ -58,62 +62,103 @@ def validate_pairs(pairs: List[str]) -> None:
:param pairs: list of pairs :param pairs: list of pairs
:return: None :return: None
""" """
markets = EXCHANGE.get_markets() markets = _API.get_markets()
for pair in pairs: for pair in pairs:
if pair not in markets: if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower())) raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
def buy(pair: str, rate: float, amount: float) -> str: def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']: if _CONF['dry_run']:
return 'dry_run' global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 1e6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_BUY',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
return EXCHANGE.buy(pair, rate, amount) return _API.buy(pair, rate, amount)
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']:
return 'dry_run' global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 1e6))
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'rate': rate,
'amount': amount,
'type': 'LIMIT_SELL',
'remaining': 0.0,
'opened': arrow.utcnow().datetime,
'closed': arrow.utcnow().datetime,
}
return order_id
return EXCHANGE.sell(pair, rate, amount) return _API.sell(pair, rate, amount)
def get_balance(currency: str) -> float: def get_balance(currency: str) -> float:
if _CONF['dry_run']: if _CONF['dry_run']:
return 999.9 return 999.9
return EXCHANGE.get_balance(currency) return _API.get_balance(currency)
def get_balances(): def get_balances():
return EXCHANGE.get_balances() if _CONF['dry_run']:
return []
return _API.get_balances()
def get_ticker(pair: str) -> dict: def get_ticker(pair: str) -> dict:
return EXCHANGE.get_ticker(pair) return _API.get_ticker(pair)
def get_ticker_history(pair: str, minimum_date: arrow.Arrow): def get_ticker_history(pair: str, tick_interval: Optional[int] = 5) -> List:
return EXCHANGE.get_ticker_history(pair, minimum_date) return _API.get_ticker_history(pair, tick_interval)
def cancel_order(order_id: str) -> None: def cancel_order(order_id: str) -> None:
if _CONF['dry_run']: if _CONF['dry_run']:
return return
return EXCHANGE.cancel_order(order_id) return _API.cancel_order(order_id)
def get_open_orders(pair: str) -> List[dict]: def get_order(order_id: str) -> Dict:
if _CONF['dry_run']: if _CONF['dry_run']:
return [] order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({
'id': order_id
})
return order
return EXCHANGE.get_open_orders(pair) return _API.get_order(order_id)
def get_pair_detail_url(pair: str) -> str: def get_pair_detail_url(pair: str) -> str:
return EXCHANGE.get_pair_detail_url(pair) return _API.get_pair_detail_url(pair)
def get_markets() -> List[str]: def get_markets() -> List[str]:
return EXCHANGE.get_markets() return _API.get_markets()
def get_name() -> str:
return _API.name
def get_sleep_time() -> float:
return _API.sleep_time
def get_fee() -> float:
return _API.fee

View File

@ -1,7 +1,6 @@
import logging import logging
from typing import List, Optional from typing import List, Dict
import arrow
import requests import requests
from bittrex.bittrex import Bittrex as _Bittrex from bittrex.bittrex import Bittrex as _Bittrex
@ -21,89 +20,116 @@ class Bittrex(Exchange):
BASE_URL: str = 'https://www.bittrex.com' BASE_URL: str = 'https://www.bittrex.com'
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks' TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
# Ticker inveral
TICKER_INTERVAL: str = 'fiveMin'
# Sleep time to avoid rate limits, used in the main loop
SLEEP_TIME: float = 25
@property @property
def sleep_time(self) -> float: def sleep_time(self) -> float:
return self.SLEEP_TIME """ Sleep time to avoid rate limits, used in the main loop """
return 25
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
global _API, _EXCHANGE_CONF global _API, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config) _EXCHANGE_CONF.update(config)
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) _API = _Bittrex(
api_key=_EXCHANGE_CONF['key'],
api_secret=_EXCHANGE_CONF['secret'],
calls_per_second=5,
)
@property
def fee(self) -> float:
# See https://bittrex.com/fees
return 0.0025
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate) data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid'] return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str: def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate) data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({pair}, {rate}, {amount})'.format(
message=data['message'],
pair=pair,
rate=rate,
amount=amount))
return data['result']['uuid'] return data['result']['uuid']
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency) data = _API.get_balance(currency)
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({currency})'.format(
message=data['message'],
currency=currency))
return float(data['result']['Balance'] or 0.0) return float(data['result']['Balance'] or 0.0)
def get_balances(self): def get_balances(self):
data = _API.get_balances() data = _API.get_balances()
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message}'.format(message=data['message']))
return data['result'] return data['result']
def get_ticker(self, pair: str) -> dict: def get_ticker(self, pair: str) -> dict:
data = _API.get_ticker(pair.replace('_', '-')) data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({pair})'.format(
message=data['message'],
pair=pair))
return { return {
'bid': float(data['result']['Bid']), 'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']), 'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']), 'last': float(data['result']['Last']),
} }
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None): def get_ticker_history(self, pair: str, tick_interval: int):
url = self.TICKER_METHOD if tick_interval == 1:
headers = { interval = 'oneMin'
# TODO: Set as global setting elif tick_interval == 5:
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36' interval = 'fiveMin'
} else:
params = { raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
data = requests.get(self.TICKER_METHOD, params={
'marketName': pair.replace('_', '-'), 'marketName': pair.replace('_', '-'),
'tickInterval': self.TICKER_INTERVAL, 'tickInterval': interval,
# TODO: Timestamp has no effect on API response }).json()
'_': minimum_date.timestamp * 1000
}
data = requests.get(url, params=params, headers=headers).json()
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({pair})'.format(
return data message=data['message'],
pair=pair))
return data['result']
def get_order(self, order_id: str) -> Dict:
data = _API.get_order(order_id)
if not data['success']:
raise RuntimeError('{message} params=({order_id})'.format(
message=data['message'],
order_id=order_id))
data = data['result']
return {
'id': data['OrderUuid'],
'type': data['Type'],
'pair': data['Exchange'].replace('-', '_'),
'opened': data['Opened'],
'rate': data['PricePerUnit'],
'amount': data['Quantity'],
'remaining': data['QuantityRemaining'],
'closed': data['Closed'],
}
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id) data = _API.cancel(order_id)
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message} params=({order_id})'.format(
message=data['message'],
def get_open_orders(self, pair: str) -> List[dict]: order_id=order_id))
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return [{
'id': entry['OrderUuid'],
'type': entry['OrderType'],
'opened': entry['Opened'],
'rate': entry['PricePerUnit'],
'amount': entry['Quantity'],
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(self, pair: str) -> str: def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
@ -111,5 +137,5 @@ class Bittrex(Exchange):
def get_markets(self) -> List[str]: def get_markets(self) -> List[str]:
data = _API.get_markets() data = _API.get_markets()
if not data['success']: if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) raise RuntimeError('{message}'.format(message=data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']] return [m['MarketName'].replace('-', '_') for m in data['result']]

View File

@ -1,7 +1,5 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Optional from typing import List, Dict
import arrow
class Exchange(ABC): class Exchange(ABC):
@ -13,6 +11,13 @@ class Exchange(ABC):
""" """
return self.__class__.__name__ return self.__class__.__name__
@property
def fee(self) -> float:
"""
Fee for placing an order
:return: percentage in float
"""
@property @property
@abstractmethod @abstractmethod
def sleep_time(self) -> float: def sleep_time(self) -> float:
@ -77,26 +82,38 @@ class Exchange(ABC):
""" """
@abstractmethod @abstractmethod
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict: def get_ticker_history(self, pair: str, tick_interval: int) -> List:
""" """
Gets ticker history for given pair. Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC :param pair: Pair as str, format: BTC_ETC
:param minimum_date: Minimum date (optional) :param tick_interval: ticker interval in minutes
:return: list, format: [
{
'O': float, (Open)
'H': float, (High)
'L': float, (Low)
'C': float, (Close)
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
"""
def get_order(self, order_id: str) -> Dict:
"""
Get order details for the given order_id.
:param order_id: ID as str
:return: dict, format: { :return: dict, format: {
'success': bool, 'id': str,
'message': str, 'type': str,
'result': [ 'pair': str,
{ 'opened': str ISO 8601 datetime,
'O': float, (Open) 'closed': str ISO 8601 datetime,
'H': float, (High) 'rate': float,
'L': float, (Low) 'amount': float,
'C': float, (Close) 'remaining': int
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
} }
""" """
@ -108,24 +125,6 @@ class Exchange(ABC):
:return: None :return: None
""" """
@abstractmethod
def get_open_orders(self, pair: str) -> List[dict]:
"""
Gets all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: List of dicts, format: [
{
'id': str,
'type': str,
'opened': datetime,
'rate': float,
'amount': float,
'remaining': int,
},
...
]
"""
@abstractmethod @abstractmethod
def get_pair_detail_url(self, pair: str) -> str: def get_pair_detail_url(self, pair: str) -> str:
""" """

View File

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python3
import copy import copy
import json import json
import logging import logging
@ -8,6 +8,7 @@ from datetime import datetime
from typing import Dict, Optional from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM from signal import signal, SIGINT, SIGABRT, SIGTERM
import requests
from jsonschema import validate from jsonschema import validate
from freqtrade import __version__, exchange, persistence from freqtrade import __version__, exchange, persistence
@ -23,12 +24,13 @@ logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
def _process() -> None: def _process() -> bool:
""" """
Queries the persistence layer for open trades and handles them, Queries the persistence layer for open trades and handles them,
otherwise a new trade is created. otherwise a new trade is created.
:return: None :return: True if a trade has been created or closed, False otherwise
""" """
state_changed = False
try: try:
# Query trades from persistence layer # Query trades from persistence layer
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
@ -38,28 +40,36 @@ def _process() -> None:
trade = create_trade(float(_CONF['stake_amount'])) trade = create_trade(float(_CONF['stake_amount']))
if trade: if trade:
Trade.session.add(trade) Trade.session.add(trade)
state_changed = True
else: else:
logging.info('Got no buy signal...') logging.info('Got no buy signal...')
except ValueError: except ValueError:
logger.exception('Unable to create trade') logger.exception('Unable to create trade')
for trade in trades: for trade in trades:
# Check if there is already an open order for this trade # Get order details for actual price per unit
orders = exchange.get_open_orders(trade.pair) if trade.open_order_id:
orders = [o for o in orders if o['id'] == trade.open_order_id] # Update trade with order values
if orders: logger.info('Got open order for %s', trade)
logger.info('There is an open order for: %s', orders[0]) trade.update(exchange.get_order(trade.open_order_id))
else:
# Update state if not close_trade_if_fulfilled(trade):
trade.open_order_id = None # Check if we can sell our current pair
# Check if this trade can be closed state_changed = handle_trade(trade) or state_changed
if not close_trade_if_fulfilled(trade):
# Check if we can sell our current pair Trade.session.flush()
handle_trade(trade) except (requests.exceptions.RequestException, json.JSONDecodeError) as error:
Trade.session.flush() msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
except (ConnectionError, json.JSONDecodeError) as error:
msg = 'Got {} in _process()'.format(error.__class__.__name__)
logger.exception(msg) logger.exception(msg)
time.sleep(30)
except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError:\n```\n{traceback}```{hint}'.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
))
logger.exception('Got RuntimeError. Stopping trader ...')
update_state(State.STOPPED)
return state_changed
def close_trade_if_fulfilled(trade: Trade) -> bool: def close_trade_if_fulfilled(trade: Trade) -> bool:
@ -80,23 +90,24 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
return False return False
def execute_sell(trade: Trade, current_rate: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
""" """
Executes a sell for the given trade and current rate Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param current_rate: current rate :param limit: limit rate for the sell order
:return: None :return: None
""" """
# Get available balance # Execute sell and update trade record
currency = trade.pair.split('_')[1] order_id = exchange.sell(str(trade.pair), limit, trade.amount)
balance = exchange.get_balance(currency) trade.open_order_id = order_id
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
message = '*{}:* 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),
trade.close_rate, limit,
round(profit, 2) fmt_exp_profit
) )
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
@ -107,41 +118,35 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
Based an earlier trade and current price and configuration, decides whether bot should sell Based an earlier trade and current price and 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 = (current_rate - trade.open_rate) / trade.open_rate current_profit = trade.calc_profit(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
for duration, threshold in sorted(_CONF['minimal_roi'].items()): for duration, threshold in sorted(_CONF['minimal_roi'].items()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60 time_diff = (current_time - trade.open_date).total_seconds() / 60
if time_diff > duration and current_profit > threshold: if time_diff > float(duration) and current_profit > threshold:
return True return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0) logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
return False return False
def handle_trade(trade: Trade) -> None: def handle_trade(trade: Trade) -> bool:
""" """
Sells the current pair if the threshold is reached and updates the trade record. Sells the current pair if the threshold is reached and updates the trade record.
:return: None :return: True if trade has been sold, False otherwise
""" """
try: if not trade.is_open:
if not trade.is_open: raise ValueError('attempt to handle closed trade: {}'.format(trade))
raise ValueError('attempt to handle closed trade: {}'.format(trade))
logger.debug('Handling open trade %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 should_sell(trade, current_rate, datetime.utcnow()): execute_sell(trade, current_rate)
execute_sell(trade, current_rate) return True
return return False
except ValueError:
logger.exception('Unable to handle open order')
def get_target_bid(ticker: Dict[str, float]) -> float: def get_target_bid(ticker: Dict[str, float]) -> float:
@ -163,7 +168,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
# 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 ValueError(
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency']) 'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
) )
# Remove currently opened and latest pairs from whitelist # Remove currently opened and latest pairs from whitelist
@ -182,27 +187,30 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
else: else:
return None return None
open_rate = get_target_bid(exchange.get_ticker(pair)) # Calculate amount and subtract fee
amount = stake_amount / open_rate fee = exchange.get_fee()
order_id = exchange.buy(pair, open_rate, amount) buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = (1 - fee) * stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return # Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( message = '*{}:* Buying [{}]({}) with limit `{:.8f}`'.format(
exchange.EXCHANGE.name.upper(), exchange.get_name().upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
open_rate buy_limit
) )
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
# 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,
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount, amount=amount,
exchange=exchange.EXCHANGE.name.upper(), fee=fee * 2,
open_order_id=order_id, open_rate=buy_limit,
is_open=True) open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id)
def init(config: dict, db_url: Optional[str] = None) -> None: def init(config: dict, db_url: Optional[str] = None) -> None:
@ -242,49 +250,38 @@ def cleanup(*args, **kwargs) -> None:
exit(0) exit(0)
def app(config: dict) -> None: def main():
""" """
Main loop which handles the application state Loads and validates the config and handles the main loop
:param config: config as dict
:return: None :return: None
""" """
logger.info('Starting freqtrade %s', __version__) logger.info('Starting freqtrade %s', __version__)
init(config)
try:
old_state = get_state()
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logging.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(exchange.EXCHANGE.sleep_time)
old_state = new_state
except RuntimeError:
telegram.send_msg(
'*Status:* Got RuntimeError:\n```\n{}\n```'.format(traceback.format_exc())
)
logger.exception('RuntimeError. Trader stopped!')
def main():
"""
Loads and validates the config and starts the main loop
:return: None
"""
global _CONF global _CONF
with open('config.json') as file: with open('config.json') as file:
_CONF = json.load(file) _CONF = json.load(file)
validate(_CONF, CONF_SCHEMA)
app(_CONF) logger.info('Validating configuration ...')
validate(_CONF, CONF_SCHEMA)
init(_CONF)
old_state = get_state()
logger.info('Initial State: %s', old_state)
telegram.send_msg('*Status:* `{}`'.format(old_state.name.lower()))
while True:
new_state = get_state()
# Log state transition
if new_state != old_state:
telegram.send_msg('*Status:* `{}`'.format(new_state.name.lower()))
logging.info('Changing state to: %s', new_state.name)
if new_state == State.STOPPED:
time.sleep(1)
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(exchange.get_sleep_time())
old_state = new_state
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -1,16 +1,20 @@
import logging
from datetime import datetime from datetime import datetime
from typing import Optional from decimal import Decimal, getcontext
from typing import Optional, Dict
import arrow
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from freqtrade import exchange logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
_DECL_BASE = declarative_base()
Base = declarative_base()
def init(config: dict, db_url: Optional[str] = None) -> None: def init(config: dict, db_url: Optional[str] = None) -> None:
@ -25,15 +29,15 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
_CONF.update(config) _CONF.update(config)
if not db_url: if not db_url:
if _CONF.get('dry_run', False): if _CONF.get('dry_run', False):
db_url = 'sqlite:///tradesv2.dry_run.sqlite' db_url = 'sqlite://'
else: else:
db_url = 'sqlite:///tradesv2.sqlite' db_url = 'sqlite:///tradesv3.sqlite'
engine = create_engine(db_url, echo=False) engine = create_engine(db_url, echo=False)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()
Trade.query = session.query_property() Trade.query = session.query_property()
Base.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
def cleanup() -> None: def cleanup() -> None:
@ -44,51 +48,63 @@ def cleanup() -> None:
Trade.session.flush() Trade.session.flush()
class Trade(Base): class Trade(_DECL_BASE):
__tablename__ = 'trades' __tablename__ = 'trades'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False) fee = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float)
close_rate = Column(Float) close_rate = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
stake_amount = Column(Float, name='btc_amount', nullable=False) stake_amount = Column(Float, nullable=False)
amount = Column(Float, nullable=False) amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)
open_order_id = Column(String) open_order_id = Column(String)
def __repr__(self): def __repr__(self):
if self.is_open:
open_since = 'closed'
else:
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format( return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
self.id, self.id,
self.pair, self.pair,
self.amount, self.amount,
self.open_rate, self.open_rate,
open_since arrow.get(self.open_date).humanize() if self.is_open else 'closed'
) )
def exec_sell_order(self, rate: float, amount: float) -> float: def update(self, order: Dict) -> None:
""" """
Executes a sell for the given trade and updated the entity. Updates this entity with amount and actual open/close rates.
:param rate: rate to sell for :param order: order retrieved by exchange.get_order()
:param amount: amount to sell :return: None
:return: current profit as percentage
""" """
profit = 100 * ((rate - self.open_rate) / self.open_rate) if not order['closed']:
return
# Execute sell and update trade record logger.debug('Updating trade (id=%d) ...', self.id)
order_id = exchange.sell(str(self.pair), rate, amount) if order['type'] == 'LIMIT_BUY':
self.close_rate = rate # Update open rate and actual amount
self.close_profit = profit self.open_rate = order['rate']
self.close_date = datetime.utcnow() self.amount = order['amount']
self.open_order_id = order_id elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
self.close_date = datetime.utcnow()
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
# Flush changes self.open_order_id = None
Trade.session.flush()
return profit def calc_profit(self, rate: Optional[float] = None) -> float:
"""
Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in percentage as float
"""
getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee))

View File

@ -1,6 +1,9 @@
import logging import logging
import re
from datetime import timedelta from datetime import timedelta
from typing import Callable, Any from typing import Callable, Any
from pandas import DataFrame
from tabulate import tabulate
import arrow import arrow
from sqlalchemy import and_, func, text from sqlalchemy import and_, func, text
@ -17,7 +20,7 @@ logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO) logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_updater: Updater = None _UPDATER: Updater = None
_CONF = {} _CONF = {}
@ -29,13 +32,13 @@ def init(config: dict) -> None:
:param config: config to use :param config: config to use
:return: None :return: None
""" """
global _updater global _UPDATER
_CONF.update(config) _CONF.update(config)
if not _CONF['telegram']['enabled']: if not is_enabled():
return return
_updater = Updater(token=config['telegram']['token'], workers=0) _UPDATER = Updater(token=config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling # Register command handler and start telegram message polling
handles = [ handles = [
@ -46,11 +49,12 @@ def init(config: dict) -> None:
CommandHandler('stop', _stop), CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell), CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance), CommandHandler('performance', _performance),
CommandHandler('count', _count),
CommandHandler('help', _help), CommandHandler('help', _help),
] ]
for handle in handles: for handle in handles:
_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=3,
timeout=30, timeout=30,
@ -67,7 +71,16 @@ def cleanup() -> None:
Stops all running telegram threads. Stops all running telegram threads.
:return: None :return: None
""" """
_updater.stop() if not is_enabled():
return
_UPDATER.stop()
def is_enabled() -> bool:
"""
Returns True if the telegram module is activated, False otherwise
"""
return bool(_CONF['telegram'].get('enabled', False))
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
@ -79,15 +92,17 @@ def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1] bot, update = kwargs.get('bot') or args[0], kwargs.get('update') or args[1]
if not isinstance(bot, Bot) or not isinstance(update, Update): # Reject unauthorized messages
raise ValueError('Received invalid Arguments: {}'.format(*args))
chat_id = int(_CONF['telegram']['chat_id']) chat_id = int(_CONF['telegram']['chat_id'])
if int(update.message.chat_id) == chat_id: if int(update.message.chat_id) != chat_id:
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
return command_handler(*args, **kwargs)
else:
logger.info('Rejected unauthorized message from: %s', update.message.chat_id) logger.info('Rejected unauthorized message from: %s', update.message.chat_id)
return wrapper
logger.info('Executing handler: %s for chat_id: %s', command_handler.__name__, chat_id)
try:
return command_handler(*args, **kwargs)
except BaseException:
logger.exception('Exception occurred within Telegram module')
return wrapper return wrapper
@ -100,32 +115,39 @@ def _status(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
# Check if additional parameters are passed
params = update.message.text.replace('/status', '').split(' ') \
if update.message.text else []
if 'table' in params:
_status_table(bot, update)
return
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades: elif not trades:
send_msg('*Status:* `no active order`', bot=bot) send_msg('*Status:* `no active trade`', bot=bot)
else: else:
for trade in trades: for trade in trades:
order = None
if trade.open_order_id:
order = exchange.get_order(trade.open_order_id)
# calculate profit and send message to user # calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) current_profit = trade.calc_profit(current_rate)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
order = orders[0] if orders else None
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) if trade.close_profit else None
message = """ message = """
*Trade ID:* `{trade_id}` *Trade ID:* `{trade_id}`
*Current Pair:* [{pair}]({market_url}) *Current Pair:* [{pair}]({market_url})
*Open Since:* `{date}` *Open Since:* `{date}`
*Amount:* `{amount}` *Amount:* `{amount}`
*Open Rate:* `{open_rate}` *Open Rate:* `{open_rate:.8f}`
*Close Rate:* `{close_rate}` *Close Rate:* `{close_rate}`
*Current Rate:* `{current_rate}` *Current Rate:* `{current_rate:.8f}`
*Close Profit:* `{close_profit}` *Close Profit:* `{close_profit}`
*Current Profit:* `{current_profit:.2f}%` *Current Profit:* `{current_profit:.2f}%`
*Open Order:* `{open_order}` *Open Order:* `{open_order}`
@ -139,12 +161,51 @@ def _status(bot: Bot, update: Update) -> None:
current_rate=current_rate, current_rate=current_rate,
amount=round(trade.amount, 8), amount=round(trade.amount, 8),
close_profit=fmt_close_profit, close_profit=fmt_close_profit,
current_profit=round(current_profit, 2), current_profit=round(current_profit * 100, 2),
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None, open_order='{} ({})'.format(
order['remaining'], order['type']
) if order else None,
) )
send_msg(message, bot=bot) send_msg(message, bot=bot)
@authorized_only
def _status_table(bot: Bot, update: Update) -> None:
"""
Handler for /status table.
Returns the current TradeThread status in table format
:param bot: telegram bot
:param update: message update
:return: None
"""
# Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades:
send_msg('*Status:* `no active order`', bot=bot)
else:
trades_list = []
for trade in trades:
# calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid']
trades_list.append([
trade.id,
trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}'.format(100 * trade.calc_profit(current_rate))
])
columns = ['ID', 'Pair', 'Since', 'Profit']
df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0])
message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _profit(bot: Bot, update: Update) -> None: def _profit(bot: Bot, update: Update) -> None:
""" """
@ -160,6 +221,8 @@ def _profit(bot: Bot, update: Update) -> None:
profits = [] profits = []
durations = [] durations = []
for trade in trades: for trade in trades:
if not trade.open_rate:
continue
if trade.close_date: if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if trade.close_profit: if trade.close_profit:
@ -167,9 +230,9 @@ def _profit(bot: Bot, update: Update) -> None:
else: else:
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit = trade.calc_profit(current_rate)
profit_amounts.append((profit / 100) * trade.stake_amount) profit_amounts.append(profit * trade.stake_amount)
profits.append(profit) profits.append(profit)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
@ -184,7 +247,7 @@ def _profit(bot: Bot, update: Update) -> None:
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
markdown_msg = """ markdown_msg = """
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)` *ROI:* `{profit_btc:.8f} ({profit:.2f}%)`
*Trade Count:* `{trade_count}` *Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}` *First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}` *Latest Trade opened:* `{latest_trade_date}`
@ -192,13 +255,13 @@ def _profit(bot: Bot, update: Update) -> None:
*Best Performing:* `{best_pair}: {best_rate:.2f}%` *Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format( """.format(
profit_btc=round(sum(profit_amounts), 8), profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits), 2), profit=round(sum(profits) * 100, 2),
trade_count=len(trades), trade_count=len(trades),
first_trade_date=arrow.get(trades[0].open_date).humanize(), first_trade_date=arrow.get(trades[0].open_date).humanize(),
latest_trade_date=arrow.get(trades[-1].open_date).humanize(), latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair, best_pair=bp_pair,
best_rate=round(bp_rate, 2), best_rate=round(bp_rate * 100, 2),
) )
send_msg(markdown_msg, bot=bot) send_msg(markdown_msg, bot=bot)
@ -282,20 +345,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
return return
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
# Get available balance from freqtrade.main import execute_sell
currency = trade.pair.split('_')[1] execute_sell(trade, current_rate)
balance = exchange.get_balance(currency)
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
send_msg(message)
except ValueError: except ValueError:
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`') send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
@ -321,13 +372,33 @@ def _performance(bot: Bot, update: Update) -> None:
.order_by(text('profit_sum DESC')) \ .order_by(text('profit_sum DESC')) \
.all() .all()
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format( stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}%</code>'.format(
index=i + 1, index=i + 1,
pair=pair, pair=pair,
profit=round(rate, 2) profit=round(rate * 100, 2)
) for i, (pair, rate) in enumerate(pair_rates)) ) for i, (pair, rate) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}\n'.format(stats) message = '<b>Performance:</b>\n{}'.format(stats)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _count(bot: Bot, update: Update) -> None:
"""
Handler for /count.
Returns the number of trades running
:param bot: telegram bot
:param update: message update
:return: None
"""
if get_state() != State.RUNNING:
send_msg('`trader is not running`', bot=bot)
return
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
message = '<b>Count:</b>\ncurrent/max\n{}/{}\n'.format(len(trades), _CONF['max_open_trades'])
logger.debug(message) logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML) send_msg(message, parse_mode=ParseMode.HTML)
@ -344,16 +415,30 @@ def _help(bot: Bot, update: Update) -> None:
message = """ message = """
*/start:* `Starts the trader` */start:* `Starts the trader`
*/stop:* `Stops the trader` */stop:* `Stops the trader`
*/status:* `Lists all open trades` */status [table]:* `Lists all open trades`
*table :* `will display trades in a table`
*/profit:* `Lists cumulative profit from all finished trades` */profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit` */forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair` */performance:* `Show performance of each finished trade grouped by pair`
*/count:* `Show number of trades running compared to allowed number of trades`
*/balance:* `Show account balance per currency` */balance:* `Show account balance per currency`
*/help:* `This help message` */help:* `This help message`
""" """
send_msg(message, bot=bot) send_msg(message, bot=bot)
def shorten_date(date):
"""
Trim the date so it fits on small screens
"""
new_date = re.sub('seconds?', 'sec', date)
new_date = re.sub('minutes?', 'min', new_date)
new_date = re.sub('hours?', 'h', new_date)
new_date = re.sub('days?', 'd', new_date)
new_date = re.sub('^an?', '1', new_date)
return new_date
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
@ -362,18 +447,17 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
if _CONF['telegram'].get('enabled', False): if not is_enabled():
try: return
bot = bot or _updater.bot
try: bot = bot or _UPDATER.bot
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 error:
# 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.',
) error.message
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode) )
except Exception: bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
logger.exception('Exception occurred within Telegram API')

118
freqtrade/tests/conftest.py Normal file
View File

@ -0,0 +1,118 @@
# pragma pylint: disable=missing-docstring
import json
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from jsonschema import validate
from telegram import Message, Chat, Update
from freqtrade.misc import CONF_SCHEMA
@pytest.fixture(scope="module")
def default_conf():
""" Returns validated configuration suitable for most tests """
configuration = {
"max_open_trades": 1,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05,
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
validate(configuration, CONF_SCHEMA)
return configuration
@pytest.fixture(scope="module")
def backtest_conf():
return {
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05
}
@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
def update():
_update = Update(0)
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
return _update
@pytest.fixture
def ticker():
return MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061,
})
@pytest.fixture
def limit_buy_order():
return {
'id': 'mocked_limit_buy',
'type': 'LIMIT_BUY',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.07256061,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
}
@pytest.fixture
def limit_sell_order():
return {
'id': 'mocked_limit_sell',
'type': 'LIMIT_SELL',
'pair': 'mocked',
'opened': datetime.utcnow(),
'rate': 0.0802134,
'amount': 206.43811673387373,
'remaining': 0.0,
'closed': datetime.utcnow(),
}

View File

@ -11,17 +11,15 @@ from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, popula
@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.json') as data_file:
data = json.load(data_file) return parse_ticker_dataframe(json.load(data_file))
return parse_ticker_dataframe(data['result'])
def test_dataframe_has_correct_columns(result): def test_dataframe_correct_columns(result):
assert result.columns.tolist() == \ assert result.columns.tolist() == \
['close', 'high', 'low', 'open', 'date', 'volume'] ['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_has_correct_length(result): def test_dataframe_correct_length(result):
assert len(result.index) == 5751 assert len(result.index) == 5751

View File

@ -1,5 +1,4 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import json
import logging import logging
import os import os
@ -7,20 +6,18 @@ import pytest
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade import exchange
from freqtrade.analyze import analyze_ticker from freqtrade.analyze import analyze_ticker
from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.main import should_sell
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
def format_results(results): def format_results(results):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format( return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index), len(results.index), results.profit.mean() * 100.0, results.profit.sum(), results.duration.mean() * 5)
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5
)
def print_pair_results(pair, results): def print_pair_results(pair, results):
@ -28,55 +25,41 @@ def print_pair_results(pair, results):
print(format_results(results[results.currency == pair])) print(format_results(results[results.currency == pair]))
@pytest.fixture def backtest(backtest_conf, backdata, mocker):
def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture
def conf():
return {
"minimal_roi": {
"50": 0.0,
"40": 0.01,
"30": 0.02,
"0": 0.045
},
"stoploss": -0.40
}
def backtest(conf, pairs, mocker):
trades = [] trades = []
exchange._API = Bittrex({'key': '', 'secret': ''})
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history') mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', backtest_conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')) mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
for pair in pairs: for pair, pair_data in backdata.items():
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file: mocked_history.return_value = pair_data
data = json.load(data_file) ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
mocked_history.return_value = data # for each buy point
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy() for row in ticker[ticker.buy == 1].itertuples(index=True):
# for each buy point trade = Trade(
for row in ticker[ticker.buy == 1].itertuples(index=True): open_rate=row.close,
trade = Trade(open_rate=row.close, open_date=row.date, amount=1) open_date=row.date,
# calculate win/lose forwards from buy point amount=1,
for row2 in ticker[row.Index:].itertuples(index=True): fee=exchange.get_fee() * 2
if should_sell(trade, row2.close, row2.date): )
current_profit = (row2.close - trade.open_rate) / trade.open_rate # calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True):
if should_sell(trade, row2.close, row2.date):
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) results = DataFrame.from_records(trades, columns=labels)
return results return results
@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_backtest(conf, pairs, mocker, report=True): def test_backtest(backtest_conf, backdata, mocker, report=True):
results = backtest(conf, pairs, mocker) results = backtest(backtest_conf, backdata, mocker)
print('====================== BACKTESTING REPORT ================================') print('====================== BACKTESTING REPORT ================================')
[print_pair_results(pair, results) for pair in pairs] for pair in backdata:
print_pair_results(pair, results)
print('TOTAL OVER ALL TRADES:') print('TOTAL OVER ALL TRADES:')
print(format_results(results)) print(format_results(results))

View File

@ -12,31 +12,12 @@ from pandas import DataFrame
from freqtrade.tests.test_backtesting import backtest, format_results from freqtrade.tests.test_backtesting import backtest, format_results
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 = 1200 TARGET_TRADES = 1200
@pytest.fixture
def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture
def conf():
return {
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05
}
def buy_strategy_generator(params): def buy_strategy_generator(params):
print(params) print(params)
@ -82,13 +63,13 @@ def buy_strategy_generator(params):
@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(conf, pairs, mocker): def test_hyperopt(backtest_conf, backdata, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend') mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
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(conf, pairs, mocker) results = backtest(backtest_conf, backdata, mocker)
result = format_results(results) result = format_results(results)
print(result) print(result)
@ -147,8 +128,8 @@ def test_hyperopt(conf, pairs, mocker):
]), ]),
} }
trials = Trials() trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40, trials=trials) best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials)
print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================') print('\n\n\n\n==================== HYPEROPT BACKTESTING REPORT ==============================')
print('Best parameters {}'.format(best)) print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss')) newlist = sorted(trials.results, key=itemgetter('loss'))
print('Result: {}'.format(newlist[0]['result'])) print('Result: {}'.format(newlist[0]['result']))

View File

@ -1,91 +1,162 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import copy import copy
from unittest.mock import MagicMock, call from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import validate import requests
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.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
get_target_bid get_target_bid, _process
from freqtrade.misc import CONF_SCHEMA from freqtrade.misc import get_state, State
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@pytest.fixture def test_process_trade_creation(default_conf, ticker, mocker):
def conf(): mocker.patch.dict('freqtrade.main._CONF', default_conf)
configuration = { mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
"max_open_trades": 3, mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
"stake_currency": "BTC", mocker.patch.multiple('freqtrade.main.exchange',
"stake_amount": 0.05, validate_pairs=MagicMock(),
"dry_run": True, get_ticker=ticker,
"minimal_roi": { buy=MagicMock(return_value='mocked_limit_buy'))
"2880": 0.005, init(default_conf, 'sqlite://')
"720": 0.01,
"0": 0.02 trades = Trade.query.filter(Trade.is_open.is_(True)).all()
}, assert len(trades) == 0
"bid_strategy": {
"ask_last_balance": 0.0 result = _process()
}, assert result is True
"exchange": {
"name": "bittrex", trades = Trade.query.filter(Trade.is_open.is_(True)).all()
"enabled": True, assert len(trades) == 1
"key": "key", trade = trades[0]
"secret": "secret", assert trade is not None
"pair_whitelist": [ assert trade.stake_amount == default_conf['stake_amount']
"BTC_ETH", assert trade.is_open
"BTC_TKN", assert trade.open_date is not None
"BTC_TRST", assert trade.exchange == Exchanges.BITTREX.name
"BTC_SWT", assert trade.open_rate == 0.072661
] assert trade.amount == 0.6864067381401302
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "chat_id"
}
}
validate(configuration, CONF_SCHEMA)
return configuration
def test_create_trade(conf, mocker): def test_process_exchange_failures(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(side_effect=requests.exceptions.RequestException))
init(default_conf, 'sqlite://')
result = _process()
assert result is False
assert sleep_mock.has_calls()
def test_process_runtime_error(default_conf, ticker, mocker):
msg_mock = MagicMock()
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=msg_mock)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(side_effect=RuntimeError))
init(default_conf, 'sqlite://')
assert get_state() == State.RUNNING
result = _process()
assert result is False
assert get_state() == State.STOPPED
assert 'RuntimeError' in msg_mock.call_args_list[-1][0][0]
def test_process_trade_handling(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_order=MagicMock(return_value=limit_buy_order))
init(default_conf, 'sqlite://')
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 0
result = _process()
assert result is True
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert len(trades) == 1
result = _process()
assert result is False
def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', 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=ticker,
'bid': 0.07256061, buy=MagicMock(return_value='mocked_limit_buy'))
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
# Save state of current whitelist # Save state of current whitelist
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(conf, 'sqlite://') init(default_conf, 'sqlite://')
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: trade = create_trade(15.0)
trade = create_trade(15.0) Trade.session.add(trade)
Trade.session.add(trade) Trade.session.flush()
Trade.session.flush() assert trade is not None
assert trade is not None assert trade.stake_amount == 15.0
assert trade.open_rate == 0.072661 assert trade.is_open
assert trade.pair == pair assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
assert trade.amount == 206.43811673387373
assert trade.stake_amount == 15.0
assert trade.is_open
assert trade.open_date is not None
assert whitelist == conf['exchange']['pair_whitelist']
buy_signal.assert_has_calls( # Simulate fulfilled LIMIT_BUY order for trade
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')] trade.update(limit_buy_order)
)
assert trade.open_rate == 0.07256061
assert trade.amount == 206.43811673387373
assert whitelist == default_conf['exchange']['pair_whitelist']
def test_handle_trade(conf, mocker): def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(ValueError, match=r'.*stake amount.*'):
create_trade(default_conf['stake_amount'])
def test_create_trade_no_pairs(default_conf, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
with pytest.raises(ValueError, match=r'.*No pair in whitelist.*'):
conf = copy.deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = []
mocker.patch.dict('freqtrade.main._CONF', conf)
create_trade(default_conf['stake_amount'])
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock()) mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -94,18 +165,45 @@ def test_handle_trade(conf, mocker):
'ask': 0.172661, 'ask': 0.172661,
'last': 0.17256061 'last': 0.17256061
}), }),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell'))
init(default_conf, 'sqlite://')
trade = create_trade(15.0)
trade.update(limit_buy_order)
Trade.session.add(trade)
Trade.session.flush()
trade = Trade.query.filter(Trade.is_open.is_(True)).first() trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
handle_trade(trade) handle_trade(trade)
assert trade.close_rate == 0.17256061 assert trade.open_order_id == 'mocked_limit_sell'
assert trade.close_profit == 137.4872490056564 assert close_trade_if_fulfilled(trade) is False
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
assert trade.close_rate == 0.0802134
assert trade.close_profit == 0.10046755
assert trade.close_date is not None assert trade.close_date is not None
assert trade.open_order_id == 'dry_run'
def test_close_trade(conf, mocker): def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'))
# Create trade and sell it
init(default_conf, 'sqlite://')
trade = create_trade(15.0)
trade.update(limit_buy_order)
trade.update(limit_sell_order)
Trade.session.add(trade)
Trade.session.flush()
trade = Trade.query.filter(Trade.is_open.is_(True)).first() trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade assert trade
@ -115,6 +213,8 @@ def test_close_trade(conf, mocker):
closed = close_trade_if_fulfilled(trade) closed = close_trade_if_fulfilled(trade)
assert closed assert closed
assert not trade.is_open assert not trade.is_open
with pytest.raises(ValueError, match=r'.*closed trade.*'):
handle_trade(trade)
def test_balance_fully_ask_side(mocker): def test_balance_fully_ask_side(mocker):
@ -127,6 +227,6 @@ def test_balance_fully_last_side(mocker):
assert get_target_bid({'ask': 20, 'last': 10}) == 10 assert get_target_bid({'ask': 20, 'last': 10}) == 10
def test_balance_when_last_bigger_than_ask(mocker): def test_balance_bigger_last_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}) mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5 assert get_target_bid({'ask': 5, 'last': 10}) == 5

View File

@ -1,21 +1,66 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring
import pytest
from freqtrade.exchange import Exchanges from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_exec_sell_order(mocker): def test_update(limit_buy_order, limit_sell_order):
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
stake_amount=1.00, stake_amount=1.00,
open_rate=0.50, fee=0.1,
amount=10.00,
exchange=Exchanges.BITTREX, exchange=Exchanges.BITTREX,
open_order_id='mocked'
) )
profit = trade.exec_sell_order(1.00, 10.00) assert trade.open_order_id is None
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0) assert trade.open_rate is None
assert profit == 100.0 assert trade.close_profit is None
assert trade.close_rate == 1.0 assert trade.close_date is None
assert trade.close_profit == profit
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.open_order_id is None
assert trade.open_rate == 0.07256061
assert trade.close_profit is None
assert trade.close_date is None
trade.open_order_id = 'something'
trade.update(limit_sell_order)
assert trade.open_order_id is None
assert trade.open_rate == 0.07256061
assert trade.close_profit == 0.00546755
assert trade.close_date is not None assert trade.close_date is not None
def test_update_open_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
limit_buy_order['closed'] = False
trade.update(limit_buy_order)
assert trade.open_order_id is None
assert trade.open_rate is None
assert trade.close_profit is None
assert trade.close_date is None
def test_update_invalid_order(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
fee=0.1,
exchange=Exchanges.BITTREX,
)
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order)

View File

@ -1,76 +1,108 @@
# pragma pylint: disable=missing-docstring # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors
import re
from datetime import datetime from datetime import datetime
from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from jsonschema import validate
from telegram import Bot, Update, Message, Chat from telegram import Bot, Update, Message, Chat
from telegram.error import NetworkError
from freqtrade.main import init, create_trade from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA from freqtrade.misc import update_state, State, get_state
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop, _balance from freqtrade.rpc import telegram
from freqtrade.rpc.telegram import (
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance,
@pytest.fixture authorized_only, _help, is_enabled, send_msg
def conf(): )
configuration = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
validate(configuration, CONF_SCHEMA)
return configuration
@pytest.fixture
def update():
_update = Update(0)
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
return _update
class MagicBot(MagicMock, Bot): class MagicBot(MagicMock, Bot):
pass pass
def test_status_handle(conf, update, mocker): def test_is_enabled(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
default_conf['telegram']['enabled'] = False
assert is_enabled() is False
def test_init_disabled(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
default_conf['telegram']['enabled'] = False
telegram.init(default_conf)
def test_authorized_only(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
chat = Chat(0, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
state = {'called': False}
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
state['called'] = True
dummy_handler(MagicMock(), update)
assert state['called'] is True
def test_authorized_only_unauthorized(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
chat = Chat(0xdeadbeef, 0)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat)
state = {'called': False}
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
state['called'] = True
dummy_handler(MagicMock(), update)
assert state['called'] is False
def test_authorized_only_exception(default_conf, mocker):
mocker.patch.dict('freqtrade.rpc.telegram._CONF', default_conf)
update = Update(randint(1, 100))
update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0))
@authorized_only
def dummy_handler(*args, **kwargs) -> None:
raise Exception('test')
dummy_handler(MagicMock(), update)
def test_status_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True) mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
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=ticker)
'bid': 0.07256061, init(default_conf, 'sqlite://')
'ask': 0.072661,
'last': 0.07256061 update_state(State.STOPPED)
}), _status(bot=MagicBot(), update=update)
buy=MagicMock(return_value='mocked_order_id')) assert msg_mock.call_count == 1
init(conf, 'sqlite://') assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
_status(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'no active trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
@ -78,60 +110,117 @@ def test_status_handle(conf, update, mocker):
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
# Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicBot(), update=update) _status(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
def test_profit_handle(conf, update, mocker): def test_status_table_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', 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_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple(
'freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
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=ticker,
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')) buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://') init(default_conf, 'sqlite://')
update_state(State.STOPPED)
_status_table(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'trader is not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
_status_table(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'no active order' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061 Trade.session.add(trade)
trade.close_profit = 100.00 Trade.session.flush()
_status_table(bot=MagicBot(), update=update)
text = re.sub('</?pre>', '', msg_mock.call_args_list[-1][0][0])
line = text.split("\n")
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1
assert fields[1] == 'BTC_ETH'
assert msg_mock.call_count == 2
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('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, 'sqlite://')
_profit(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'no closed trade' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
# Create some test data
trade = create_trade(15.0)
assert trade
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
_profit(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock()
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False trade.is_open = False
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
_profit(bot=MagicBot(), update=update) _profit(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 1
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* `1.50701325 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
def test_forcesell_handle(conf, update, mocker): def test_forcesell_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', 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_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
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=ticker)
'bid': 0.07256061, init(default_conf, 'sqlite://')
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -140,31 +229,69 @@ def test_forcesell_handle(conf, update, mocker):
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0] assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
assert '0.072561' in msg_mock.call_args_list[-1][0][0] assert '0.07256061 (profit: ~-0.64%)' in msg_mock.call_args_list[-1][0][0]
def test_performance_handle(conf, update, mocker): def test_forcesell_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', 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_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock())
init(default_conf, 'sqlite://')
# Trader is not running
update_state(State.STOPPED)
update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
# No argument
msg_mock.reset_mock()
update_state(State.RUNNING)
update.message.text = '/forcesell'
_forcesell(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'Invalid argument' in msg_mock.call_args_list[0][0][0]
# Invalid argument
msg_mock.reset_mock()
update_state(State.RUNNING)
update.message.text = '/forcesell 123456'
_forcesell(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'no open trade' in msg_mock.call_args_list[0][0][0]
def test_performance_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
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=ticker)
'bid': 0.07256061, init(default_conf, 'sqlite://')
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data # Create some test data
trade = create_trade(15.0) trade = create_trade(15.0)
assert trade assert trade
trade.close_rate = 0.07256061
trade.close_profit = 100.00 # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False trade.is_open = False
Trade.session.add(trade) Trade.session.add(trade)
Trade.session.flush() Trade.session.flush()
@ -172,16 +299,74 @@ def test_performance_handle(conf, update, mocker):
_performance(bot=MagicBot(), update=update) _performance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2 assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0] assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0] assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
def test_start_handle(conf, update, mocker): def test_count_handle(default_conf, update, ticker, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple(
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock()) 'freqtrade.main.telegram',
init(conf, 'sqlite://') _CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
buy=MagicMock(return_value='mocked_order_id'))
init(default_conf, 'sqlite://')
update_state(State.STOPPED)
_count(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
update_state(State.RUNNING)
# Create some test data
trade = create_trade(15.0)
trade2 = create_trade(15.0)
assert trade
assert trade2
Trade.session.add(trade)
Trade.session.add(trade2)
Trade.session.flush()
_count(bot=MagicBot(), update=update)
line = msg_mock.call_args_list[-1][0][0].split("\n")
assert line[2] == '{}/{}'.format(2, default_conf['max_open_trades'])
def test_performance_handle_invalid(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock())
init(default_conf, 'sqlite://')
# Trader is not running
update_state(State.STOPPED)
_performance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert 'not running' in msg_mock.call_args_list[0][0][0]
def test_start_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, 'sqlite://')
update_state(State.STOPPED) update_state(State.STOPPED)
assert get_state() == State.STOPPED assert get_state() == State.STOPPED
_start(bot=MagicBot(), update=update) _start(bot=MagicBot(), update=update)
@ -189,13 +374,36 @@ def test_start_handle(conf, update, mocker):
assert msg_mock.call_count == 0 assert msg_mock.call_count == 0
def test_stop_handle(conf, update, mocker): def test_start_handle_already_running(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock()) _CONF=default_conf,
init(conf, 'sqlite://') init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, 'sqlite://')
update_state(State.RUNNING)
assert get_state() == State.RUNNING
_start(bot=MagicBot(), update=update)
assert get_state() == State.RUNNING
assert msg_mock.call_count == 1
assert 'already running' in msg_mock.call_args_list[0][0][0]
def test_stop_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, 'sqlite://')
update_state(State.RUNNING) update_state(State.RUNNING)
assert get_state() == State.RUNNING assert get_state() == State.RUNNING
_stop(bot=MagicBot(), update=update) _stop(bot=MagicBot(), update=update)
@ -204,16 +412,45 @@ def test_stop_handle(conf, update, mocker):
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(conf, update, mocker): def test_stop_handle_already_stopped(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
_CONF=default_conf,
init=MagicMock())
init(default_conf, 'sqlite://')
update_state(State.STOPPED)
assert get_state() == State.STOPPED
_stop(bot=MagicBot(), update=update)
assert get_state() == State.STOPPED
assert msg_mock.call_count == 1
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(default_conf, update, mocker):
mock_balance = [{ mock_balance = [{
'Currency': 'BTC', 'Currency': 'BTC',
'Balance': 10.0, 'Balance': 10.0,
'Available': 12.0, 'Available': 12.0,
'Pending': 0.0, 'Pending': 0.0,
'CryptoAddress': 'XXXX'}] 'CryptoAddress': 'XXXX',
mocker.patch.dict('freqtrade.main._CONF', conf) }, {
'Currency': 'ETH',
'Balance': 0.0,
'Available': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}]
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock) mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance)) get_balances=MagicMock(return_value=mock_balance))
@ -221,3 +458,46 @@ def test_balance_handle(conf, update, mocker):
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0] assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
assert 'Balance' in msg_mock.call_args_list[0][0][0] assert 'Balance' in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=msg_mock)
_help(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert '*/help:* `This help message`' in msg_mock.call_args_list[0][0][0]
def test_send_msg(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock())
bot = MagicMock()
send_msg('test', bot)
assert len(bot.method_calls) == 0
bot.reset_mock()
default_conf['telegram']['enabled'] = True
send_msg('test', bot)
assert len(bot.method_calls) == 1
def test_send_msg_network_error(default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch.multiple('freqtrade.main.telegram',
_CONF=default_conf,
init=MagicMock())
default_conf['telegram']['enabled'] = True
bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
with pytest.raises(NetworkError, match=r'Oh snap'):
send_msg('test', bot)
# Bot should've tried to send it twice
assert len(bot.method_calls) == 2

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

30
freqtrade/tests/testdata/download_backtest_data.py vendored Normal file → Executable file
View File

@ -1,18 +1,24 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""This script generate json data from bittrex""" """This script generate json data from bittrex"""
import json
from os import path
from urllib.request import urlopen from freqtrade import exchange
from freqtrade.exchange import Bittrex
CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"] PAIRS = ['BTC-OK', 'BTC-NEO', 'BTC-DASH', 'BTC-ETC', 'BTC-ETH', 'BTC-SNT']
OUTPUT_DIR = 'freqtrade/tests/testdata/' TICKER_INTERVAL = 1 # ticker interval in minutes (currently implemented: 1 and 5)
OUTPUT_DIR = path.dirname(path.realpath(__file__))
for cur in CURRENCIES: # Init Bittrex exchange
url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-' exchange._API = Bittrex({'key': '', 'secret': ''})
url = url1+cur+'&tickInterval=fiveMin'
x = urlopen(url) for pair in PAIRS:
json_data = x.read() data = exchange.get_ticker_history(pair, TICKER_INTERVAL)
json_str = str(json_data, 'utf-8') filename = path.join(OUTPUT_DIR, '{}-{}m.json'.format(
output = OUTPUT_DIR + 'btc-'+cur+'.json' pair.lower(),
with open(output, 'w') as file: TICKER_INTERVAL,
file.write(json_str) ))
with open(filename, 'w') as fp:
json.dump(data, fp)

View File

@ -91,7 +91,7 @@ def session(df, start='17:00', end='16:00'):
curr = prev = df[-1:].index[0].strftime('%Y-%m-%d') curr = prev = df[-1:].index[0].strftime('%Y-%m-%d')
# globex/forex session # globex/forex session
if is_same_day == False: if not is_same_day:
prev = (datetime.strptime(curr, '%Y-%m-%d') - prev = (datetime.strptime(curr, '%Y-%m-%d') -
timedelta(1)).strftime('%Y-%m-%d') timedelta(1)).strftime('%Y-%m-%d')
@ -117,13 +117,19 @@ def heikinashi(bars):
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
return pd.DataFrame(index=bars.index, data={'open': bars['ha_open'], return pd.DataFrame(
'high': bars['ha_high'], 'low': bars['ha_low'], 'close': bars['ha_close']}) index=bars.index,
data={
'open': bars['ha_open'],
'high': bars['ha_high'],
'low': bars['ha_low'],
'close': bars['ha_close']})
# --------------------------------------------- # ---------------------------------------------
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2, rsi_signal_len=7, bollinger_std=1.6185): def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2,
rsi_signal_len=7, bollinger_std=1.6185):
rsi_series = rsi(series, rsi_len) rsi_series = rsi(series, rsi_len)
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std) bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
signal = sma(rsi_series, rsi_signal_len) signal = sma(rsi_series, rsi_signal_len)
@ -248,9 +254,9 @@ def rolling_std(series, window=200, min_periods=None):
else: else:
try: try:
return series.rolling(window=window, min_periods=min_periods).std() return series.rolling(window=window, min_periods=min_periods).std()
except: except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).std() return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
except: except BaseException:
return pd.rolling_std(series, window=window, min_periods=min_periods) return pd.rolling_std(series, window=window, min_periods=min_periods)
@ -264,9 +270,9 @@ def rolling_mean(series, window=200, min_periods=None):
else: else:
try: try:
return series.rolling(window=window, min_periods=min_periods).mean() return series.rolling(window=window, min_periods=min_periods).mean()
except: except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean() return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
except: except BaseException:
return pd.rolling_mean(series, window=window, min_periods=min_periods) return pd.rolling_mean(series, window=window, min_periods=min_periods)
@ -277,9 +283,9 @@ def rolling_min(series, window=14, min_periods=None):
try: try:
try: try:
return series.rolling(window=window, min_periods=min_periods).min() return series.rolling(window=window, min_periods=min_periods).min()
except: except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min() return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.rolling_min(series, window=window, min_periods=min_periods)
@ -290,9 +296,9 @@ def rolling_max(series, window=14, min_periods=None):
try: try:
try: try:
return series.rolling(window=window, min_periods=min_periods).min() return series.rolling(window=window, min_periods=min_periods).min()
except: except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min() return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.rolling_min(series, window=window, min_periods=min_periods)
@ -302,7 +308,7 @@ def rolling_weighted_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: try:
return series.ewm(span=window, min_periods=min_periods).mean() return series.ewm(span=window, min_periods=min_periods).mean()
except: except BaseException:
return pd.ewma(series, span=window, min_periods=min_periods) return pd.ewma(series, span=window, min_periods=min_periods)
@ -457,7 +463,7 @@ def returns(series):
try: try:
res = (series / series.shift(1) - res = (series / series.shift(1) -
1).replace([np.inf, -np.inf], float('NaN')) 1).replace([np.inf, -np.inf], float('NaN'))
except: except BaseException:
res = nans(len(series)) res = nans(len(series))
return pd.Series(index=series.index, data=res) return pd.Series(index=series.index, data=res)
@ -469,7 +475,7 @@ def log_returns(series):
try: try:
res = np.log(series / series.shift(1) res = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN')) ).replace([np.inf, -np.inf], float('NaN'))
except: except BaseException:
res = nans(len(series)) res = nans(len(series))
return pd.Series(index=series.index, data=res) return pd.Series(index=series.index, data=res)
@ -482,7 +488,7 @@ def implied_volatility(series, window=252):
logret = np.log(series / series.shift(1) logret = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN')) ).replace([np.inf, -np.inf], float('NaN'))
res = numpy_rolling_std(logret, window) * np.sqrt(window) res = numpy_rolling_std(logret, window) * np.sqrt(window)
except: except BaseException:
res = nans(len(series)) res = nans(len(series))
return pd.Series(index=series.index, data=res) return pd.Series(index=series.index, data=res)

View File

@ -1,4 +1,4 @@
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex -e git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex
SQLAlchemy==1.1.14 SQLAlchemy==1.1.14
python-telegram-bot==8.1.1 python-telegram-bot==8.1.1
arrow==0.10.0 arrow==0.10.0
@ -17,7 +17,8 @@ pytest-cov==2.5.1
hyperopt==0.1 hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 networkx==1.11
tabulate==0.8.1
# Required for plotting data # Required for plotting data
#matplotlib==2.1.0 #matplotlib==2.1.0
#PYQT5==5.9 #PYQT5==5.9

View File

@ -1,5 +1,11 @@
from sys import version_info
from setuptools import setup from setuptools import setup
if version_info.major == 3 and version_info.minor < 6 or \
version_info.major < 3:
print('Your Python interpreter must be 3.6 or greater!')
exit(1)
from freqtrade import __version__ from freqtrade import __version__
@ -15,21 +21,22 @@ setup(name='freqtrade',
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
'python-bittrex==0.1.3', 'python-bittrex',
'SQLAlchemy==1.1.13', 'SQLAlchemy',
'python-telegram-bot==8.1.1', 'python-telegram-bot',
'arrow==0.10.0', 'arrow',
'requests==2.18.4', 'requests',
'urllib3==1.22', 'urllib3',
'wrapt==1.10.11', 'wrapt',
'pandas==0.20.3', 'pandas',
'scikit-learn==0.19.0', 'scikit-learn',
'scipy==0.19.1', 'scipy',
'jsonschema==2.6.0', 'jsonschema',
'TA-Lib==0.4.10', 'TA-Lib',
'tabulate',
], ],
dependency_links=[ dependency_links=[
"git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3" "git+https://github.com/ericsomdahl/python-bittrex.git@0.2.0#egg=python-bittrex"
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,