don't spend the whole coin balance when selling

This commit is contained in:
gcarq 2017-11-01 00:22:38 +01:00
parent 9b9d0250f7
commit 0e96197a94
3 changed files with 111 additions and 91 deletions

View File

@ -80,23 +80,25 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
return False return False
def execute_sell(trade: Trade, current_rate: float) -> None: def execute_sell(trade: Trade, limit: float) -> None:
""" """
Executes a sell for the given trade and current rate Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param current_rate: current rate :param limit: limit rate for the sell order
:return: None :return: None
""" """
# Get available balance # Execute sell and update trade record
currency = trade.pair.split('_')[1] order_id = exchange.sell(str(trade.pair), limit, trade.amount)
balance = exchange.get_balance(currency) trade.open_order_id = order_id
profit = trade.exec_sell_order(current_rate, balance) trade.close_date = datetime.utcnow()
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
exp_profit = round(trade.calc_profit(limit), 2)
message = '*{}:* Selling [{}]({}) with limit `{:f} (profit: ~{}%)`'.format(
trade.exchange, trade.exchange,
trade.pair.replace('_', '/'), trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair), exchange.get_pair_detail_url(trade.pair),
trade.close_rate, limit,
round(profit, 2) exp_profit
) )
logger.info(message) logger.info(message)
telegram.send_msg(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 Based an earlier trade and current price and configuration, decides whether bot should sell
:return True if bot should sell at current rate :return True if bot should sell at current rate
""" """
current_profit = (current_rate - trade.open_rate) / trade.open_rate current_profit = trade.calc_profit(current_rate)
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']): if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
for duration, threshold in sorted(_CONF['minimal_roi'].items()): for duration, threshold in sorted(_CONF['minimal_roi'].items()):
duration, threshold = float(duration), float(threshold)
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (current_time - trade.open_date).total_seconds() / 60 time_diff = (current_time - trade.open_date).total_seconds() / 60
if time_diff > duration and current_profit > threshold: if time_diff > float(duration) and current_profit > threshold:
return True return True
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0) logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
@ -182,25 +182,24 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
else: else:
return None return None
open_rate = get_target_bid(exchange.get_ticker(pair)) buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = stake_amount / open_rate # TODO: apply fee to amount and also consider it for profit calculations
order_id = exchange.buy(pair, open_rate, amount) amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)
# Create trade entity and return # Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format( message = '*{}:* Buying [{}]({}) with limit `{:f}`'.format(
exchange.EXCHANGE.name.upper(), exchange.get_name().upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
open_rate buy_limit
) )
logger.info(message) logger.info(message)
telegram.send_msg(message) telegram.send_msg(message)
return Trade(pair=pair, return Trade(pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
open_rate=open_rate,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
amount=amount, exchange=exchange.get_name().upper(),
exchange=exchange.EXCHANGE.name.upper(),
open_order_id=order_id, open_order_id=order_id,
is_open=True) is_open=True)

View File

@ -1,15 +1,18 @@
import logging
from datetime import datetime 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 import Boolean, Column, DateTime, Float, Integer, String, create_engine
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker from sqlalchemy.orm.session import sessionmaker
from freqtrade import exchange logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
_CONF = {} _CONF = {}
Base = declarative_base() Base = declarative_base()
@ -51,44 +54,55 @@ class Trade(Base):
exchange = Column(String, nullable=False) exchange = Column(String, nullable=False)
pair = Column(String, nullable=False) pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True) is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False) open_rate = Column(Float)
close_rate = Column(Float) close_rate = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
stake_amount = Column(Float, name='btc_amount', nullable=False) stake_amount = Column(Float, name='btc_amount', nullable=False)
amount = Column(Float, nullable=False) amount = Column(Float)
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)
open_order_id = Column(String) open_order_id = Column(String)
def __repr__(self): def __repr__(self):
if self.is_open:
open_since = 'closed'
else:
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format( return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
self.id, self.id,
self.pair, self.pair,
self.amount, self.amount,
self.open_rate, self.open_rate,
open_since arrow.get(self.open_date).humanize() if self.is_open else 'closed'
) )
def exec_sell_order(self, rate: float, amount: float) -> float: def update(self, order: Dict) -> None:
""" """
Executes a sell for the given trade and updated the entity. Updates this entity with amount and actual open/close rates.
:param rate: rate to sell for :param order: order retrieved by exchange.get_order()
:param amount: amount to sell :return: None
:return: current profit as percentage
""" """
profit = 100 * ((rate - self.open_rate) / self.open_rate) if not order['closed']:
return
# Execute sell and update trade record logger.debug('Updating trade (id=%d) ...', self.id)
order_id = exchange.sell(str(self.pair), rate, amount) if order['type'] == 'LIMIT_BUY':
self.close_rate = rate # Set open rate and actual amount
self.close_profit = profit self.open_rate = order['rate']
self.close_date = datetime.utcnow() self.amount = order['amount']
self.open_order_id = order_id elif order['type'] == 'LIMIT_SELL':
# Set close rate and set actual profit
self.close_rate = order['rate']
self.close_profit = self.calc_profit()
else:
raise ValueError('Unknown order type: {}'.format(order['type']))
self.open_order_id = None
# Flush changes # Flush changes
Trade.session.flush() 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

