Merge remote-tracking branch 'upstream/develop' into StopLossSupport

This commit is contained in:
Gert Wohlgemuth 2018-05-19 12:58:56 -07:00
commit b01e9b3f2f
23 changed files with 429 additions and 151 deletions

View File

@ -4,3 +4,12 @@ Dockerfile
.dockerignore .dockerignore
config.json* config.json*
*.sqlite *.sqlite
.coveragerc
.eggs
.github
.pylintrc
.travis.yml
CONTRIBUTING.md
MANIFEST.in
README.md
freqtrade.service

View File

@ -3,6 +3,7 @@
"stake_currency": "BTC", "stake_currency": "BTC",
"stake_amount": 0.05, "stake_amount": 0.05,
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval" : "5m",
"dry_run": false, "dry_run": false,
"trailing_stop": { "trailing_stop": {
"positive" : 0.005 "positive" : 0.005

View File

@ -43,6 +43,10 @@ python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200
``` ```
Timerange doesn't work with live data. Timerange doesn't work with live data.
To plot trades stored in a database use `--db-url` argument:
```
python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH
```
## Plot profit ## Plot profit

View File

@ -32,9 +32,12 @@ CREATE TABLE trades (
exchange VARCHAR NOT NULL, exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL, pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL, is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL, fee_open FLOAT NOT NULL,
fee_close FLOAT NOT NULL,
open_rate FLOAT, open_rate FLOAT,
open_rate_requested FLOAT,
close_rate FLOAT, close_rate FLOAT,
close_rate_requested FLOAT,
close_profit FLOAT, close_profit FLOAT,
stake_amount FLOAT NOT NULL, stake_amount FLOAT NOT NULL,
amount FLOAT, amount FLOAT,
@ -71,13 +74,13 @@ WHERE id=31;
```sql ```sql
INSERT INSERT
INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>') VALUES ('BITTREX', 'BTC_<COIN>', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
``` ```
**Example:** **Example:**
```sql ```sql
INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000') INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) VALUES ('BITTREX', 'BTC_ETC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000')
``` ```
## Fix wrong fees in the table ## Fix wrong fees in the table

View File

@ -129,12 +129,8 @@ Day Profit BTC Profit USD
> **Version:** `0.14.3` > **Version:** `0.14.3`
### using proxy with telegram ### using proxy with telegram
in [freqtrade/freqtrade/rpc/telegram.py](https://github.com/gcarq/freqtrade/blob/develop/freqtrade/rpc/telegram.py) replace
``` ```
self._updater = Updater(token=self._config['telegram']['token'], workers=0) $ export HTTP_PROXY="http://addr:port"
``` $ export HTTPS_PROXY="http://addr:port"
$ freqtrade
with
```
self._updater = Updater(token=self._config['telegram']['token'], request_kwargs={'proxy_url': 'socks5://127.0.0.1:1080/'}, workers=0)
``` ```

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.16.0' __version__ = '0.17.0'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@ -260,6 +260,13 @@ class Arguments(object):
default=None default=None
) )
self.parser.add_argument(
'-db', '--db-url',
help='Show trades stored in database.',
dest='db_url',
default=None
)
def testdata_dl_options(self) -> None: def testdata_dl_options(self) -> None:
""" """
Parses given arguments for testdata download Parses given arguments for testdata download

View File

@ -297,9 +297,10 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
if not data_part: if not data_part:
break break
logger.info('Downloaded data for time range [%s, %s]', logger.debug('Downloaded data for %s time range [%s, %s]',
arrow.get(data_part[0][0] / 1000).format(), pair,
arrow.get(data_part[-1][0] / 1000).format()) arrow.get(data_part[0][0] / 1000).format(),
arrow.get(data_part[-1][0] / 1000).format())
data.extend(data_part) data.extend(data_part)
since_ms = data[-1][0] + 1 since_ms = data[-1][0] + 1

View File

@ -5,6 +5,7 @@ e.g BTC to USD
import logging import logging
import time import time
from typing import Dict
from coinmarketcap import Market from coinmarketcap import Market
@ -73,12 +74,7 @@ class CryptoToFiatConverter(object):
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD" "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
] ]
CRYPTOMAP = { _cryptomap: Dict = {}
'BTC': 'bitcoin',
'ETH': 'ethereum',
'USDT': 'thether',
'BNB': 'binance-coin'
}
def __new__(cls): def __new__(cls):
if CryptoToFiatConverter.__instance is None: if CryptoToFiatConverter.__instance is None:
@ -91,6 +87,15 @@ class CryptoToFiatConverter(object):
def __init__(self) -> None: def __init__(self) -> None:
self._pairs = [] self._pairs = []
self._load_cryptomap()
def _load_cryptomap(self) -> None:
try:
coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"]))
except ValueError:
logger.error("Could not load FIAT Cryptocurrency map")
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
""" """
@ -182,16 +187,17 @@ class CryptoToFiatConverter(object):
if not self._is_supported_fiat(fiat=fiat_symbol): if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol)) raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
if crypto_symbol not in self.CRYPTOMAP: if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot) # return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol) logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0 return 0.0
try: try:
return float( return float(
self._coinmarketcap.ticker( self._coinmarketcap.ticker(
currency=self.CRYPTOMAP[crypto_symbol], currency=self._cryptomap[crypto_symbol],
convert=fiat_symbol convert=fiat_symbol
)[0]['price_' + fiat_symbol.lower()] )['data']['quotes'][fiat_symbol.upper()]['price']
) )
except BaseException: except BaseException as ex:
logger.error("Error in _find_price: %s", ex)
return 0.0 return 0.0

