Fix the fee calculation

This commit is contained in:
Gerald Lonlas 2017-12-17 13:07:56 -08:00
parent 642422d5c4
commit d613d63fdc
9 changed files with 344 additions and 83 deletions

View File

@ -51,7 +51,7 @@ class Bittrex(Exchange):
@property @property
def fee(self) -> float: def fee(self) -> float:
# See https://bittrex.com/fees # See https://bittrex.com/fees
return 0.0025 return 0.0025 #0.25%
def buy(self, pair: str, rate: float, amount: float) -> str: def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate) data = _API.buy_limit(pair.replace('_', '-'), amount, rate)

View File

@ -118,13 +118,14 @@ def execute_sell(trade: Trade, limit: float) -> None:
order_id = exchange.sell(str(trade.pair), limit, trade.amount) order_id = exchange.sell(str(trade.pair), limit, trade.amount)
trade.open_order_id = order_id trade.open_order_id = order_id
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2) fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%)`'.format( rpc.send_msg('*{}:* Selling [{}]({}) with limit `{:.8f} (profit: ~{:.2f}%, {:.8f})`'.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),
limit, limit,
fmt_exp_profit fmt_exp_profit,
trade.calc_profit(rate=limit),
)) ))
Trade.session.flush() Trade.session.flush()
@ -134,7 +135,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
Based an earlier trade and current price and ROI configuration, decides whether bot should sell Based an earlier trade and current price and ROI 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 = trade.calc_profit(current_rate) current_profit = trade.calc_profit_percent(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
@ -145,7 +146,7 @@ def min_roi_reached(trade: Trade, current_rate: float, current_time: datetime) -
if time_diff > float(duration) and current_profit > threshold: 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%%)', float(current_profit) * 100.0)
return False return False
@ -233,7 +234,7 @@ def create_trade(stake_amount: float) -> bool:
pair=pair, pair=pair,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=amount, amount=amount,
fee=exchange.get_fee() * 2, fee=exchange.get_fee(),
open_rate=buy_limit, open_rate=buy_limit,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(), exchange=exchange.get_name().upper(),

View File

@ -99,7 +99,7 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
open_rate=row.close, open_rate=row.close,
open_date=row.date, open_date=row.date,
amount=config['stake_amount'], amount=config['stake_amount'],
fee=exchange.get_fee() * 2 fee=exchange.get_fee()
) )
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
@ -109,7 +109,7 @@ def backtest(config: Dict, processed: Dict[str, DataFrame],
trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1 trade_count_lock[row2.date] = trade_count_lock.get(row2.date, 0) + 1
if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1: if min_roi_reached(trade, row2.close, row2.date) or row2.sell == 1:
current_profit = trade.calc_profit(row2.close) current_profit = trade.calc_profit_percent(row2.close)
lock_pair_until = row2.Index lock_pair_until = row2.Index
trades.append((pair, current_profit, row2.Index - row.Index)) trades.append((pair, current_profit, row2.Index - row.Index))

View File

@ -92,10 +92,12 @@ class Trade(_DECL_BASE):
return return
logger.info('Updating trade (id=%d) ...', self.id) logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'LIMIT_BUY': if order['type'] == 'LIMIT_BUY':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = order['rate'] self.open_rate = Decimal(order['rate'])
self.amount = order['amount'] self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self) logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None self.open_order_id = None
elif order['type'] == 'LIMIT_SELL': elif order['type'] == 'LIMIT_SELL':
@ -109,8 +111,8 @@ class Trade(_DECL_BASE):
Sets close_rate to the given rate, calculates total profit Sets close_rate to the given rate, calculates total profit
and marks trade as closed and marks trade as closed
""" """
self.close_rate = rate self.close_rate = Decimal(rate)
self.close_profit = self.calc_profit() self.close_profit = self.calc_profit_percent()
self.close_date = datetime.utcnow() self.close_date = datetime.utcnow()
self.is_open = False self.is_open = False
self.open_order_id = None self.open_order_id = None
@ -119,7 +121,54 @@ class Trade(_DECL_BASE):
self self
) )
def calc_profit(self, rate: Optional[float] = None) -> float: def calc_open_trade_price(self, fee: Optional[float] = None) -> float:
"""
Calculate the open_rate in BTC
:param fee: fee to use on the open rate (optional).
If rate is not set self.fee will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee)
return float(buy_trade + fees)
def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float:
"""
Calculate the close_rate in BTC
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: Price in BTC of the open trade
"""
getcontext().prec = 8
if rate is None and not self.close_rate:
return 0.0
sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate))
fees = sell_trade * Decimal(fee or self.fee)
return float(sell_trade - fees)
def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float:
"""
Calculate the profit in BTC between Close and Open trade
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:return: profit in BTC as float
"""
open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format(close_trade_price - open_trade_price))
def calc_profit_percent(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float:
""" """
Calculates the profit in percentage (including fee). Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
@ -127,5 +176,11 @@ class Trade(_DECL_BASE):
:return: profit in percentage as float :return: profit in percentage as float
""" """
getcontext().prec = 8 getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee)) open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price(
rate=Decimal(rate or self.close_rate),
fee=Decimal(fee or self.fee)
)
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1))

View File

@ -1,7 +1,7 @@
import logging import logging
import re import re
from datetime import timedelta, date
from decimal import Decimal from decimal import Decimal
from datetime import timedelta, date, datetime
from typing import Callable, Any from typing import Callable, Any
import arrow import arrow
@ -139,7 +139,7 @@ def _status(bot: Bot, update: Update) -> None:
order = exchange.get_order(trade.open_order_id) order = exchange.get_order(trade.open_order_id)
# 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 = trade.calc_profit(current_rate) current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit * 100, 2) round(trade.close_profit * 100, 2)
) if trade.close_profit else None ) if trade.close_profit else None
@ -196,7 +196,7 @@ def _status_table(bot: Bot, update: Update) -> None:
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}'.format(100 * trade.calc_profit(current_rate)) '{:.2f}'.format(100 * trade.calc_profit_percent(current_rate))
]) ])
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
@ -218,7 +218,7 @@ def _daily(bot: Bot, update: Update) -> None:
:param update: message update :param update: message update
:return: None :return: None
""" """
today = date.today().toordinal() today = datetime.utcnow().toordinal()
profit_days = {} profit_days = {}
try: try:
@ -234,9 +234,13 @@ def _daily(bot: Bot, update: Update) -> None:
# need to query between day+1 and day-1 # need to query between day+1 and day-1
nextdate = date.fromordinal(today-day+1) nextdate = date.fromordinal(today-day+1)
prevdate = date.fromordinal(today-day-1) prevdate = date.fromordinal(today-day-1)
trades = Trade.query.filter(between(Trade.close_date, prevdate, nextdate)).all() trades = Trade.query \
curdayprofit = sum(trade.close_profit * trade.stake_amount for trade in trades) .filter(Trade.is_open.is_(False)) \
profit_days[date.fromordinal(today-day)] = format(curdayprofit, '.8f') .filter(between(Trade.close_date, prevdate, nextdate)) \
.order_by(Trade.close_date)\
.all()
curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[date.fromordinal(today - day)] = format(curdayprofit, '.8f')
stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()] stats = [[key, str(value) + ' BTC'] for key, value in profit_days.items()]
stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple') stats = tabulate(stats, headers=['Day', 'Profit'], tablefmt='simple')
@ -257,9 +261,9 @@ def _profit(bot: Bot, update: Update) -> None:
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_btc = [] profit_all_btc = []
profit_all = [] profit_all_percent = []
profit_btc_closed = [] profit_btc_closed = []
profit_closed = [] profit_closed_percent = []
durations = [] durations = []
for trade in trades: for trade in trades:
@ -271,16 +275,16 @@ def _profit(bot: Bot, update: Update) -> None:
durations.append((trade.close_date - trade.open_date).total_seconds()) durations.append((trade.close_date - trade.open_date).total_seconds())
if not trade.is_open: if not trade.is_open:
profit = trade.close_profit profit_percent = trade.calc_profit_percent()
profit_btc_closed.append(Decimal(trade.close_rate) - Decimal(trade.open_rate)) profit_btc_closed.append(trade.calc_profit())
profit_closed.append(profit) profit_closed_percent.append(profit_percent)
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 = trade.calc_profit(current_rate) profit_percent = trade.calc_profit_percent(rate=current_rate)
profit_all_btc.append(Decimal(trade.close_rate or current_rate) - Decimal(trade.open_rate)) profit_all_btc.append(trade.calc_profit(rate=Decimal(trade.close_rate or current_rate)))
profit_all.append(profit) profit_all_percent.append(profit_percent)
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')) \
.filter(Trade.is_open.is_(False)) \ .filter(Trade.is_open.is_(False)) \
@ -294,8 +298,8 @@ def _profit(bot: Bot, update: Update) -> None:
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
markdown_msg = """ markdown_msg = """
*ROI Trade closed:* `{profit_closed_btc:.8f} BTC ({profit_closed:.2f}%)` *ROI Trade closed:* `{profit_closed_btc:.8f} BTC ({profit_closed_percent:.2f}%)`
*ROI All trades:* `{profit_all_btc:.8f} BTC ({profit_all:.2f}%)` *ROI All trades:* `{profit_all_btc:.8f} BTC ({profit_all_percent:.2f}%)`
*Total Trade Count:* `{trade_count}` *Total 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}`
@ -303,9 +307,9 @@ def _profit(bot: Bot, update: Update) -> None:
*Best Performing:* `{best_pair}: {best_rate:.2f}%` *Best Performing:* `{best_pair}: {best_rate:.2f}%`
""".format( """.format(
profit_closed_btc=round(sum(profit_btc_closed), 8), profit_closed_btc=round(sum(profit_btc_closed), 8),
profit_closed=round(sum(profit_closed) * 100, 2), profit_closed_percent=round(sum(profit_closed_percent) * 100, 2),
profit_all_btc=round(sum(profit_all_btc), 8), profit_all_btc=round(sum(profit_all_btc), 8),
profit_all=round(sum(profit_all) * 100, 2), profit_all_percent=round(sum(profit_all_percent) * 100, 2),
trade_count=len(trades), 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(),

View File

@ -15,7 +15,7 @@ def default_conf():
configuration = { configuration = {
"max_open_trades": 1, "max_open_trades": 1,
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.001,
"dry_run": True, "dry_run": True,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
@ -61,11 +61,26 @@ def update():
@pytest.fixture @pytest.fixture
def ticker(): def ticker():
return MagicMock(return_value={ return MagicMock(return_value={
'bid': 0.07256061, 'bid': 0.00001098,
'ask': 0.072661, 'ask': 0.00001099,
'last': 0.07256061, 'last': 0.00001098,
}) })
@pytest.fixture
def ticker_sell_up():
return MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172,
})
@pytest.fixture
def ticker_sell_down():
return MagicMock(return_value={
'bid': 0.00001044,
'ask': 0.00001043,
'last': 0.00001044,
})
@pytest.fixture @pytest.fixture
def health(): def health():
@ -104,8 +119,8 @@ def limit_buy_order():
'type': 'LIMIT_BUY', 'type': 'LIMIT_BUY',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': datetime.utcnow(),
'rate': 0.07256061, 'rate': 0.00001099,
'amount': 206.43811673387373, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': datetime.utcnow(),
} }
@ -118,8 +133,8 @@ def limit_sell_order():
'type': 'LIMIT_SELL', 'type': 'LIMIT_SELL',
'pair': 'mocked', 'pair': 'mocked',
'opened': datetime.utcnow(), 'opened': datetime.utcnow(),
'rate': 0.0802134, 'rate': 0.00001173,
'amount': 206.43811673387373, 'amount': 90.99181073,
'remaining': 0.0, 'remaining': 0.0,
'closed': datetime.utcnow(), 'closed': datetime.utcnow(),
} }
@ -155,4 +170,4 @@ def ticker_history():
"T": "2017-11-26T09:00:00", "T": "2017-11-26T09:00:00",
"BV": 0.7039405 "BV": 0.7039405
} }
] ]

