don't spend the whole coin balance when selling
This commit is contained in:
		| @@ -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) | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 <trade_id>`') | ||||
| @@ -333,7 +340,7 @@ def _performance(bot: Bot, update: Update) -> None: | ||||
|     stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format( | ||||
|         index=i + 1, | ||||
|         pair=pair, | ||||
|         profit=round(rate, 2) | ||||
|         profit=round(rate * 100, 2) | ||||
|     ) for i, (pair, rate) in enumerate(pair_rates)) | ||||
|  | ||||
|     message = '<b>Performance:</b>\n{}\n'.format(stats) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user