View File

@ -330,6 +330,7 @@ class FreqtradeBot(object):
fee_open=fee, fee_open=fee,
fee_close=fee, fee_close=fee,
open_rate=buy_limit, open_rate=buy_limit,
open_rate_requested=buy_limit,
open_date=datetime.utcnow(), open_date=datetime.utcnow(),
exchange=exchange.get_id(), exchange=exchange.get_id(),
open_order_id=order_id open_order_id=order_id
@ -396,7 +397,7 @@ class FreqtradeBot(object):
return order_amount return order_amount
# use fee from order-dict if possible # use fee from order-dict if possible
if 'fee' in order and order['fee']: if 'fee' in order and order['fee'] and (order['fee'].keys() >= {'currency', 'cost'}):
if trade.pair.startswith(order['fee']['currency']): if trade.pair.startswith(order['fee']['currency']):
new_amount = order_amount - order['fee']['cost'] new_amount = order_amount - order['fee']['cost']
logger.info("Applying fee on amount for %s (from %s to %s) from Order", logger.info("Applying fee on amount for %s (from %s to %s) from Order",
@ -413,7 +414,7 @@ class FreqtradeBot(object):
fee_abs = 0 fee_abs = 0
for exectrade in trades: for exectrade in trades:
amount += exectrade['amount'] amount += exectrade['amount']
if "fee" in exectrade: if "fee" in exectrade and (exectrade['fee'].keys() >= {'currency', 'cost'}):
# only applies if fee is in quote currency! # only applies if fee is in quote currency!
if trade.pair.startswith(exectrade['fee']['currency']): if trade.pair.startswith(exectrade['fee']['currency']):
fee_abs += exectrade['fee']['cost'] fee_abs += exectrade['fee']['cost']
@ -538,6 +539,7 @@ class FreqtradeBot(object):
# Execute sell and update trade record # Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id'] order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id trade.open_order_id = order_id
trade.close_rate_requested = limit
fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2)
profit_trade = trade.calc_profit(rate=limit) profit_trade = trade.calc_profit(rate=limit)

View File

@ -15,6 +15,8 @@ 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 sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from sqlalchemy import inspect
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -50,12 +52,61 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
Trade.session = session() Trade.session = session()
Trade.query = session.query_property() Trade.query = session.query_property()
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
check_migrate(engine)
# Clean dry_run DB # Clean dry_run DB
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False): if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
clean_dry_run_db() clean_dry_run_db()
def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def check_migrate(engine) -> None:
"""
Checks if migration is necessary and migrates if necessary
"""
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'fee_open'):
# Schema migration necessary
engine.execute("alter table trades rename to trades_bak")
# let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema
engine.execute("""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id)
select id, lower(exchange),
case
when instr(pair, '_') != 0 then
substr(pair, instr(pair, '_') + 1) || '/' ||
substr(pair, 1, instr(pair, '_') - 1)
else pair
end
pair,
is_open, fee fee_open, fee fee_close,
open_rate, null open_rate_requested, close_rate,
null close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id
from trades_bak
""")
# Reread columns - the above recreated the table!
inspector = inspect(engine)
cols = inspector.get_columns('trades')
if not has_column(cols, 'open_rate_requested'):
engine.execute("alter table trades add open_rate_requested float")
if not has_column(cols, 'close_rate_requested'):
engine.execute("alter table trades add close_rate_requested float")
def cleanup() -> None: def cleanup() -> None:
""" """
Flushes all pending operations to disk. Flushes all pending operations to disk.
@ -88,7 +139,9 @@ class Trade(_DECL_BASE):
fee_open = Column(Float, nullable=False, default=0.0) fee_open = Column(Float, nullable=False, default=0.0)
fee_close = Column(Float, nullable=False, default=0.0) fee_close = Column(Float, nullable=False, default=0.0)
open_rate = Column(Float) open_rate = Column(Float)
open_rate_requested = Column(Float)
close_rate = Column(Float) close_rate = Column(Float)
close_rate_requested = Column(Float)
close_profit = Column(Float) close_profit = Column(Float)
stake_amount = Column(Float, nullable=False) stake_amount = Column(Float, nullable=False)
amount = Column(Float) amount = Column(Float)