View File

@ -40,8 +40,8 @@ def test_process_trade_creation(default_conf, ticker, health, mocker):
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
assert trade.open_rate == 0.072661 assert trade.open_rate == 0.00001099
assert trade.amount == 0.6881270557795791 assert trade.amount == 90.99181073703367
def test_process_exchange_failures(default_conf, ticker, health, mocker): def test_process_exchange_failures(default_conf, ticker, health, mocker):
@ -115,11 +115,11 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist']) whitelist = copy.deepcopy(default_conf['exchange']['pair_whitelist'])
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade is not None assert trade is not None
assert trade.stake_amount == 15.0 assert trade.stake_amount == 0.001
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == Exchanges.BITTREX.name assert trade.exchange == Exchanges.BITTREX.name
@ -127,8 +127,8 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker):
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order) trade.update(limit_buy_order)
assert trade.open_rate == 0.07256061 assert trade.open_rate == 0.00001099
assert trade.amount == 206.43811673387373 assert trade.amount == 90.99181073
assert whitelist == default_conf['exchange']['pair_whitelist'] assert whitelist == default_conf['exchange']['pair_whitelist']
@ -186,14 +186,14 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
mocker.patch.multiple('freqtrade.main.exchange', mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={ get_ticker=MagicMock(return_value={
'bid': 0.17256061, 'bid': 0.00001172,
'ask': 0.172661, 'ask': 0.00001173,
'last': 0.17256061 'last': 0.00001172
}), }),
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
sell=MagicMock(return_value='mocked_limit_sell')) sell=MagicMock(return_value='mocked_limit_sell'))
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -207,8 +207,9 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, mocker):
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade.update(limit_sell_order) trade.update(limit_sell_order)
assert trade.close_rate == 0.0802134 assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.10046755 assert trade.close_profit == 0.06201057
assert trade.calc_profit() == 0.00006217
assert trade.close_date is not None assert trade.close_date is not None
@ -223,7 +224,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, mo
# Create trade and sell it # Create trade and sell it
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade

