From 4a35676794754848a483d4afa33ff8699f963feb Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:12:18 +0100 Subject: [PATCH 01/16] rename and exchange instance and mark it as private --- freqtrade/analyze.py | 6 ++--- freqtrade/exchange/__init__.py | 48 ++++++++++++++++++++++------------ freqtrade/main.py | 2 +- 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index b99a67809..73365969d 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -17,8 +17,8 @@ logger = logging.getLogger(__name__) def parse_ticker_dataframe(ticker: list) -> DataFrame: """ - Analyses the trend for the given pair - :param pair: pair as str in format BTC_ETH or BTC-ETH + Analyses the trend for the given ticker history + :param ticker: See exchange.get_ticker_history :return: DataFrame """ df = DataFrame(ticker) \ @@ -161,7 +161,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None: if __name__ == '__main__': # Install PYQT5==5.9 manually if you want to test this helper function while True: - exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''}) + exchange._API = Bittrex({'key': '', 'secret': ''}) test_pair = 'BTC_ETH' # for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']: # get_buy_signal(pair) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b424660bf..769d076e9 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -10,7 +10,7 @@ from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) # Current selected exchange -EXCHANGE: Exchange = None +_API: Exchange = None _CONF: dict = {} @@ -29,7 +29,7 @@ def init(config: dict) -> None: :param config: config to use :return: None """ - global _CONF, EXCHANGE + global _CONF, _API _CONF.update(config) @@ -45,7 +45,7 @@ def init(config: dict) -> None: except KeyError: raise RuntimeError('Exchange {} is not supported'.format(name)) - EXCHANGE = exchange_class(exchange_config) + _API = exchange_class(exchange_config) # Check if all pairs are available validate_pairs(config['exchange']['pair_whitelist']) @@ -58,62 +58,76 @@ def validate_pairs(pairs: List[str]) -> None: :param pairs: list of pairs :return: None """ - markets = EXCHANGE.get_markets() + markets = _API.get_markets() for pair in pairs: 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: if _CONF['dry_run']: return 'dry_run' - return EXCHANGE.buy(pair, rate, amount) + return _API.buy(pair, rate, amount) def sell(pair: str, rate: float, amount: float) -> str: if _CONF['dry_run']: return 'dry_run' - return EXCHANGE.sell(pair, rate, amount) + return _API.sell(pair, rate, amount) def get_balance(currency: str) -> float: if _CONF['dry_run']: return 999.9 - return EXCHANGE.get_balance(currency) + return _API.get_balance(currency) def get_balances(): - return EXCHANGE.get_balances() + return _API.get_balances() 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): - return EXCHANGE.get_ticker_history(pair, minimum_date) + return _API.get_ticker_history(pair, minimum_date) def cancel_order(order_id: str) -> None: if _CONF['dry_run']: 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']: - return [] + return { - return EXCHANGE.get_open_orders(pair) + } + + return _API.get_order(order_id) 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]: - 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 diff --git a/freqtrade/main.py b/freqtrade/main.py index 68277adaa..15778cb46 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -266,7 +266,7 @@ def app(config: dict) -> None: 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) + time.sleep(exchange.get_sleep_time()) old_state = new_state except RuntimeError: telegram.send_msg( From 9b9d0250f77710413e552ebb34e239a7cf5fafad Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:13:23 +0100 Subject: [PATCH 02/16] replace get_open_oders() with get_order() and add property for fee --- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/bittrex.py | 36 ++++++++++++++++----------- freqtrade/exchange/interface.py | 44 +++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 769d076e9..170acbf3d 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,6 @@ import enum import logging -from typing import List +from typing import List, Dict import arrow diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 0b70e7a3b..306a7b698 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,5 +1,5 @@ import logging -from typing import List, Optional +from typing import List, Optional, Dict import arrow import requests @@ -36,6 +36,11 @@ class Bittrex(Exchange): _EXCHANGE_CONF.update(config) _API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret']) + @property + def fee(self) -> float: + # See https://bittrex.com/fees + return 0.0025 + def buy(self, pair: str, rate: float, amount: float) -> str: data = _API.buy_limit(pair.replace('_', '-'), amount, rate) if not data['success']: @@ -87,24 +92,27 @@ class Bittrex(Exchange): raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) return data + def get_order(self, order_id: str) -> Dict: + data = _API.get_order(order_id) + if not data['success']: + raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) + 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: data = _API.cancel(order_id) if not data['success']: raise RuntimeError('{}: {}'.format(self.name.upper(), data['message'])) - def get_open_orders(self, pair: str) -> List[dict]: - 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: return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 7c6f30be2..364b5afa4 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import List, Optional +from typing import List, Optional, Dict import arrow @@ -13,6 +13,14 @@ class Exchange(ABC): """ return self.__class__.__name__ + @property + def fee(self) -> float: + """ + Fee for placing an order + :return: percentage in float + """ + return 0.0 + @property @abstractmethod def sleep_time(self) -> float: @@ -100,6 +108,22 @@ class Exchange(ABC): } """ + 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: { + 'id': str, + 'type': str, + 'pair': str, + 'opened': str ISO 8601 datetime, + 'closed': str ISO 8601 datetime, + 'rate': float, + 'amount': float, + 'remaining': int + } + """ + @abstractmethod def cancel_order(self, order_id: str) -> None: """ @@ -108,24 +132,6 @@ class Exchange(ABC): :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 def get_pair_detail_url(self, pair: str) -> str: """ From 0e96197a94404caf6d381637606ac53b4433644b Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:22:38 +0100 Subject: [PATCH 03/16] don't spend the whole coin balance when selling --- freqtrade/main.py | 45 +++++++++--------- freqtrade/persistence.py | 60 ++++++++++++++---------- freqtrade/rpc/telegram.py | 97 +++++++++++++++++++++------------------ 3 files changed, 111 insertions(+), 91 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 15778cb46..18fa04af9 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -80,23 +80,25 @@ def close_trade_if_fulfilled(trade: Trade) -> bool: 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 current_rate: current rate + :param limit: limit rate for the sell order :return: None """ - # Get available balance - currency = trade.pair.split('_')[1] - balance = exchange.get_balance(currency) - profit = trade.exec_sell_order(current_rate, balance) - message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format( + # Execute sell and update trade record + order_id = exchange.sell(str(trade.pair), limit, trade.amount) + trade.open_order_id = order_id + trade.close_date = datetime.utcnow() + + exp_profit = round(trade.calc_profit(limit), 2) + message = '*{}:* Selling [{}]({}) with limit `{:f} (profit: ~{}%)`'.format( trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), - trade.close_rate, - round(profit, 2) + limit, + exp_profit ) logger.info(message) telegram.send_msg(message) @@ -107,17 +109,15 @@ 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 :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']): logger.debug('Stop loss hit.') return True 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 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 logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0) @@ -182,25 +182,24 @@ def create_trade(stake_amount: float) -> Optional[Trade]: else: return None - open_rate = get_target_bid(exchange.get_ticker(pair)) - amount = stake_amount / open_rate - order_id = exchange.buy(pair, open_rate, amount) + buy_limit = get_target_bid(exchange.get_ticker(pair)) + # TODO: apply fee to amount and also consider it for profit calculations + amount = stake_amount / buy_limit + order_id = exchange.buy(pair, buy_limit, amount) # Create trade entity and return - message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( - exchange.EXCHANGE.name.upper(), + message = '*{}:* Buying [{}]({}) with limit `{:f}`'.format( + exchange.get_name().upper(), pair.replace('_', '/'), exchange.get_pair_detail_url(pair), - open_rate + buy_limit ) logger.info(message) telegram.send_msg(message) return Trade(pair=pair, stake_amount=stake_amount, - open_rate=open_rate, open_date=datetime.utcnow(), - amount=amount, - exchange=exchange.EXCHANGE.name.upper(), + exchange=exchange.get_name().upper(), open_order_id=order_id, is_open=True) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index dc9078d5f..b916c2a34 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -1,15 +1,18 @@ +import logging from datetime import datetime -from typing import Optional +from typing import Optional, Dict +import arrow from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session 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 = {} - Base = declarative_base() @@ -51,44 +54,55 @@ class Trade(Base): exchange = Column(String, nullable=False) pair = Column(String, nullable=False) is_open = Column(Boolean, nullable=False, default=True) - open_rate = Column(Float, nullable=False) + open_rate = Column(Float) close_rate = Column(Float) close_profit = Column(Float) stake_amount = Column(Float, name='btc_amount', nullable=False) - amount = Column(Float, nullable=False) + amount = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) open_order_id = Column(String) 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( self.id, self.pair, self.amount, 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. - :param rate: rate to sell for - :param amount: amount to sell - :return: current profit as percentage + Updates this entity with amount and actual open/close rates. + :param order: order retrieved by exchange.get_order() + :return: None """ - profit = 100 * ((rate - self.open_rate) / self.open_rate) + if not order['closed']: + return - # Execute sell and update trade record - order_id = exchange.sell(str(self.pair), rate, amount) - self.close_rate = rate - self.close_profit = profit - self.close_date = datetime.utcnow() - self.open_order_id = order_id + logger.debug('Updating trade (id=%d) ...', self.id) + if order['type'] == 'LIMIT_BUY': + # Set open rate and actual amount + self.open_rate = order['rate'] + self.amount = order['amount'] + elif order['type'] == 'LIMIT_SELL': + # Set close rate and set actual profit + self.close_rate = order['rate'] + self.close_profit = self.calc_profit() + else: + raise ValueError('Unknown order type: {}'.format(order['type'])) + + self.open_order_id = None # Flush changes Trade.session.flush() - return profit + + def calc_profit(self, rate: float=None) -> float: + """ + Calculates the profit in percentage. + :param rate: rate to compare with (optional). + If rate is not set self.close_rate will be used + :return: profit in percentage as float + """ + return (rate or self.close_rate - self.open_rate) / self.open_rate diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b18cb821d..3cf035e35 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -114,20 +114,18 @@ def _status(bot: Bot, update: Update) -> None: 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) + send_msg('*Status:* `no active trade`', bot=bot) else: for trade in trades: - # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair)['bid'] - current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_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( - round(trade.close_profit, 2) - ) if trade.close_profit else None - message = """ + order = exchange.get_order(trade.open_order_id) + if trade.open_rate: + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair)['bid'] + current_profit = trade.calc_profit(current_rate) + fmt_close_profit = '{:.2f}%'.format( + round(trade.close_profit * 100, 2) + ) if trade.close_profit else None + message = """ *Trade ID:* `{trade_id}` *Current Pair:* [{pair}]({market_url}) *Open Since:* `{date}` @@ -138,19 +136,38 @@ def _status(bot: Bot, update: Update) -> None: *Close Profit:* `{close_profit}` *Current Profit:* `{current_profit:.2f}%` *Open Order:* `{open_order}` - """.format( - trade_id=trade.id, - pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), - open_rate=trade.open_rate, - close_rate=trade.close_rate, - current_rate=current_rate, - amount=round(trade.amount, 8), - close_profit=fmt_close_profit, - current_profit=round(current_profit, 2), - open_order='{} ({})'.format(order['remaining'], order['type']) if order else None, - ) + """.format( + trade_id=trade.id, + pair=trade.pair, + market_url=exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date).humanize(), + open_rate=trade.open_rate, + close_rate=trade.close_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + close_profit=fmt_close_profit, + current_profit=round(current_profit * 100, 2), + open_order='{} ({})'.format( + order['remaining'], order['type'] + ) if order else None, + ) + else: + message = """ +*Trade ID:* `{trade_id}` +*Current Pair:* [{pair}]({market_url}) +*Open Since:* `{date}` +*Open Order:* `{open_order}` + +`Waiting until order is fulfilled.` + """.format( + trade_id=trade.id, + pair=trade.pair, + market_url=exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date).humanize(), + open_order='{} ({})'.format( + order['remaining'], order['type'] + ) if order else None, + ) send_msg(message, bot=bot) @@ -169,6 +186,8 @@ def _profit(bot: Bot, update: Update) -> None: profits = [] durations = [] for trade in trades: + if not trade.open_rate: + continue if trade.close_date: durations.append((trade.close_date - trade.open_date).total_seconds()) if trade.close_profit: @@ -176,9 +195,9 @@ def _profit(bot: Bot, update: Update) -> None: else: # Get current rate 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) best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ @@ -193,7 +212,7 @@ def _profit(bot: Bot, update: Update) -> None: bp_pair, bp_rate = best_pair markdown_msg = """ -*ROI:* `{profit_btc:.2f} ({profit:.2f}%)` +*ROI:* `{profit_btc:.6f} ({profit:.2f}%)` *Trade Count:* `{trade_count}` *First Trade opened:* `{first_trade_date}` *Latest Trade opened:* `{latest_trade_date}` @@ -201,13 +220,13 @@ def _profit(bot: Bot, update: Update) -> None: *Best Performing:* `{best_pair}: {best_rate:.2f}%` """.format( profit_btc=round(sum(profit_amounts), 8), - profit=round(sum(profits), 2), + profit=round(sum(profits) * 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(), avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], best_pair=bp_pair, - best_rate=round(bp_rate, 2), + best_rate=round(bp_rate * 100, 2), ) send_msg(markdown_msg, bot=bot) @@ -291,20 +310,8 @@ def _forcesell(bot: Bot, update: Update) -> None: return # Get current rate current_rate = exchange.get_ticker(trade.pair)['bid'] - # Get available balance - currency = trade.pair.split('_')[1] - 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) + from freqtrade.main import execute_sell + execute_sell(trade, current_rate) except ValueError: send_msg('Invalid argument. Usage: `/forcesell `') @@ -333,7 +340,7 @@ def _performance(bot: Bot, update: Update) -> None: stats = '\n'.join('{index}. {pair}\t{profit:.2f}%'.format( index=i + 1, pair=pair, - profit=round(rate, 2) + profit=round(rate * 100, 2) ) for i, (pair, rate) in enumerate(pair_rates)) message = 'Performance:\n{}\n'.format(stats) From 9ff4a7b20537d10b82dde7302725b35c2a93e8e5 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:25:12 +0100 Subject: [PATCH 04/16] refactor _process to update trade state --- freqtrade/main.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 18fa04af9..bedaccb7e 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -8,6 +8,7 @@ from datetime import datetime from typing import Dict, Optional from signal import signal, SIGINT, SIGABRT, SIGTERM +import requests from jsonschema import validate from freqtrade import __version__, exchange, persistence @@ -44,22 +45,21 @@ def _process() -> None: logger.exception('Unable to create trade') for trade in trades: - # Check if there is already an open order for this trade - orders = exchange.get_open_orders(trade.pair) - orders = [o for o in orders if o['id'] == trade.open_order_id] - if orders: - logger.info('There is an open order for: %s', orders[0]) - else: - # Update state - trade.open_order_id = None - # Check if this trade can be closed - if not close_trade_if_fulfilled(trade): - # Check if we can sell our current pair - handle_trade(trade) - Trade.session.flush() - except (ConnectionError, json.JSONDecodeError) as error: - msg = 'Got {} in _process()'.format(error.__class__.__name__) + # Get order details for actual price per unit + if trade.open_order_id: + # Update trade with order values + logger.info('Got open order for %s', trade) + trade.update(exchange.get_order(trade.open_order_id)) + + if not close_trade_if_fulfilled(trade) and trade.open_rate: + # Check if we can sell our current pair + handle_trade(trade) + + Trade.session.flush() + except (requests.exceptions.ConnectionError, json.JSONDecodeError) as error: + msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__) logger.exception(msg) + time.sleep(30) def close_trade_if_fulfilled(trade: Trade) -> bool: @@ -133,7 +133,7 @@ def handle_trade(trade: Trade) -> None: if not trade.is_open: 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'] if should_sell(trade, current_rate, datetime.utcnow()): From 0d0d822904a92b5b3e28a2c14c31f88ba37e4188 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:26:21 +0100 Subject: [PATCH 05/16] bump dburl to tradesv3 --- freqtrade/persistence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index b916c2a34..d18d397c2 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -28,9 +28,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None: _CONF.update(config) if not db_url: if _CONF.get('dry_run', False): - db_url = 'sqlite:///tradesv2.dry_run.sqlite' + db_url = 'sqlite:///tradesv3.dry_run.sqlite' else: - db_url = 'sqlite:///tradesv2.sqlite' + db_url = 'sqlite:///tradesv3.sqlite' engine = create_engine(db_url, echo=False) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) From 1e5b0e87269834c9a24e1f5d687e9f58bc67c2d4 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:26:32 +0100 Subject: [PATCH 06/16] adapt tests --- freqtrade/tests/test_backtesting.py | 4 +- freqtrade/tests/test_hyperopt.py | 2 +- freqtrade/tests/test_main.py | 46 +++++++++++--- freqtrade/tests/test_persistence.py | 21 ------- freqtrade/tests/test_telegram.py | 96 ++++++++++++++++++++++++++--- 5 files changed, 127 insertions(+), 42 deletions(-) delete mode 100644 freqtrade/tests/test_persistence.py diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index 4bd04e721..deabebfb9 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -11,7 +11,7 @@ from freqtrade.analyze import analyze_ticker from freqtrade.main import should_sell 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): @@ -63,7 +63,7 @@ def backtest(conf, pairs, mocker): # 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 = (row2.close - trade.open_rate) / trade.open_rate + current_profit = trade.calc_profit(row2.close) trades.append((pair, current_profit, row2.Index - row.Index)) break diff --git a/freqtrade/tests/test_hyperopt.py b/freqtrade/tests/test_hyperopt.py index 5fedff519..5053cbfcd 100644 --- a/freqtrade/tests/test_hyperopt.py +++ b/freqtrade/tests/test_hyperopt.py @@ -12,7 +12,7 @@ from pandas import DataFrame from freqtrade.tests.test_backtesting import backtest, format_results 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 TARGET_TRADES = 1200 diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 67e66bd23..c098c3dc0 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring import copy +from datetime import datetime from unittest.mock import MagicMock, call import pytest @@ -60,23 +61,36 @@ def test_create_trade(conf, mocker): 'ask': 0.072661, 'last': 0.07256061 }), - buy=MagicMock(return_value='mocked_order_id')) + buy=MagicMock(return_value='mocked_limit_buy')) # Save state of current whitelist whitelist = copy.deepcopy(conf['exchange']['pair_whitelist']) init(conf, 'sqlite://') - for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: + for _ in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']: trade = create_trade(15.0) Trade.session.add(trade) Trade.session.flush() assert trade is not None - assert trade.open_rate == 0.072661 - assert trade.pair == pair - 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 trade.exchange == Exchanges.BITTREX.name + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update({ + 'id': 'mocked_limit_buy', + 'type': 'LIMIT_BUY', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.072661, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + + assert trade.open_rate == 0.072661 + assert trade.amount == 206.43811673387373 + assert whitelist == conf['exchange']['pair_whitelist'] buy_signal.assert_has_calls( @@ -94,14 +108,28 @@ def test_handle_trade(conf, mocker): 'ask': 0.172661, 'last': 0.17256061 }), - buy=MagicMock(return_value='mocked_order_id')) + sell=MagicMock(return_value='mocked_limit_sell')) trade = Trade.query.filter(Trade.is_open.is_(True)).first() assert trade + handle_trade(trade) + assert trade.open_order_id == 'mocked_limit_sell' + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update({ + 'id': 'mocked_sell_limit', + 'type': 'LIMIT_SELL', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.17256061, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + assert trade.close_rate == 0.17256061 - assert trade.close_profit == 137.4872490056564 + assert trade.close_profit == 1.3748724900565639 assert trade.close_date is not None - assert trade.open_order_id == 'dry_run' def test_close_trade(conf, mocker): diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py deleted file mode 100644 index 6a74ad715..000000000 --- a/freqtrade/tests/test_persistence.py +++ /dev/null @@ -1,21 +0,0 @@ -# pragma pylint: disable=missing-docstring -from freqtrade.exchange import Exchanges -from freqtrade.persistence import Trade - - -def test_exec_sell_order(mocker): - api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id') - trade = Trade( - pair='BTC_ETH', - stake_amount=1.00, - open_rate=0.50, - amount=10.00, - exchange=Exchanges.BITTREX, - open_order_id='mocked' - ) - profit = trade.exec_sell_order(1.00, 10.00) - api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0) - assert profit == 100.0 - assert trade.close_rate == 1.0 - assert trade.close_profit == profit - assert trade.close_date is not None diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 354abb086..8a6018129 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -46,6 +46,7 @@ def conf(): validate(configuration, CONF_SCHEMA) return configuration + @pytest.fixture def update(): _update = Update(0) @@ -78,8 +79,27 @@ def test_status_handle(conf, update, mocker): Trade.session.add(trade) Trade.session.flush() + # Trigger status while we don't know the open_rate yet _status(bot=MagicBot(), update=update) - assert msg_mock.call_count == 2 + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update({ + 'id': 'mocked_limit_buy', + 'type': 'LIMIT_BUY', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.07256060, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + Trade.session.flush() + + # Trigger status while we have a fulfilled order for the open trade + _status(bot=MagicBot(), update=update) + + assert msg_mock.call_count == 3 + assert 'Waiting until order is fulfilled' in msg_mock.call_args_list[-2][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] @@ -95,14 +115,36 @@ def test_profit_handle(conf, update, mocker): 'ask': 0.072661, 'last': 0.07256061 }), - buy=MagicMock(return_value='mocked_order_id')) + buy=MagicMock(return_value='mocked_limit_buy')) init(conf, 'sqlite://') # Create some test data trade = create_trade(15.0) assert trade - trade.close_rate = 0.07256061 - trade.close_profit = 100.00 + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update({ + 'id': 'mocked_limit_buy', + 'type': 'LIMIT_BUY', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.07256061, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + # Simulate fulfilled LIMIT_SELL order for trade + trade.update({ + 'id': 'mocked_limit_sell', + 'type': 'LIMIT_SELL', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.0802134, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + trade.close_date = datetime.utcnow() trade.open_order_id = None trade.is_open = False @@ -111,7 +153,8 @@ def test_profit_handle(conf, update, mocker): _profit(bot=MagicBot(), update=update) assert msg_mock.call_count == 2 - assert '(100.00%)' in msg_mock.call_args_list[-1][0][0] + assert '*ROI:* `1.582013 (10.55%)`' in msg_mock.call_args_list[-1][0][0] + assert 'Best Performing:* `BTC_ETH: 10.55%`' in msg_mock.call_args_list[-1][0][0] def test_forcesell_handle(conf, update, mocker): @@ -132,6 +175,19 @@ def test_forcesell_handle(conf, update, mocker): # Create some test data trade = create_trade(15.0) assert trade + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update({ + 'id': 'mocked_limit_buy', + 'type': 'LIMIT_BUY', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.07256060, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + Trade.session.add(trade) Trade.session.flush() @@ -161,10 +217,32 @@ def test_performance_handle(conf, update, mocker): # Create some test data trade = create_trade(15.0) assert trade - trade.close_rate = 0.07256061 - trade.close_profit = 100.00 + + # Simulate fulfilled LIMIT_BUY order for trade + trade.update({ + 'id': 'mocked_limit_buy', + 'type': 'LIMIT_BUY', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.07256061, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + + # Simulate fulfilled LIMIT_SELL order for trade + trade.update({ + 'id': 'mocked_limit_sell', + 'type': 'LIMIT_SELL', + 'pair': 'mocked', + 'opened': datetime.utcnow(), + 'rate': 0.0802134, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': datetime.utcnow(), + }) + trade.close_date = datetime.utcnow() - trade.open_order_id = None trade.is_open = False Trade.session.add(trade) Trade.session.flush() @@ -172,7 +250,7 @@ def test_performance_handle(conf, update, mocker): _performance(bot=MagicBot(), update=update) assert msg_mock.call_count == 2 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 'BTC_ETH\t10.55%' in msg_mock.call_args_list[-1][0][0] def test_start_handle(conf, update, mocker): From 543857ddb290b9fe80a41a4c32244e40602481d0 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:54:16 +0100 Subject: [PATCH 07/16] set initial open_rate and amount in create_trade This is mostly needed by dry_run --- freqtrade/exchange/__init__.py | 4 +-- freqtrade/main.py | 4 ++- freqtrade/persistence.py | 4 +-- freqtrade/rpc/telegram.py | 62 ++++++++++++-------------------- freqtrade/tests/test_telegram.py | 1 - 5 files changed, 28 insertions(+), 47 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 170acbf3d..58a020e12 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -106,9 +106,7 @@ def cancel_order(order_id: str) -> None: def get_order(order_id: str) -> Dict: if _CONF['dry_run']: - return { - - } + return {} return _API.get_order(order_id) diff --git a/freqtrade/main.py b/freqtrade/main.py index bedaccb7e..f96718c6b 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -51,7 +51,7 @@ def _process() -> None: logger.info('Got open order for %s', trade) trade.update(exchange.get_order(trade.open_order_id)) - if not close_trade_if_fulfilled(trade) and trade.open_rate: + if not close_trade_if_fulfilled(trade): # Check if we can sell our current pair handle_trade(trade) @@ -198,6 +198,8 @@ def create_trade(stake_amount: float) -> Optional[Trade]: telegram.send_msg(message) return Trade(pair=pair, stake_amount=stake_amount, + amount=amount, + open_rate=buy_limit, open_date=datetime.utcnow(), exchange=exchange.get_name().upper(), open_order_id=order_id, diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index d18d397c2..d16859724 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -78,12 +78,12 @@ class Trade(Base): :param order: order retrieved by exchange.get_order() :return: None """ - if not order['closed']: + if not order.get('closed'): return logger.debug('Updating trade (id=%d) ...', self.id) if order['type'] == 'LIMIT_BUY': - # Set open rate and actual amount + # Update open rate and actual amount self.open_rate = order['rate'] self.amount = order['amount'] elif order['type'] == 'LIMIT_SELL': diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3cf035e35..08396ca6f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -118,14 +118,13 @@ def _status(bot: Bot, update: Update) -> None: else: for trade in trades: order = exchange.get_order(trade.open_order_id) - if trade.open_rate: - # calculate profit and send message to user - current_rate = exchange.get_ticker(trade.pair)['bid'] - current_profit = trade.calc_profit(current_rate) - fmt_close_profit = '{:.2f}%'.format( - round(trade.close_profit * 100, 2) - ) if trade.close_profit else None - message = """ + # calculate profit and send message to user + current_rate = exchange.get_ticker(trade.pair)['bid'] + current_profit = trade.calc_profit(current_rate) + fmt_close_profit = '{:.2f}%'.format( + round(trade.close_profit * 100, 2) + ) if trade.close_profit else None + message = """ *Trade ID:* `{trade_id}` *Current Pair:* [{pair}]({market_url}) *Open Since:* `{date}` @@ -136,38 +135,21 @@ def _status(bot: Bot, update: Update) -> None: *Close Profit:* `{close_profit}` *Current Profit:* `{current_profit:.2f}%` *Open Order:* `{open_order}` - """.format( - trade_id=trade.id, - pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), - open_rate=trade.open_rate, - close_rate=trade.close_rate, - current_rate=current_rate, - amount=round(trade.amount, 8), - close_profit=fmt_close_profit, - current_profit=round(current_profit * 100, 2), - open_order='{} ({})'.format( - order['remaining'], order['type'] - ) if order else None, - ) - else: - message = """ -*Trade ID:* `{trade_id}` -*Current Pair:* [{pair}]({market_url}) -*Open Since:* `{date}` -*Open Order:* `{open_order}` - -`Waiting until order is fulfilled.` - """.format( - trade_id=trade.id, - pair=trade.pair, - market_url=exchange.get_pair_detail_url(trade.pair), - date=arrow.get(trade.open_date).humanize(), - open_order='{} ({})'.format( - order['remaining'], order['type'] - ) if order else None, - ) + """.format( + trade_id=trade.id, + pair=trade.pair, + market_url=exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date).humanize(), + open_rate=trade.open_rate, + close_rate=trade.close_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + close_profit=fmt_close_profit, + current_profit=round(current_profit * 100, 2), + open_order='{} ({})'.format( + order['remaining'], order['type'] + ) if order else None, + ) send_msg(message, bot=bot) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 8a6018129..109374321 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -99,7 +99,6 @@ def test_status_handle(conf, update, mocker): _status(bot=MagicBot(), update=update) assert msg_mock.call_count == 3 - assert 'Waiting until order is fulfilled' in msg_mock.call_args_list[-2][0][0] assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0] From 9cb249610a580a506f7bf0c1e2aad0908d2ff727 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 01:12:16 +0100 Subject: [PATCH 08/16] adapt dry_run return values --- freqtrade/exchange/__init__.py | 15 ++++++++++++--- freqtrade/persistence.py | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 58a020e12..566c5a41b 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -66,14 +66,14 @@ def validate_pairs(pairs: List[str]) -> None: def buy(pair: str, rate: float, amount: float) -> str: if _CONF['dry_run']: - return 'dry_run' + return 'dry_run_buy' return _API.buy(pair, rate, amount) def sell(pair: str, rate: float, amount: float) -> str: if _CONF['dry_run']: - return 'dry_run' + return 'dry_run_sell' return _API.sell(pair, rate, amount) @@ -106,7 +106,16 @@ def cancel_order(order_id: str) -> None: def get_order(order_id: str) -> Dict: if _CONF['dry_run']: - return {} + return { + 'id': 'dry_run_sell', + 'type': 'LIMIT_SELL', + 'pair': 'mocked', + 'opened': arrow.utcnow().datetime, + 'rate': 0.07256060, + 'amount': 206.43811673387373, + 'remaining': 0.0, + 'closed': arrow.utcnow().datetime, + } return _API.get_order(order_id) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index d16859724..d33f494e9 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -78,7 +78,7 @@ class Trade(Base): :param order: order retrieved by exchange.get_order() :return: None """ - if not order.get('closed'): + if not order['closed']: return logger.debug('Updating trade (id=%d) ...', self.id) From 41510fdb32426c49343ade14c034e28b087162ae Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 01:25:48 +0100 Subject: [PATCH 09/16] add dry_run for get_balance --- freqtrade/exchange/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 566c5a41b..4807ff295 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -86,6 +86,9 @@ def get_balance(currency: str) -> float: def get_balances(): + if _CONF['dry_run']: + return [] + return _API.get_balances() From cd18629433668edbff0fcfb3fc81b6ea876fa7ff Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 02:20:55 +0100 Subject: [PATCH 10/16] add fee to sqlalchemy model and respecting it in calc_profit --- freqtrade/main.py | 9 ++++++--- freqtrade/persistence.py | 10 ++++------ freqtrade/tests/test_main.py | 2 +- freqtrade/tests/test_telegram.py | 6 +++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index f96718c6b..ae8561c96 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -182,11 +182,12 @@ def create_trade(stake_amount: float) -> Optional[Trade]: else: return None + # Calculate amount and subtract fee + fee = exchange.get_fee() buy_limit = get_target_bid(exchange.get_ticker(pair)) - # TODO: apply fee to amount and also consider it for profit calculations - amount = stake_amount / buy_limit - order_id = exchange.buy(pair, buy_limit, amount) + amount = (1 - fee) * stake_amount / buy_limit + order_id = exchange.buy(pair, buy_limit, amount) # Create trade entity and return message = '*{}:* Buying [{}]({}) with limit `{:f}`'.format( exchange.get_name().upper(), @@ -196,9 +197,11 @@ def create_trade(stake_amount: float) -> Optional[Trade]: ) logger.info(message) telegram.send_msg(message) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL return Trade(pair=pair, stake_amount=stake_amount, amount=amount, + fee=fee * 2, open_rate=buy_limit, open_date=datetime.utcnow(), exchange=exchange.get_name().upper(), diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index d33f494e9..a30bf9bcf 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -54,10 +54,11 @@ class Trade(Base): exchange = Column(String, nullable=False) pair = Column(String, nullable=False) is_open = Column(Boolean, nullable=False, default=True) + fee = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) close_rate = Column(Float) close_profit = Column(Float) - stake_amount = Column(Float, name='btc_amount', nullable=False) + stake_amount = Column(Float, nullable=False) amount = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) @@ -95,14 +96,11 @@ class Trade(Base): self.open_order_id = None - # Flush changes - Trade.session.flush() - def calc_profit(self, rate: float=None) -> float: """ - Calculates the profit in percentage. + 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 """ - return (rate or self.close_rate - self.open_rate) / self.open_rate + return (rate or self.close_rate - self.open_rate) / self.open_rate - self.fee diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index c098c3dc0..cddf61f52 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -128,7 +128,7 @@ def test_handle_trade(conf, mocker): }) assert trade.close_rate == 0.17256061 - assert trade.close_profit == 1.3748724900565639 + assert trade.close_profit == 1.369872490056564 assert trade.close_date is not None diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 109374321..7024aa282 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -152,8 +152,8 @@ def test_profit_handle(conf, update, mocker): _profit(bot=MagicBot(), update=update) assert msg_mock.call_count == 2 - assert '*ROI:* `1.582013 (10.55%)`' in msg_mock.call_args_list[-1][0][0] - assert 'Best Performing:* `BTC_ETH: 10.55%`' in msg_mock.call_args_list[-1][0][0] + assert '*ROI:* `1.507013 (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): @@ -249,7 +249,7 @@ def test_performance_handle(conf, update, mocker): _performance(bot=MagicBot(), update=update) assert msg_mock.call_count == 2 assert 'Performance' in msg_mock.call_args_list[-1][0][0] - assert 'BTC_ETH\t10.55%' in msg_mock.call_args_list[-1][0][0] + assert 'BTC_ETH\t10.05%' in msg_mock.call_args_list[-1][0][0] def test_start_handle(conf, update, mocker): From 99724e2458314c6020154913d7ac6a6495459886 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 02:51:10 +0100 Subject: [PATCH 11/16] use Decimal for profit calculation --- freqtrade/persistence.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a30bf9bcf..15e7fa126 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -3,6 +3,7 @@ from datetime import datetime from typing import Optional, Dict import arrow +from decimal import Decimal, getcontext from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session @@ -103,4 +104,6 @@ class Trade(Base): If rate is not set self.close_rate will be used :return: profit in percentage as float """ - return (rate or self.close_rate - self.open_rate) / self.open_rate - self.fee + getcontext().prec = 8 + return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate)) + / Decimal(self.open_rate) - Decimal(self.fee)) From 1daeed4a527ff719a540c89df7fba30ba5e6df64 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 02:59:11 +0100 Subject: [PATCH 12/16] fix assert --- freqtrade/tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index cddf61f52..b49a685b6 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -128,7 +128,7 @@ def test_handle_trade(conf, mocker): }) assert trade.close_rate == 0.17256061 - assert trade.close_profit == 1.369872490056564 + assert trade.close_profit == 1.3698725 assert trade.close_date is not None From 08a1d3ca1dc9d2f75536cc4e34a34d80c334d128 Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 03:27:08 +0100 Subject: [PATCH 13/16] pylint changes --- .pylintrc | 1 + freqtrade/persistence.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.pylintrc b/.pylintrc index eddcbf7a5..a876357c4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,3 @@ [BASIC] good-names=logger +ignore=vendor \ No newline at end of file diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 15e7fa126..c122d830e 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -1,9 +1,9 @@ import logging from datetime import datetime +from decimal import Decimal, getcontext from typing import Optional, Dict import arrow -from decimal import Decimal, getcontext from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm.scoping import scoped_session @@ -97,7 +97,7 @@ class Trade(Base): self.open_order_id = None - def calc_profit(self, rate: float=None) -> float: + def calc_profit(self, rate: Optional[float] = None) -> float: """ Calculates the profit in percentage (including fee). :param rate: rate to compare with (optional). From 460dfa1031e3da3bdb6317c017c175f2a639709f Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 17:39:32 +0100 Subject: [PATCH 14/16] fix percentage formating in execute_sell --- freqtrade/main.py | 4 ++-- freqtrade/tests/test_telegram.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index ae8561c96..72c8a1f3e 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -92,13 +92,13 @@ def execute_sell(trade: Trade, limit: float) -> None: trade.open_order_id = order_id trade.close_date = datetime.utcnow() - exp_profit = round(trade.calc_profit(limit), 2) + fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2) message = '*{}:* Selling [{}]({}) with limit `{:f} (profit: ~{}%)`'.format( trade.exchange, trade.pair.replace('_', '/'), exchange.get_pair_detail_url(trade.pair), limit, - exp_profit + fmt_exp_profit ) logger.info(message) telegram.send_msg(message) diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index 7024aa282..4270ba21c 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -195,7 +195,7 @@ def test_forcesell_handle(conf, update, mocker): assert msg_mock.call_count == 2 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.072561 (profit: ~-0.5%)' in msg_mock.call_args_list[-1][0][0] def test_performance_handle(conf, update, mocker): From c37df0e70d31e8def45f597086f8b0a0e3ada422 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 3 Nov 2017 21:36:55 +0100 Subject: [PATCH 15/16] inform about mocked values with dry_run --- freqtrade/rpc/telegram.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 08396ca6f..9f2559000 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -200,6 +200,7 @@ def _profit(bot: Bot, update: Update) -> None: *Latest Trade opened:* `{latest_trade_date}` *Avg. Duration:* `{avg_duration}` *Best Performing:* `{best_pair}: {best_rate:.2f}%` +{dry_run_info} """.format( profit_btc=round(sum(profit_amounts), 8), profit=round(sum(profits) * 100, 2), @@ -209,6 +210,8 @@ def _profit(bot: Bot, update: Update) -> None: avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], best_pair=bp_pair, best_rate=round(bp_rate * 100, 2), + dry_run_info='\n*NOTE:* These values are mocked because *dry_run* is enabled!' + if _CONF['dry_run'] else '' ) send_msg(markdown_msg, bot=bot) @@ -325,7 +328,11 @@ def _performance(bot: Bot, update: Update) -> None: profit=round(rate * 100, 2) ) for i, (pair, rate) in enumerate(pair_rates)) - message = 'Performance:\n{}\n'.format(stats) + message = 'Performance:\n{}\n{}'.format( + stats, + 'NOTE: These values are mocked because dry_run is enabled.' + if _CONF['dry_run'] else '' + ) logger.debug(message) send_msg(message, parse_mode=ParseMode.HTML) From 04342acff18125748dcc58edbf4db91f50f8a627 Mon Sep 17 00:00:00 2001 From: gcarq Date: Fri, 3 Nov 2017 21:37:20 +0100 Subject: [PATCH 16/16] fix typo --- freqtrade/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index 72c8a1f3e..7c89da55f 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -163,7 +163,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]: # Check if stake_amount is fulfilled if exchange.get_balance(_CONF['stake_currency']) < stake_amount: 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