View File

@ -245,35 +245,34 @@ class RPC(object):
""" """
:return: current account balance per crypto :return: current account balance per crypto
""" """
balances = [
c for c in exchange.get_balances()
if c['Balance'] or c['Available'] or c['Pending']
]
if not balances:
return True, '`All balances are zero.`'
output = [] output = []
total = 0.0 total = 0.0
for currency in balances: for coin, balance in exchange.get_balances().items():
coin = currency['Currency'] if not balance['total']:
continue
rate = None
if coin == 'BTC': if coin == 'BTC':
currency["Rate"] = 1.0 rate = 1.0
else: else:
if coin == 'USDT': if coin == 'USDT':
currency["Rate"] = 1.0 / exchange.get_ticker('BTC/USDT', False)['bid'] rate = 1.0 / exchange.get_ticker('BTC/USDT', False)['bid']
else: else:
currency["Rate"] = exchange.get_ticker(coin + '/BTC', False)['bid'] rate = exchange.get_ticker(coin + '/BTC', False)['bid']
currency['BTC'] = currency["Rate"] * currency["Balance"] est_btc: float = rate * balance['total']
total = total + currency['BTC'] total = total + est_btc
output.append( output.append(
{ {
'currency': currency['Currency'], 'currency': coin,
'available': currency['Available'], 'available': balance['free'],
'balance': currency['Balance'], 'balance': balance['total'],
'pending': currency['Pending'], 'pending': balance['used'],
'est_btc': currency['BTC'] 'est_btc': est_btc
} }
) )
if total == 0.0:
return True, '`All balances are zero.`'
fiat = self.freqtrade.fiat_converter fiat = self.freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)

View File

