diff --git a/.gitignore b/.gitignore index bcf06e9c5..672dd1f6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,10 @@ # Freqtrade rules freqtrade/tests/testdata/*.json +hyperopt_conf.py +config.json +*.sqlite +.hyperopt +logfile.txt # Byte-compiled / optimized / DLL files __pycache__/ @@ -76,12 +81,6 @@ target/ # pyenv .python-version -config.json -preprocessor.py -*.sqlite -.hyperopt -logfile.txt - .env .venv .idea diff --git a/README.md b/README.md index f2cf6e8bd..2254296d8 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ if not feel free to raise a github issue. * python3.6 * sqlite * [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries +* Minimal (advised) system requirements: 2GB RAM, 1GB data, 2vCPU ### Install diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 1a0a035a9..ca7fce262 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -50,7 +50,7 @@ class Bittrex(Exchange): @property def fee(self) -> float: - # See https://bittrex.com/fees + # 0.25 %: See https://bittrex.com/fees return 0.0025 def buy(self, pair: str, rate: float, amount: float) -> str: diff --git a/freqtrade/main.py b/freqtrade/main.py index 941d6870a..212e9f0e9 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -118,13 +118,14 @@ def execute_sell(trade: Trade, limit: float) -> None: order_id = exchange.sell(str(trade.pair), limit, trade.amount) trade.open_order_id = order_id - fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2) - rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format( + fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) + rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%, {:.8f})`'.format( trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), limit, - fmt_exp_profit + fmt_exp_profit, + trade.calc_profit(rate=limit), )) Trade.session.flush() @@ -134,7 +135,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) - Based an earlier trade and current price and ROI configuration, decides whether bot should sell :return True if bot should sell at current rate """ - current_profit = trade.calc_profit(current_rate) + current_profit = trade.calc_profit_percent(current_rate) if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): logger.debug('Stop loss hit.') return True @@ -145,7 +146,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) - if time_diff > float(duration) and current_profit > threshold: return True - logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0) + logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', float(current_profit) * 100.0) return False @@ -233,7 +234,7 @@ def create_trade(stake_amount: float) -> bool: pair=pair, stake_amount=stake_amount, amount=amount, - fee=exchange.get_fee() * 2, + fee=exchange.get_fee(), open_rate=buy_limit, open_date=datetime.utcnow(), exchange=exchange.get_name().upper(), diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 12425cda2..e94fd913c 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -5,6 +5,7 @@ import json import os from typing import Optional, List, Dict from freqtrade.exchange import get_ticker_history +from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf from pandas import DataFrame @@ -13,7 +14,7 @@ from freqtrade.analyze import populate_indicators, parse_ticker_dataframe logger = logging.getLogger(__name__) -def load_data(pairs: List[str], ticker_interval: int = 5, +def load_data(ticker_interval: int = 5, pairs: Optional[List[str]] = None, refresh_pairs: Optional[bool] = False) -> Dict[str, List]: """ Loads ticker history data for the given parameters @@ -24,12 +25,14 @@ def load_data(pairs: List[str], ticker_interval: int = 5, path = testdata_path() result = {} + _pairs = pairs or hyperopt_optimize_conf()['exchange']['pair_whitelist'] + # If the user force the refresh of pairs if refresh_pairs: logger.info('Download data for all pairs and store them in freqtrade/tests/testsdata') - download_pairs(pairs) + download_pairs(_pairs) - for pair in pairs: + for pair in _pairs: file = '{abspath}/{pair}-{ticker_interval}.json'.format( abspath=path, pair=pair, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c281d439e..5761c9507 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -48,8 +48,8 @@ def generate_text_table( tabular_data.append([ pair, len(result.index), - '{:.2f}%'.format(result.profit.mean() * 100.0), - '{:.08f} {}'.format(result.profit.sum(), stake_currency), + '{:.2f}%'.format(result.profit_percent.mean() * 100.0), + '{:.08f} {}'.format(result.profit_BTC.sum(), stake_currency), '{:.2f}'.format(result.duration.mean() * ticker_interval), ]) @@ -57,8 +57,8 @@ def generate_text_table( tabular_data.append([ 'TOTAL', len(results.index), - '{:.2f}%'.format(results.profit.mean() * 100.0), - '{:.08f} {}'.format(results.profit.sum(), stake_currency), + '{:.2f}%'.format(results.profit_percent.mean() * 100.0), + '{:.08f} {}'.format(results.profit_BTC.sum(), stake_currency), '{:.2f}'.format(results.duration.mean() * ticker_interval), ]) return tabulate(tabular_data, headers=headers) @@ -98,8 +98,9 @@ def backtest(config: Dict, processed: Dict[str, DataFrame], trade = Trade( open_rate=row.close, open_date=row.date, - amount=config['stake_amount'], - fee=exchange.get_fee() * 2 + stake_amount=config['stake_amount'], + amount=config['stake_amount'] / row.open, + fee=exchange.get_fee() ) # calculate win/lose forwards from buy point @@ -109,12 +110,20 @@ def backtest(config: Dict, processed: Dict[str, DataFrame], trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: - current_profit = trade.calc_profit(row2.close) + current_profit_percent = trade.calc_profit_percent(rate=row2.close) + current_profit_BTC = trade.calc_profit(rate=row2.close) lock_pair_until = row2.Index - trades.append((pair, current_profit, row2.Index - row.Index)) + trades.append( + ( + pair, + current_profit_percent, + current_profit_BTC, + row2.Index - row.Index + ) + ) break - labels = ['currency', 'profit', 'duration'] + labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] return DataFrame.from_records(trades, columns=labels) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 07e988a91..e1d4e3a56 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -16,6 +16,7 @@ from freqtrade import exchange, optimize from freqtrade.exchange import Bittrex from freqtrade.misc import load_config from freqtrade.optimize.backtesting import backtest +from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf from freqtrade.vendor.qtpylib.indicators import crossed_above # Remove noisy log messages @@ -35,19 +36,8 @@ AVG_PROFIT_TO_BEAT = 0.2 AVG_DURATION_TO_BEAT = 50 # Configuration and data used by hyperopt -PROCESSED = [] -OPTIMIZE_CONFIG = { - 'max_open_trades': 3, - 'stake_currency': 'BTC', - 'stake_amount': 0.01, - 'minimal_roi': { - '40': 0.0, - '30': 0.01, - '20': 0.02, - '0': 0.04, - }, - 'stoploss': -0.10, -} +PROCESSED = optimize.preprocess(optimize.load_data()) +OPTIMIZE_CONFIG = hyperopt_optimize_conf() # Monkey patch config from freqtrade import main # noqa @@ -131,7 +121,7 @@ def optimizer(params): result = format_results(results) - total_profit = results.profit.sum() * 1000 + total_profit = results.profit_percent.sum() * 1000 trade_count = len(results.index) trade_loss = 1 - 0.35 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.2) @@ -144,7 +134,7 @@ def optimizer(params): 'total_profit': total_profit, 'trade_loss': trade_loss, 'profit_loss': profit_loss, - 'avg_profit': results.profit.mean() * 100.0, + 'avg_profit': results.profit_percent.mean() * 100.0, 'avg_duration': results.duration.mean() * 5, 'current_tries': _CURRENT_TRIES, 'total_tries': TOTAL_TRIES, @@ -166,8 +156,8 @@ def format_results(results: DataFrame): return ('Made {:6d} buys. Average profit {: 5.2f}%. ' 'Total profit was {: 7.3f}. Average duration {:5.1f} mins.').format( len(results.index), - results.profit.mean() * 100.0, - results.profit.sum(), + results.profit_percent.mean() * 100.0, + results.profit_BTC.sum(), results.duration.mean() * 5, ) diff --git a/freqtrade/optimize/hyperopt_conf.py b/freqtrade/optimize/hyperopt_conf.py new file mode 100644 index 000000000..cbd95973a --- /dev/null +++ b/freqtrade/optimize/hyperopt_conf.py @@ -0,0 +1,41 @@ +""" +File that contains the configuration for Hyperopt +""" + + +def hyperopt_optimize_conf() -> dict: + """ + This function is used to define which parameters Hyperopt must used. + The "pair_whitelist" is only used is your are using Hyperopt with MongoDB, + without MongoDB, Hyperopt will use the pair your have set in your config file. + :return: + """ + return { + 'max_open_trades': 3, + 'stake_currency': 'BTC', + 'stake_amount': 0.01, + "minimal_roi": { + '40': 0.0, + '30': 0.01, + '20': 0.02, + '0': 0.04, + }, + 'stoploss': -0.10, + "bid_strategy": { + "ask_last_balance": 0.0 + }, + "exchange": { + "pair_whitelist": [ + "BTC_ETH", + "BTC_LTC", + "BTC_ETC", + "BTC_DASH", + "BTC_ZEC", + "BTC_XLM", + "BTC_NXT", + "BTC_POWR", + "BTC_ADA", + "BTC_XMR" + ] + } + } diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index ba0ad1785..9825299b7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -92,10 +92,12 @@ class Trade(_DECL_BASE): return logger.info('Updating trade (id=%d) ...', self.id) + + getcontext().prec = 8 # Bittrex do not go above 8 decimal if order['type'] == 'LIMIT_BUY': # Update open rate and actual amount - self.open_rate = order['rate'] - self.amount = order['amount'] + self.open_rate = Decimal(order['rate']) + self.amount = Decimal(order['amount']) logger.info('LIMIT_BUY has been fulfilled for %s.', self) self.open_order_id = None elif order['type'] == 'LIMIT_SELL': @@ -109,8 +111,8 @@ class Trade(_DECL_BASE): Sets close_rate to the given rate, calculates total profit and marks trade as closed """ - self.close_rate = rate - self.close_profit = self.calc_profit() + self.close_rate = Decimal(rate) + self.close_profit = self.calc_profit_percent() self.close_date = datetime.utcnow() self.is_open = False self.open_order_id = None @@ -119,7 +121,65 @@ class Trade(_DECL_BASE): self ) - def calc_profit(self, rate: Optional[float] = None) -> float: + def calc_open_trade_price( + self, + fee: Optional[float] = None) -> float: + """ + Calculate the open_rate in BTC + :param fee: fee to use on the open rate (optional). + If rate is not set self.fee will be used + :return: Price in BTC of the open trade + """ + getcontext().prec = 8 + + buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) + fees = buy_trade * Decimal(fee or self.fee) + return float(buy_trade + fees) + + def calc_close_trade_price( + self, + rate: Optional[float] = None, + fee: Optional[float] = None) -> float: + """ + Calculate the close_rate in BTC + :param fee: fee to use on the close rate (optional). + If rate is not set self.fee will be used + :param rate: rate to compare with (optional). + If rate is not set self.close_rate will be used + :return: Price in BTC of the open trade + """ + getcontext().prec = 8 + + if rate is None and not self.close_rate: + return 0.0 + + sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate)) + fees = sell_trade * Decimal(fee or self.fee) + return float(sell_trade - fees) + + def calc_profit( + self, + rate: Optional[float] = None, + fee: Optional[float] = None) -> float: + """ + Calculate the profit in BTC between Close and Open trade + :param fee: fee to use on the close rate (optional). + If rate is not set self.fee will be used + :param rate: close rate to compare with (optional). + If rate is not set self.close_rate will be used + :return: profit in BTC as float + """ + open_trade_price = self.calc_open_trade_price() + close_trade_price = self.calc_close_trade_price( + rate=Decimal(rate or self.close_rate), + fee=Decimal(fee or self.fee) + ) + return float("{0:.8f}".format(close_trade_price - open_trade_price)) + + def calc_profit_percent( + self, + rate: Optional[float] = None, + fee: Optional[float] = None) -> float: """ Calculates the profit in percentage (including fee). :param rate: rate to compare with (optional). @@ -127,5 +187,11 @@ class Trade(_DECL_BASE): :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)) + + open_trade_price = self.calc_open_trade_price() + close_trade_price = self.calc_close_trade_price( + rate=Decimal(rate or self.close_rate), + fee=Decimal(fee or self.fee) + ) + + return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1)) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e63579ee3..d62d491e1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1,7 +1,7 @@ import logging import re -from datetime import timedelta, date from decimal import Decimal +from datetime import timedelta, date, datetime from typing import Callable, Any import arrow @@ -139,7 +139,7 @@ def _status(bot: Bot, update: Update) -> None: order = exchange.get_order(trade.open_order_id) # calculate profit and send message to user current_rate = exchange.get_ticker(trade.pair)['bid'] - current_profit = trade.calc_profit(current_rate) + current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = '{:.2f}%'.format( round(trade.close_profit * 100, 2) ) if trade.close_profit else None @@ -196,7 +196,7 @@ def _status_table(bot: Bot, update: Update) -> None: trade.id, trade.pair, shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), - '{:.2f}%'.format(100 * trade.calc_profit(current_rate)) + '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) ]) columns = ['ID', 'Pair', 'Since', 'Profit'] @@ -218,7 +218,7 @@ def _daily(bot: Bot, update: Update) -> None: :param update: message update :return: None """ - today = date.today().toordinal() + today = datetime.utcnow().toordinal() profit_days = {} try: @@ -234,8 +234,12 @@ def _daily(bot: Bot, update: Update) -> None: # need to query between day+1 and day-1 nextdate = date.fromordinal(today - day + 1) prevdate = date.fromordinal(today - day - 1) - trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() - curdayprofit = sum(trade.close_profit * trade.stake_amount for trade in trades) + trades = Trade.query \ + .filter(Trade.is_open.is_(False)) \ + .filter(between(Trade.close_date, prevdate, nextdate)) \ + .order_by(Trade.close_date)\ + .all() + curdayprofit = sum(trade.calc_profit() for trade in trades) profit_days[date.fromordinal(today - day)] = format(curdayprofit, '.8f') stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()] @@ -257,9 +261,9 @@ def _profit(bot: Bot, update: Update) -> None: trades = Trade.query.order_by(Trade.id).all() profit_all_btc = [] - profit_all = [] + profit_all_percent = [] profit_btc_closed = [] - profit_closed = [] + profit_closed_percent = [] durations = [] for trade in trades: @@ -271,16 +275,16 @@ def _profit(bot: Bot, update: Update) -> None: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit = trade.close_profit - profit_btc_closed.append(Decimal(trade.close_rate) - Decimal(trade.open_rate)) - profit_closed.append(profit) + profit_percent = trade.calc_profit_percent() + profit_btc_closed.append(trade.calc_profit()) + profit_closed_percent.append(profit_percent) else: # Get current rate current_rate = exchange.get_ticker(trade.pair)['bid'] - profit = trade.calc_profit(current_rate) + profit_percent = trade.calc_profit_percent(rate=current_rate) - profit_all_btc.append(Decimal(trade.close_rate or current_rate) - Decimal(trade.open_rate)) - profit_all.append(profit) + profit_all_btc.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate))) + profit_all_percent.append(profit_percent) best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ .filter(Trade.is_open.is_(False)) \ @@ -294,8 +298,8 @@ def _profit(bot: Bot, update: Update) -> None: bp_pair, bp_rate = best_pair markdown_msg = """ -*ROI Trade closed:* `{profit_closed_btc:.8f} BTC ({profit_closed:.2f}%)` -*ROI All trades:* `{profit_all_btc:.8f} BTC ({profit_all:.2f}%)` +*ROI Trade closed:* `{profit_closed_btc:.8f} BTC ({profit_closed_percent:.2f}%)` +*ROI All trades:* `{profit_all_btc:.8f} BTC ({profit_all_percent:.2f}%)` *Total Trade Count:* `{trade_count}` *First Trade opened:* `{first_trade_date}` *Latest Trade opened:* `{latest_trade_date}` @@ -303,9 +307,9 @@ def _profit(bot: Bot, update: Update) -> None: *Best Performing:* `{best_pair}: {best_rate:.2f}%` """.format( profit_closed_btc=round(sum(profit_btc_closed), 8), - profit_closed=round(sum(profit_closed) * 100, 2), + profit_closed_percent=round(sum(profit_closed_percent) * 100, 2), profit_all_btc=round(sum(profit_all_btc), 8), - profit_all=round(sum(profit_all) * 100, 2), + profit_all_percent=round(sum(profit_all_percent) * 100, 2), trade_count=len(trades), first_trade_date=arrow.get(trades[0].open_date).humanize(), latest_trade_date=arrow.get(trades[-1].open_date).humanize(), diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 7e7908748..b034b8c9f 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -15,7 +15,7 @@ def default_conf(): configuration = { "max_open_trades": 1, "stake_currency": "BTC", - "stake_amount": 0.05, + "stake_amount": 0.001, "dry_run": True, "minimal_roi": { "40": 0.0, @@ -61,9 +61,27 @@ def update(): @pytest.fixture def ticker(): return MagicMock(return_value={ - 'bid': 0.07256061, - 'ask': 0.072661, - 'last': 0.07256061, + 'bid': 0.00001098, + 'ask': 0.00001099, + 'last': 0.00001098, + }) + + +@pytest.fixture +def ticker_sell_up(): + return MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172, + }) + + +@pytest.fixture +def ticker_sell_down(): + return MagicMock(return_value={ + 'bid': 0.00001044, + 'ask': 0.00001043, + 'last': 0.00001044, }) @@ -104,8 +122,8 @@ def limit_buy_order(): 'type': 'LIMIT_BUY', 'pair': 'mocked', 'opened': datetime.utcnow(), - 'rate': 0.07256061, - 'amount': 206.43811673387373, + 'rate': 0.00001099, + 'amount': 90.99181073, 'remaining': 0.0, 'closed': datetime.utcnow(), } @@ -118,8 +136,8 @@ def limit_sell_order(): 'type': 'LIMIT_SELL', 'pair': 'mocked', 'opened': datetime.utcnow(), - 'rate': 0.0802134, - 'amount': 206.43811673387373, + 'rate': 0.00001173, + 'amount': 90.99181073, 'remaining': 0.0, 'closed': datetime.utcnow(), } diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index aeea5a1a1..d9b774a9d 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -40,8 +40,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker): assert trade.is_open assert trade.open_date is not None assert trade.exchange == Exchanges.BITTREX.name - assert trade.open_rate == 0.072661 - assert trade.amount == 0.6881270557795791 + assert trade.open_rate == 0.00001099 + assert trade.amount == 90.99181073703367 def test_process_exchange_failures(default_conf, ticker, health, mocker): @@ -115,11 +115,11 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) init(default_conf, create_engine('sqlite://')) - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() assert trade is not None - assert trade.stake_amount == 15.0 + assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None assert trade.exchange == Exchanges.BITTREX.name @@ -127,8 +127,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker): # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) - assert trade.open_rate == 0.07256061 - assert trade.amount == 206.43811673387373 + assert trade.open_rate == 0.00001099 + assert trade.amount == 90.99181073 assert whitelist == default_conf['exchange']['pair_whitelist'] @@ -186,14 +186,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): mocker.patch.multiple('freqtrade.main.exchange', validate_pairs=MagicMock(), get_ticker=MagicMock(return_value={ - 'bid': 0.17256061, - 'ask': 0.172661, - 'last': 0.17256061 + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 }), buy=MagicMock(return_value='mocked_limit_buy'), sell=MagicMock(return_value='mocked_limit_sell')) init(default_conf, create_engine('sqlite://')) - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() assert trade @@ -207,8 +207,9 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker): # 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_rate == 0.00001173 + assert trade.close_profit == 0.06201057 + assert trade.calc_profit() == 0.00006217 assert trade.close_date is not None @@ -223,7 +224,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo # Create trade and sell it init(default_conf, create_engine('sqlite://')) - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() assert trade diff --git a/freqtrade/tests/test_optimize_hyperopt_config.py b/freqtrade/tests/test_optimize_hyperopt_config.py new file mode 100644 index 000000000..e06c1f2eb --- /dev/null +++ b/freqtrade/tests/test_optimize_hyperopt_config.py @@ -0,0 +1,16 @@ +# pragma pylint: disable=missing-docstring,W0212 + +from freqtrade.optimize.hyperopt_conf import hyperopt_optimize_conf + + +def test_hyperopt_optimize_conf(): + hyperopt_conf = hyperopt_optimize_conf() + + assert "max_open_trades" in hyperopt_conf + assert "stake_currency" in hyperopt_conf + assert "stake_amount" in hyperopt_conf + assert "minimal_roi" in hyperopt_conf + assert "stoploss" in hyperopt_conf + assert "bid_strategy" in hyperopt_conf + assert "exchange" in hyperopt_conf + assert "pair_whitelist" in hyperopt_conf['exchange'] diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index eeb89d831..f91a8efd0 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -5,11 +5,38 @@ from freqtrade.exchange import Exchanges from freqtrade.persistence import Trade -def test_update(limit_buy_order, limit_sell_order): +def test_update_with_bittrex(limit_buy_order, limit_sell_order): + """ + On this test we will buy and sell a crypto currency. + + Buy + - Buy: 90.99181073 Crypto at 0.00001099 BTC + (90.99181073*0.00001099 = 0.0009999 BTC) + - Buying fee: 0.25% + - Total cost of buy trade: 0.001002500 BTC + ((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025)) + + Sell + - Sell: 90.99181073 Crypto at 0.00001173 BTC + (90.99181073*0.00001173 = 0,00106733394 BTC) + - Selling fee: 0.25% + - Total cost of sell trade: 0.001064666 BTC + ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025)) + + Profit/Loss: +0.000062166 BTC + (Sell:0.001064666 - Buy:0.001002500) + Profit/Loss percentage: 0.0620 + ((0.001064666/0.001002500)-1 = 6.20%) + + :param limit_buy_order: + :param limit_sell_order: + :return: + """ + trade = Trade( pair='BTC_ETH', - stake_amount=1.00, - fee=0.1, + stake_amount=0.001, + fee=0.0025, exchange=Exchanges.BITTREX, ) assert trade.open_order_id is None @@ -20,18 +47,40 @@ def test_update(limit_buy_order, limit_sell_order): 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.open_rate == 0.00001099 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_rate == 0.00001173 + assert trade.close_profit == 0.06201057 assert trade.close_date is not None +def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order): + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + fee=0.0025, + exchange=Exchanges.BITTREX, + ) + + trade.open_order_id = 'something' + trade.update(limit_buy_order) + assert trade.calc_open_trade_price() == 0.001002500 + + trade.update(limit_sell_order) + assert trade.calc_close_trade_price() == 0.0010646656 + + # Profit in BTC + assert trade.calc_profit() == 0.00006217 + + # Profit in percent + assert trade.calc_profit_percent() == 0.06201057 + + def test_update_open_order(limit_buy_order): trade = Trade( pair='BTC_ETH', @@ -64,3 +113,103 @@ def test_update_invalid_order(limit_buy_order): limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): trade.update(limit_buy_order) + + +def test_calc_open_trade_price(limit_buy_order): + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + fee=0.0025, + exchange=Exchanges.BITTREX, + ) + trade.open_order_id = 'open_trade' + trade.update(limit_buy_order) # Buy @ 0.00001099 + + # Get the open rate price with the standard fee rate + assert trade.calc_open_trade_price() == 0.001002500 + + # Get the open rate price with a custom fee rate + assert trade.calc_open_trade_price(fee=0.003) == 0.001003000 + + +def test_calc_close_trade_price(limit_buy_order, limit_sell_order): + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + fee=0.0025, + exchange=Exchanges.BITTREX, + ) + trade.open_order_id = 'close_trade' + trade.update(limit_buy_order) # Buy @ 0.00001099 + + # Get the close rate price with a custom close rate and a regular fee rate + assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318 + + # Get the close rate price with a custom close rate and a custom fee rate + assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704 + + # Test when we apply a Sell order, and ask price with a custom fee rate + trade.update(limit_sell_order) + assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972 + + +def test_calc_profit(limit_buy_order, limit_sell_order): + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + fee=0.0025, + exchange=Exchanges.BITTREX, + ) + trade.open_order_id = 'profit_percent' + trade.update(limit_buy_order) # Buy @ 0.00001099 + + # Custom closing rate and regular fee rate + # Higher than open rate + assert trade.calc_profit(rate=0.00001234) == 0.00011753 + # Lower than open rate + assert trade.calc_profit(rate=0.00000123) == -0.00089086 + + # Custom closing rate and custom fee rate + # Higher than open rate + assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697 + # Lower than open rate + assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092 + + # Only custom fee without sell order applied + with pytest.raises(TypeError): + trade.calc_profit(fee=0.003) + + # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 + trade.update(limit_sell_order) + assert trade.calc_profit() == 0.00006217 + + # Test with a custom fee rate on the close trade + assert trade.calc_profit(fee=0.003) == 0.00006163 + + +def test_calc_profit_percent(limit_buy_order, limit_sell_order): + trade = Trade( + pair='BTC_ETH', + stake_amount=0.001, + fee=0.0025, + exchange=Exchanges.BITTREX, + ) + trade.open_order_id = 'profit_percent' + trade.update(limit_buy_order) # Buy @ 0.00001099 + + # Get percent of profit with a custom rate (Higher than open rate) + assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387 + + # Get percent of profit with a custom rate (Lower than open rate) + assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827 + + # Only custom fee without sell order applied + with pytest.raises(TypeError): + trade.calc_profit_percent(fee=0.003) + + # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 + trade.update(limit_sell_order) + assert trade.calc_profit_percent() == 0.06201057 + + # Test with a custom fee rate on the close trade + assert trade.calc_profit_percent(fee=0.003) == 0.0614782 diff --git a/freqtrade/tests/test_rpc_telegram.py b/freqtrade/tests/test_rpc_telegram.py index afdef2692..3b6197f23 100644 --- a/freqtrade/tests/test_rpc_telegram.py +++ b/freqtrade/tests/test_rpc_telegram.py @@ -1,6 +1,6 @@ # pragma pylint: disable=missing-docstring, too-many-arguments, too-many-ancestors, C0103 import re -from datetime import datetime, date +from datetime import datetime from random import randint from unittest.mock import MagicMock @@ -102,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker): msg_mock.reset_mock() # Create some test data - create_trade(15.0) + create_trade(0.001) # Trigger status while we have a fulfilled order for the open trade _status(bot=MagicMock(), update=update) @@ -151,7 +151,8 @@ def test_status_table_handle(default_conf, update, ticker, mocker): assert msg_mock.call_count == 1 -def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): +def test_profit_handle( + default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) msg_mock = MagicMock() @@ -171,7 +172,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell msg_mock.reset_mock() # Create some test data - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -182,7 +183,10 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] msg_mock.reset_mock() - # Simulate fulfilled LIMIT_SELL order for trade + # Update the ticker with a market going up + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_up) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -190,11 +194,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell _profit(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert '*ROI All trades:* `0.00765279 BTC (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] + assert '*ROI Trade closed:* `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert '*ROI All trades:* `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] + assert 'Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0] -def test_forcesell_handle(default_conf, update, ticker, mocker): +def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker): mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) @@ -208,7 +213,44 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): init(default_conf, create_engine('sqlite://')) # Create some test data - create_trade(15.0) + create_trade(0.001) + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_up) + + update.message.text = '/forcesell 1' + _forcesell(bot=MagicMock(), update=update) + + assert rpc_mock.call_count == 2 + assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '0.00001172 (profit: ~6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0] + + +def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker): + mocker.patch.dict('freqtrade.main._CONF', default_conf) + mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) + rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) + mocker.patch.multiple('freqtrade.rpc.telegram', + _CONF=default_conf, + init=MagicMock(), + send_msg=MagicMock()) + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker) + init(default_conf, create_engine('sqlite://')) + + # Create some test data + create_trade(0.001) + + # Decrease the price and sell it + mocker.patch.multiple('freqtrade.main.exchange', + validate_pairs=MagicMock(), + get_ticker=ticker_sell_down) trade = Trade.query.first() assert trade @@ -218,7 +260,7 @@ def test_forcesell_handle(default_conf, update, ticker, mocker): assert rpc_mock.call_count == 2 assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] - assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0] + assert '0.00001044 (profit: ~-5.48%, -0.00005492)' in rpc_mock.call_args_list[-1][0][0] def test_exec_forcesell_open_orders(default_conf, ticker, mocker): @@ -260,7 +302,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): # Create some test data for _ in range(4): - create_trade(15.0) + create_trade(0.001) rpc_mock.reset_mock() update.message.text = '/forcesell all' @@ -268,7 +310,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker): assert rpc_mock.call_count == 4 for args in rpc_mock.call_args_list: - assert '0.07256061 (profit: ~-0.64%)' in args[0][0] + assert '0.00001098 (profit: ~-0.59%, -0.00000591)' in args[0][0] def test_forcesell_handle_invalid(default_conf, update, mocker): @@ -323,7 +365,7 @@ def test_performance_handle( init(default_conf, create_engine('sqlite://')) # Create some test data - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() assert trade @@ -339,7 +381,7 @@ def test_performance_handle( _performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'BTC_ETH\t10.05%' in msg_mock.call_args_list[0][0][0] + assert 'BTC_ETH\t6.20%' in msg_mock.call_args_list[0][0][0] def test_daily_handle( @@ -358,7 +400,7 @@ def test_daily_handle( init(default_conf, create_engine('sqlite://')) # Create some test data - create_trade(15.0) + create_trade(0.001) trade = Trade.query.first() assert trade @@ -371,14 +413,14 @@ def test_daily_handle( trade.close_date = datetime.utcnow() trade.is_open = False - # try valid data - update.message.text = '/daily 7' + # Try valid data + update.message.text = '/daily 2' _daily(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(date.today()) + ' 1.50701325 BTC' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) + ' 0.00006217 BTC' in msg_mock.call_args_list[0][0][0] - # try invalid data + # Try invalid data msg_mock.reset_mock() update_state(State.RUNNING) update.message.text = '/daily -2' @@ -409,7 +451,7 @@ def test_count_handle(default_conf, update, ticker, mocker): update_state(State.RUNNING) # Create some test data - create_trade(15.0) + create_trade(0.001) msg_mock.reset_mock() _count(bot=MagicMock(), update=update)