View File

@ -114,18 +114,16 @@ def _status(bot: Bot, update: Update) -> None:
if get_state() != State.RUNNING: if get_state() != State.RUNNING:
send_msg('*Status:* `trader is not running`', bot=bot) send_msg('*Status:* `trader is not running`', bot=bot)
elif not trades: elif not trades:
send_msg('*Status:* `no active order`', bot=bot) send_msg('*Status:* `no active trade`', bot=bot)
else: else:
for trade in trades: for trade in trades:
order = exchange.get_order(trade.open_order_id)
if trade.open_rate:
# calculate profit and send message to user # calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) current_profit = trade.calc_profit(current_rate)
orders = exchange.get_open_orders(trade.pair)
orders = [o for o in orders if o['id'] == trade.open_order_id]
order = orders[0] if orders else None
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) if trade.close_profit else None
message = """ message = """
*Trade ID:* `{trade_id}` *Trade ID:* `{trade_id}`
@ -148,8 +146,27 @@ def _status(bot: Bot, update: Update) -> None:
current_rate=current_rate, current_rate=current_rate,
amount=round(trade.amount, 8), amount=round(trade.amount, 8),
close_profit=fmt_close_profit, close_profit=fmt_close_profit,
current_profit=round(current_profit, 2), current_profit=round(current_profit * 100, 2),
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None, open_order='{} ({})'.format(
order['remaining'], order['type']
) if order else None,
)
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) send_msg(message, bot=bot)
@ -169,6 +186,8 @@ def _profit(bot: Bot, update: Update) -> None:
profits = [] profits = []
durations = [] durations = []
for trade in trades: for trade in trades:
if not trade.open_rate:
continue
if trade.close_date: if trade.close_date:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if trade.close_profit: if trade.close_profit:
@ -176,9 +195,9 @@ def _profit(bot: Bot, update: Update) -> None:
else: else:
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate) profit = trade.calc_profit(current_rate)
profit_amounts.append((profit / 100) * trade.stake_amount) profit_amounts.append(profit * trade.stake_amount)
profits.append(profit) profits.append(profit)
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \ best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
@ -193,7 +212,7 @@ def _profit(bot: Bot, update: Update) -> None:
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
markdown_msg = """ markdown_msg = """
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)` *ROI:* `{profit_btc:.6f} ({profit:.2f}%)`
*Trade Count:* `{trade_count}` *Trade Count:* `{trade_count}`
*First Trade opened:* `{first_trade_date}` *First Trade opened:* `{first_trade_date}`
*Latest Trade opened:* `{latest_trade_date}` *Latest Trade opened:* `{latest_trade_date}`
@ -201,13 +220,13 @@ def _profit(bot: Bot, update: Update) -> None:
*Best Performing:* `{best_pair}: {best_rate:.2f}%` *Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format( """.format(
profit_btc=round(sum(profit_amounts), 8), profit_btc=round(sum(profit_amounts), 8),
profit=round(sum(profits), 2), profit=round(sum(profits) * 100, 2),
trade_count=len(trades), trade_count=len(trades),
first_trade_date=arrow.get(trades[0].open_date).humanize(), first_trade_date=arrow.get(trades[0].open_date).humanize(),
latest_trade_date=arrow.get(trades[-1].open_date).humanize(), latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0], avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
best_pair=bp_pair, best_pair=bp_pair,
best_rate=round(bp_rate, 2), best_rate=round(bp_rate * 100, 2),
) )
send_msg(markdown_msg, bot=bot) send_msg(markdown_msg, bot=bot)
@ -291,20 +310,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
return return
# Get current rate # Get current rate
current_rate = exchange.get_ticker(trade.pair)['bid'] current_rate = exchange.get_ticker(trade.pair)['bid']
# Get available balance from freqtrade.main import execute_sell
currency = trade.pair.split('_')[1] execute_sell(trade, current_rate)
balance = exchange.get_balance(currency)
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
round(profit, 2)
)
logger.info(message)
send_msg(message)
except ValueError: except ValueError:
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`') send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
@ -333,7 +340,7 @@ def _performance(bot: Bot, update: Update) -> None:
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format( stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
index=i + 1, index=i + 1,
pair=pair, pair=pair,
profit=round(rate, 2) profit=round(rate * 100, 2)
) for i, (pair, rate) in enumerate(pair_rates)) ) for i, (pair, rate) in enumerate(pair_rates))
message = '<b>Performance:</b>\n{}\n'.format(stats) message = '<b>Performance:</b>\n{}\n'.format(stats)