View File

@ -5,11 +5,32 @@ from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
def test_update(limit_buy_order, limit_sell_order): def test_update_with_bittrex(limit_buy_order, limit_sell_order):
"""
On this test we will buy and sell a crypto currency.
Buy
- Buy: 90.99181073 Crypto at 0.00001099 BTC (90.99181073*0.00001099 = 0.0009999 BTC)
- Buying fee: 0.25%
- Total cost of buy trade: 0.001002500 BTC ((90.99181073*0.00001099) + ((90.99181073*0.00001099)*0.0025))
Sell
- Sell: 90.99181073 Crypto at 0.00001173 BTC (90.99181073*0.00001173 = 0,00106733394 BTC)
- Selling fee: 0.25%
- Total cost of sell trade: 0.001064666 BTC ((90.99181073*0.00001173) - ((90.99181073*0.00001173)*0.0025))
Profit/Loss: +0.000062166 BTC (Sell:0.001064666 - Buy:0.001002500)
Profit/Loss percentage: 0.0620 ((0.001064666/0.001002500)-1 = 6.20%)
:param limit_buy_order:
:param limit_sell_order:
:return:
"""
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
stake_amount=1.00, stake_amount=0.001,
fee=0.1, fee=0.0025,
exchange=Exchanges.BITTREX, exchange=Exchanges.BITTREX,
) )
assert trade.open_order_id is None assert trade.open_order_id is None
@ -20,18 +41,40 @@ def test_update(limit_buy_order, limit_sell_order):
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order) trade.update(limit_buy_order)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 0.07256061 assert trade.open_rate == 0.00001099
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date is None assert trade.close_date is None
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_sell_order) trade.update(limit_sell_order)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 0.07256061 assert trade.close_rate == 0.00001173
assert trade.close_profit == 0.00546755 assert trade.close_profit == 0.06201057
assert trade.close_date is not None assert trade.close_date is not None
def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'something'
trade.update(limit_buy_order)
assert trade.calc_open_trade_price() == 0.001002500
trade.update(limit_sell_order)
assert trade.calc_close_trade_price() == 0.0010646656
# Profit in BTC
assert trade.calc_profit() == 0.00006217
# Profit in percent
assert trade.calc_profit_percent() == 0.06201057
def test_update_open_order(limit_buy_order): def test_update_open_order(limit_buy_order):
trade = Trade( trade = Trade(
pair='BTC_ETH', pair='BTC_ETH',
@ -64,3 +107,103 @@ def test_update_invalid_order(limit_buy_order):
limit_buy_order['type'] = 'invalid' limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'): with pytest.raises(ValueError, match=r'Unknown order type'):
trade.update(limit_buy_order) trade.update(limit_buy_order)
def test_calc_open_trade_price(limit_buy_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the open rate price with the standard fee rate
assert trade.calc_open_trade_price() == 0.001002500
# Get the open rate price with a custom fee rate
assert trade.calc_open_trade_price(fee=0.003) == 0.001003000
def test_calc_close_trade_price(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get the close rate price with a custom close rate and a regular fee rate
assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318
# Get the close rate price with a custom close rate and a custom fee rate
assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704
# Test when we apply a Sell order, and ask price with a custom fee rate
trade.update(limit_sell_order)
assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972
def test_calc_profit(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Custom closing rate and regular fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234) == 0.00011753
# Lower than open rate
assert trade.calc_profit(rate=0.00000123) == -0.00089086
# Custom closing rate and custom fee rate
# Higher than open rate
assert trade.calc_profit(rate=0.00001234, fee=0.003) == 0.00011697
# Lower than open rate
assert trade.calc_profit(rate=0.00000123, fee=0.003) == -0.00089092
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit() == 0.00006217
# Test with a custom fee rate on the close trade
assert trade.calc_profit(fee=0.003) == 0.00006163
def test_calc_profit_percent(limit_buy_order, limit_sell_order):
trade = Trade(
pair='BTC_ETH',
stake_amount=0.001,
fee=0.0025,
exchange=Exchanges.BITTREX,
)
trade.open_order_id = 'profit_percent'
trade.update(limit_buy_order) # Buy @ 0.00001099
# Get percent of profit with a custom rate (Higher than open rate)
assert trade.calc_profit_percent(rate=0.00001234) == 0.1172387
# Get percent of profit with a custom rate (Lower than open rate)
assert trade.calc_profit_percent(rate=0.00000123) == -0.88863827
# Only custom fee without sell order applied
with pytest.raises(TypeError):
trade.calc_profit_percent(fee=0.003)
# Test when we apply a Sell order. Sell higher than open rate @ 0.00001173
trade.update(limit_sell_order)
assert trade.calc_profit_percent() == 0.06201057
# Test with a custom fee rate on the close trade
assert trade.calc_profit_percent(fee=0.003) == 0.0614782

View File

@ -102,7 +102,7 @@ def test_status_handle(default_conf, update, ticker, mocker):
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
# Trigger status while we have a fulfilled order for the open trade # Trigger status while we have a fulfilled order for the open trade
_status(bot=MagicMock(), update=update) _status(bot=MagicMock(), update=update)
@ -151,7 +151,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker):
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell_order, mocker): def test_profit_handle(default_conf, update, ticker, ticker_sell_up, limit_buy_order, limit_sell_order, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
msg_mock = MagicMock() msg_mock = MagicMock()
@ -171,7 +171,7 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
msg_mock.reset_mock() msg_mock.reset_mock()
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade # Simulate fulfilled LIMIT_BUY order for trade
@ -182,7 +182,10 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
assert 'no closed trade' in msg_mock.call_args_list[-1][0][0] assert 'no closed trade' in msg_mock.call_args_list[-1][0][0]
msg_mock.reset_mock() msg_mock.reset_mock()
# Simulate fulfilled LIMIT_SELL order for trade # Update the ticker with a market going up
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
trade.update(limit_sell_order) trade.update(limit_sell_order)
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
@ -190,11 +193,12 @@ def test_profit_handle(default_conf, update, ticker, limit_buy_order, limit_sell
_profit(bot=MagicMock(), update=update) _profit(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*ROI All trades:* `0.00765279 BTC (10.05%)`' in msg_mock.call_args_list[-1][0][0] assert '*ROI Trade closed:* `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0] assert '*ROI All trades:* `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0]
assert 'Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0]
def test_forcesell_handle(default_conf, update, ticker, mocker): def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True) mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
@ -208,7 +212,44 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first()
assert trade
# Increase the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up)
update.message.text = '/forcesell 1'
_forcesell(bot=MagicMock(), update=update)
assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.00001172 (profit: ~6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0]
def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
mocker.patch('freqtrade.main.get_signal', side_effect=lambda s, t: True)
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.rpc.telegram',
_CONF=default_conf,
init=MagicMock(),
send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker)
init(default_conf, create_engine('sqlite://'))
# Create some test data
create_trade(0.001)
## Decrease the price and sell it
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_down)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -218,7 +259,7 @@ def test_forcesell_handle(default_conf, update, ticker, mocker):
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0] assert 'Selling [BTC/ETH]' in rpc_mock.call_args_list[-1][0][0]
assert '0.07256061 (profit: ~-0.64%)' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044 (profit: ~-5.48%, -0.00005492)' in rpc_mock.call_args_list[-1][0][0]
def test_exec_forcesell_open_orders(default_conf, ticker, mocker): def test_exec_forcesell_open_orders(default_conf, ticker, mocker):
@ -260,7 +301,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
# Create some test data # Create some test data
for _ in range(4): for _ in range(4):
create_trade(15.0) create_trade(0.001)
rpc_mock.reset_mock() rpc_mock.reset_mock()
update.message.text = '/forcesell all' update.message.text = '/forcesell all'
@ -268,7 +309,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker):
assert rpc_mock.call_count == 4 assert rpc_mock.call_count == 4
for args in rpc_mock.call_args_list: for args in rpc_mock.call_args_list:
assert '0.07256061 (profit: ~-0.64%)' in args[0][0] assert '0.00001098 (profit: ~-0.59%, -0.00000591)' in args[0][0]
def test_forcesell_handle_invalid(default_conf, update, mocker): def test_forcesell_handle_invalid(default_conf, update, mocker):
@ -323,7 +364,7 @@ def test_performance_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -339,7 +380,8 @@ def test_performance_handle(
_performance(bot=MagicMock(), update=update) _performance(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Performance' in msg_mock.call_args_list[0][0][0] assert 'Performance' in msg_mock.call_args_list[0][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[0][0][0] assert '<code>BTC_ETH\t6.20%</code>' in msg_mock.call_args_list[0][0][0]
def test_daily_handle( def test_daily_handle(
@ -358,7 +400,7 @@ def test_daily_handle(
init(default_conf, create_engine('sqlite://')) init(default_conf, create_engine('sqlite://'))
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
@ -371,14 +413,14 @@ def test_daily_handle(
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
# try valid data # Try valid data
update.message.text = '/daily 7' update.message.text = '/daily 2'
_daily(bot=MagicMock(), update=update) _daily(bot=MagicMock(), update=update)
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert 'Daily' in msg_mock.call_args_list[0][0][0] assert 'Daily' in msg_mock.call_args_list[0][0][0]
assert str(date.today()) + ' 1.50701325 BTC' in msg_mock.call_args_list[0][0][0] assert str(datetime.utcnow().date()) + ' 0.00006217 BTC' in msg_mock.call_args_list[0][0][0]
# try invalid data # Try invalid data
msg_mock.reset_mock() msg_mock.reset_mock()
update_state(State.RUNNING) update_state(State.RUNNING)
update.message.text = '/daily -2' update.message.text = '/daily -2'
@ -409,7 +451,7 @@ def test_count_handle(default_conf, update, ticker, mocker):
update_state(State.RUNNING) update_state(State.RUNNING)
# Create some test data # Create some test data
create_trade(15.0) create_trade(0.001)
msg_mock.reset_mock() msg_mock.reset_mock()
_count(bot=MagicMock(), update=update) _count(bot=MagicMock(), update=update)