@ -264,17 +264,15 @@ class Telegram(RPC):
(currencys, total, symbol, value) = result (currencys, total, symbol, value) = result
output = '' output = ''
for currency in currencys: for currency in currencys:
output += """*Currency*: {currency} output += "*{currency}:*\n" \
*Available*: {available} "\t`Available: {available: .8f}`\n" \
*Balance*: {balance} "\t`Balance: {balance: .8f}`\n" \
*Pending*: {pending} "\t`Pending: {pending: .8f}`\n" \
*Est. BTC*: {est_btc: .8f} "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
""".format(**currency)
output += """*Estimated Value*: output += "\n*Estimated Value*:\n" \
*BTC*: {0: .8f} "\t`BTC: {0: .8f}`\n" \
*{1}*: {2: .2f} "\t`{1}: {2: .2f}`\n".format(total, symbol, value)
""".format(total, symbol, value)
self.send_msg(output) self.send_msg(output)
@authorized_only @authorized_only
@ -357,8 +355,9 @@ class Telegram(RPC):
message = tabulate({ message = tabulate({
'current': [len(trades)], 'current': [len(trades)],
'max': [self._config['max_open_trades']] 'max': [self._config['max_open_trades']],
}, headers=['current', 'max'], tablefmt='simple') 'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
}, headers=['current', 'max', 'total stake'], tablefmt='simple')
message = "<pre>{}</pre>".format(message) message = "<pre>{}</pre>".format(message)
logger.debug(message) logger.debug(message)
self.send_msg(message, parse_mode=ParseMode.HTML) self.send_msg(message, parse_mode=ParseMode.HTML)

View File

@ -2,6 +2,7 @@
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Dict, Optional
from functools import reduce from functools import reduce
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -34,7 +35,8 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
:param config: Config to pass to the bot :param config: Config to pass to the bot
:return: None :return: None
""" """
mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0}) # mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0})
patch_coinmarketcap(mocker, {'price_usd': 12345.0})
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock())
@ -46,6 +48,27 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
return FreqtradeBot(config, create_engine('sqlite://')) return FreqtradeBot(config, create_engine('sqlite://'))
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
tickermock = MagicMock(return_value={'price_usd': 12345.0})
listmock = MagicMock(return_value={'data': [{'id': 1, 'name': 'Bitcoin', 'symbol': 'BTC',
'website_slug': 'bitcoin'},
{'id': 1027, 'name': 'Ethereum', 'symbol': 'ETH',
'website_slug': 'ethereum'}
]})
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
ticker=tickermock,
listings=listmock,
)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def default_conf(): def default_conf():
""" Returns validated configuration suitable for most tests """ """ Returns validated configuration suitable for most tests """

View File

