From 0e96197a94404caf6d381637606ac53b4433644b Mon Sep 17 00:00:00 2001 From: gcarq Date: Wed, 1 Nov 2017 00:22:38 +0100 Subject: [PATCH] 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)