Merge pull request #89 from gcarq/feature/take-fees-into-account

take fees into account & sell amount equal to amount purchased
This commit is contained in:
Michael Egger 2017-11-03 21:47:46 +01:00 committed by GitHub
commit 7cc8533b8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 336 additions and 198 deletions

View File

@ -1,2 +1,3 @@
[BASIC]
good-names=logger
ignore=vendor

View File

@ -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)

View File

@ -1,6 +1,6 @@
import enum
import logging
from typing import List
from typing import List, Dict
import arrow
@ -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,86 @@ 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 'dry_run_buy'
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 'dry_run_sell'
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()
if _CONF['dry_run']:
return []
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 {
'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 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

View File

@ -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('_', '-'))

View File

@ -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:
"""

View File

@ -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):
# 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:
@ -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()
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),
trade.close_rate,
round(profit, 2)
limit,
fmt_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)
@ -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()):
@ -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
@ -182,25 +182,29 @@ 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)
# Calculate amount and subtract fee
fee = exchange.get_fee()
buy_limit = get_target_bid(exchange.get_ticker(pair))
amount = (1 - fee) * 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)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
return Trade(pair=pair,
stake_amount=stake_amount,
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount,
exchange=exchange.EXCHANGE.name.upper(),
fee=fee * 2,
open_rate=buy_limit,
open_date=datetime.utcnow(),
exchange=exchange.get_name().upper(),
open_order_id=order_id,
is_open=True)
@ -266,7 +270,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(

View File

@ -1,15 +1,19 @@
import logging
from datetime import datetime
from typing import Optional
from decimal import Decimal, getcontext
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()
@ -25,9 +29,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))
@ -51,44 +55,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)
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)
amount = Column(Float, nullable=False)
stake_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':
# Update 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']))
# Flush changes
Trade.session.flush()
return profit
self.open_order_id = None
def calc_profit(self, rate: Optional[float] = None) -> float:
"""
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
"""
getcontext().prec = 8
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
/ Decimal(self.open_rate) - Decimal(self.fee))

View File

@ -114,18 +114,15 @@ 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:
order = exchange.get_order(trade.open_order_id)
# 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
current_profit = trade.calc_profit(current_rate)
fmt_close_profit = '{:.2f}%'.format(
round(trade.close_profit, 2)
round(trade.close_profit * 100, 2)
) if trade.close_profit else None
message = """
*Trade ID:* `{trade_id}`
@ -148,8 +145,10 @@ def _status(bot: Bot, update: Update) -> None:
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,
current_profit=round(current_profit * 100, 2),
open_order='{} ({})'.format(
order['remaining'], order['type']
) if order else None,
)
send_msg(message, bot=bot)
@ -169,6 +168,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 +177,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,21 +194,24 @@ 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}`
*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), 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),
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)
@ -291,20 +295,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,10 +325,14 @@ 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)
message = '<b>Performance:</b>\n{}\n{}'.format(
stats,
'<b>NOTE:</b> These values are mocked because <b>dry_run</b> is enabled.'
if _CONF['dry_run'] else ''
)
logger.debug(message)
send_msg(message, parse_mode=ParseMode.HTML)

View File

@ -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

View File

@ -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

View File

@ -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.3698725
assert trade.close_date is not None
assert trade.open_order_id == 'dry_run'
def test_close_trade(conf, mocker):

View File

@ -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

View File

@ -46,6 +46,7 @@ def conf():
validate(configuration, CONF_SCHEMA)
return configuration
@pytest.fixture
def update():
_update = Update(0)
@ -78,8 +79,26 @@ 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 '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
@ -95,14 +114,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 +152,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.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):
@ -132,6 +174,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()
@ -140,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):
@ -161,10 +216,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 +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 100.00%' in msg_mock.call_args_list[-1][0][0]
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
def test_start_handle(conf, update, mocker):