@ -123,7 +123,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None:
assert under > correct assert under > correct
def test_log_results_if_loss_improves(capsys) -> None: def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None:
hyperopt = _HYPEROPT hyperopt = _HYPEROPT
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.log_results( hyperopt.log_results(

View File

@ -288,22 +288,18 @@ def test_rpc_balance_handle(default_conf, mocker):
""" """
Test rpc_balance() method Test rpc_balance() method
""" """
mock_balance = [ mock_balance = {
{ 'BTC': {
'Currency': 'BTC', 'free': 10.0,
'Balance': 10.0, 'total': 12.0,
'Available': 12.0, 'used': 2.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}, },
{ 'ETH': {
'Currency': 'ETH', 'free': 0.0,
'Balance': 0.0, 'total': 0.0,
'Available': 0.0, 'used': 0.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX',
} }
] }
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
mocker.patch.multiple( mocker.patch.multiple(
@ -324,15 +320,15 @@ def test_rpc_balance_handle(default_conf, mocker):
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) (error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
assert not error assert not error
(trade, x, y, z) = res (trade, x, y, z) = res
assert prec_satoshi(x, 10) assert prec_satoshi(x, 12)
assert prec_satoshi(z, 150000) assert prec_satoshi(z, 180000)
assert 'USD' in y assert 'USD' in y
assert len(trade) == 1 assert len(trade) == 1
assert 'BTC' in trade[0]['currency'] assert 'BTC' in trade[0]['currency']
assert prec_satoshi(trade[0]['available'], 12) assert prec_satoshi(trade[0]['available'], 10)
assert prec_satoshi(trade[0]['balance'], 10) assert prec_satoshi(trade[0]['balance'], 12)
assert prec_satoshi(trade[0]['pending'], 0) assert prec_satoshi(trade[0]['pending'], 2)
assert prec_satoshi(trade[0]['est_btc'], 10) assert prec_satoshi(trade[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:

View File

@ -554,36 +554,29 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
""" """
Test _balance() method Test _balance() method
""" """
mock_balance = [
{ mock_balance = {
'Currency': 'BTC', 'BTC': {
'Balance': 10.0, 'total': 12.0,
'Available': 12.0, 'free': 12.0,
'Pending': 0.0, 'used': 0.0
'CryptoAddress': 'XXXX',
}, },
{ 'ETH': {
'Currency': 'ETH', 'total': 0.0,
'Balance': 0.0, 'free': 0.0,
'Available': 0.0, 'used': 0.0
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}, },
{ 'USDT': {
'Currency': 'USDT', 'total': 10000.0,
'Balance': 10000.0, 'free': 10000.0,
'Available': 0.0, 'used': 0.0
'Pending': 0.0,
'CryptoAddress': 'XXXX',
}, },
{ 'LTC': {
'Currency': 'LTC', 'total': 10.0,
'Balance': 10.0, 'free': 10.0,
'Available': 10.0, 'used': 0.0
'Pending': 0.0,
'CryptoAddress': 'XXXX',
} }
] }
def mock_ticker(symbol, refresh): def mock_ticker(symbol, refresh):
""" """
@ -621,12 +614,12 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
telegram._balance(bot=MagicMock(), update=update) telegram._balance(bot=MagicMock(), update=update)
result = msg_mock.call_args_list[0][0][0] result = msg_mock.call_args_list[0][0][0]
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert '*Currency*: BTC' in result assert '*BTC:*' in result
assert '*Currency*: ETH' not in result assert '*ETH:*' not in result
assert '*Currency*: USDT' in result assert '*USDT:*' in result
assert 'Balance' in result assert 'Balance:' in result
assert 'Est. BTC' in result assert 'Est. BTC:' in result
assert '*BTC*: 12.00000000' in result assert 'BTC: 14.00000000' in result
def test_zero_balance_handle(default_conf, update, mocker) -> None: def test_zero_balance_handle(default_conf, update, mocker) -> None:
@ -636,7 +629,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value=[]) mocker.patch('freqtrade.freqtradebot.exchange.get_balances', return_value={})
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -1013,9 +1006,12 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
msg_mock.reset_mock() msg_mock.reset_mock()
telegram._count(bot=MagicMock(), update=update) telegram._count(bot=MagicMock(), update=update)
msg = '<pre> current max\n--------- -----\n 1 {}</pre>'.format( msg = '<pre> current max total stake\n--------- ----- -------------\n' \
default_conf['max_open_trades'] ' 1 {} {}</pre>'\
) .format(
default_conf['max_open_trades'],
default_conf['stake_amount']
)
assert msg in msg_mock.call_args_list[0][0][0] assert msg in msg_mock.call_args_list[0][0][0]

View File

@ -7,6 +7,7 @@ from unittest.mock import MagicMock
import pytest import pytest
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import patch_coinmarketcap
def test_pair_convertion_object(): def test_pair_convertion_object():
@ -123,12 +124,23 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration assert fiat_convert._pairs[0]._expiration is not expiration
def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert len(fiat_convert._cryptomap) == 2
assert fiat_convert._cryptomap["BTC"] == "1"
def test_fiat_convert_without_network(): def test_fiat_convert_without_network():
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
fiat_convert = CryptoToFiatConverter() fiat_convert = CryptoToFiatConverter()
cmc_temp = CryptoToFiatConverter._coinmarketcap
CryptoToFiatConverter._coinmarketcap = None CryptoToFiatConverter._coinmarketcap = None
assert fiat_convert._coinmarketcap is None assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0 assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
CryptoToFiatConverter._coinmarketcap = cmc_temp

View File

@ -8,7 +8,6 @@ import logging
import re import re
import time import time
from copy import deepcopy from copy import deepcopy
from typing import Dict, Optional
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
@ -20,7 +19,7 @@ from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has, patch_coinmarketcap
# Functions for recurrent object patching # Functions for recurrent object patching
@ -64,20 +63,6 @@ def patch_RPCManager(mocker) -> MagicMock:
return rpc_mock return rpc_mock
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
"""
Mocker to coinmarketcap to speed up tests
:param mocker: mocker to patch coinmarketcap class
:return: None
"""
mock = MagicMock()
if value:
mock.ticker = {'price_usd': 12345.0}
mocker.patch('freqtrade.fiat_convert.Market', mock)
# Unit tests # Unit tests
def test_freqtradebot_object() -> None: def test_freqtradebot_object() -> None:
""" """
@ -1458,7 +1443,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[trades_for_order])
amount = float(sum(x['amount'] for x in trades_for_order)) amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade( trade = Trade(
pair='LTC/ETH', pair='LTC/ETH',
@ -1473,3 +1458,53 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order', 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996) from Order',
caplog.record_tuples) caplog.record_tuples)
def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount with split trades (multiple trades for this order)
"""
limit_buy_order = deepcopy(buy_order_fee)
limit_buy_order['fee'] = {'cost': 0.004}
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=[])
amount = float(sum(x['amount'] for x in trades_for_order))
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount - fees in Stake currency
"""
# Remove "Currency" from fee dict
trades_for_order[0]['fee'] = {'cost': 0.008}
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.get_trades_for_order', return_value=trades_for_order)
amount = sum(x['amount'] for x in trades_for_order)
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount

View File

@ -375,3 +375,105 @@ def test_clean_dry_run_db(default_conf, fee):
# We have now only the prod # We have now only the prod
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1 assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
def test_migrate_old(default_conf, fee):
"""
Test Database migration(starting with old pairformat)
"""
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
0.00258580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex"
def test_migrate_new(default_conf, fee):
"""
Test Database migration (starting with new pairformat)
"""
amount = 103.223
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
id INTEGER NOT NULL,
exchange VARCHAR NOT NULL,
pair VARCHAR NOT NULL,
is_open BOOLEAN NOT NULL,
fee FLOAT NOT NULL,
open_rate FLOAT,
close_rate FLOAT,
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
PRIMARY KEY (id),
CHECK (is_open IN (0, 1))
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, stake_amount, amount, open_date)
VALUES ('binance', 'ETC/BTC', 1, {fee},
0.00258580, {stake}, {amount},
'2019-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
stake=default_conf.get("stake_amount"),
amount=amount
)
engine = create_engine('sqlite://')
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
assert trade.fee_open == fee.return_value
assert trade.fee_close == fee.return_value
assert trade.open_rate_requested is None
assert trade.close_rate_requested is None
assert trade.is_open == 1
assert trade.amount == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance"

View File

@ -1,14 +1,14 @@
ccxt==1.11.149 ccxt==1.14.24
SQLAlchemy==1.2.7 SQLAlchemy==1.2.7
python-telegram-bot==10.0.2 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.0.1 cachetools==2.1.0
requests==2.18.4 requests==2.18.4
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.22.0 pandas==0.23.0
scikit-learn==0.19.1 scikit-learn==0.19.1
scipy==1.0.1 scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.3 numpy==1.14.3
TA-Lib==0.4.17 TA-Lib==0.4.17
@ -19,7 +19,7 @@ hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 networkx==1.11
tabulate==0.8.2 tabulate==0.8.2
coinmarketcap==4.2.1 coinmarketcap==5.0.3
# Required for plotting data # Required for plotting data
#plotly==2.3.0 #plotly==2.3.0

View File

@ -10,6 +10,7 @@ Optional Cli parameters
-d / --datadir: path to pair backtest data -d / --datadir: path to pair backtest data
--timerange: specify what timerange of data to use. --timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair -l / --live: Live, to download the latest ticker for the pair
-db / --db-url: Show trades stored in database
""" """
import logging import logging
import sys import sys
@ -21,18 +22,18 @@ from plotly import tools
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objs as go import plotly.graph_objs as go
from typing import Dict, List, Any
from sqlalchemy import create_engine
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade import exchange from freqtrade import exchange
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import persistence
from freqtrade.persistence import Trade
<<<<<<< HEAD
logger = logging.getLogger('freqtrade')
=======
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
>>>>>>> bddf009a2b6d0e1a19cca558887ce972e99a6238 _CONF: Dict[str, Any] = {}
def plot_analyzed_dataframe(args: Namespace) -> None: def plot_analyzed_dataframe(args: Namespace) -> None:
""" """
@ -73,6 +74,12 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_buy_trend(dataframe)
dataframe = analyze.populate_sell_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe)
trades = []
if args.db_url:
engine = create_engine('sqlite:///' + args.db_url)
persistence.init(_CONF, engine)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
if len(dataframe.index) > 750: if len(dataframe.index) > 750:
logger.warning('Ticker contained more than 750 candles, clipping.') logger.warning('Ticker contained more than 750 candles, clipping.')
data = dataframe.tail(750) data = dataframe.tail(750)
@ -113,6 +120,31 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
) )
) )
trade_buys = go.Scattergl(
x=[t.open_date.isoformat() for t in trades],
y=[t.open_rate for t in trades],
mode='markers',
name='trade_buy',
marker=dict(
symbol='square-open',
size=11,
line=dict(width=2),
color='green'
)
)
trade_sells = go.Scattergl(
x=[t.close_date.isoformat() for t in trades],
y=[t.close_rate for t in trades],
mode='markers',
name='trade_sell',
marker=dict(
symbol='square-open',
size=11,
line=dict(width=2),
color='red'
)
)
bb_lower = go.Scatter( bb_lower = go.Scatter(
x=data.date, x=data.date,
y=data.bb_lowerband, y=data.bb_lowerband,
@ -147,6 +179,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
fig.append_trace(volume, 2, 1) fig.append_trace(volume, 2, 1)
fig.append_trace(macd, 3, 1) fig.append_trace(macd, 3, 1)
fig.append_trace(macdsignal, 3, 1) fig.append_trace(macdsignal, 3, 1)
fig.append_trace(trade_buys, 1, 1)
fig.append_trace(trade_sells, 1, 1)
fig['layout'].update(title=args.pair) fig['layout'].update(title=args.pair)
fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis1'].update(title='Price')

View File

@ -24,8 +24,7 @@ import plotly.graph_objs as go
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtradeimport constants from freqtrade import constants
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
import freqtrade.misc as misc import freqtrade.misc as misc
@ -33,11 +32,12 @@ import freqtrade.misc as misc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# data:: [ pair, profit-%, enter, exit, time, duration] # data:: [ pair, profit-%, enter, exit, time, duration]
# data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65] # data:: ["ETH/BTC", 0.0023975, "1515598200", "1515602100", "2018-01-10 07:30:00+00:00", 65]
def make_profit_array( def make_profit_array(data: List, px: int, min_date: int,
data: List, px: int, min_date: int, interval: int,
interval: int, filter_pairs: Optional[List] = None) -> np.ndarray: filter_pairs: Optional[List] = None) -> np.ndarray:
pg = np.zeros(px) pg = np.zeros(px)
filter_pairs = filter_pairs or [] filter_pairs = filter_pairs or []
# Go through the trades # Go through the trades