This commit is contained in:
Ramon Bastiaans 2018-02-08 12:39:01 +00:00 committed by GitHub
commit 3e1bddbdd1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1207 additions and 101 deletions

View File

@ -71,7 +71,7 @@ your current trades.
### Exchange supported ### Exchange supported
- [x] Bittrex - [x] Bittrex
- [ ] Binance - [x] Binance
- [ ] Others - [ ] Others
## Quick start ## Quick start

View File

@ -24,7 +24,7 @@ The table below will list all configuration parameters.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. | `exchange.name` | bittrex | Yes | Name of the exchange class to use. Valid values are: `bittrex` or `binance`
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param. | `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
@ -94,7 +94,7 @@ creating trades.
"dry_run": true, "dry_run": true,
``` ```
3. Remove your Bittrex API key (change them by fake api credentials) 3. Remove your exchange API key (change them by fake api credentials)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -120,7 +120,7 @@ you run it in production mode.
"dry_run": false, "dry_run": false,
``` ```
3. Insert your Bittrex API key (change them by fake api keys) 3. Insert your exchange API key (change them by fake api keys)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -129,7 +129,7 @@ you run it in production mode.
... ...
} }
``` ```
If you have not your Bittrex API key yet, If you have not your exchange API key yet,
[see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md). [see our tutorial](https://github.com/gcarq/freqtrade/blob/develop/docs/pre-requisite.md).

View File

@ -1,15 +1,23 @@
# Pre-requisite # Pre-requisite
Before running your bot in production you will need to setup few Before running your bot in production you will need to setup few
external API. In production mode, the bot required valid Bittrex API external API. In production mode, the bot requires valid exchange API
credentials and a Telegram bot (optional but recommended). credentials and a Telegram bot (optional but recommended).
## Table of Contents ## Table of Contents
- [Setup your Bittrex account](#setup-your-bittrex-account) - [Setup your Exchange account](#setup-your-exchange-account)
- [Backtesting commands](#setup-your-telegram-bot) - [Backtesting commands](#setup-your-telegram-bot)
## Setup your Bittrex account ## Setup your exchange account
### Bittrex
*To be completed, please feel free to complete this section.* *To be completed, please feel free to complete this section.*
### Binance
- Go to: https://www.binance.com/userCenter/createApi.html
- Enter API key label: "freqtrade bot" and click "Create New Key"
- Check the "Enable Trading" checkbox
- Write down the API key and secret to put in: config.json
## Setup your Telegram bot ## Setup your Telegram bot
The only things you need is a working Telegram bot and its API token. The only things you need is a working Telegram bot and its API token.
Below we explain how to create your Telegram Bot, and how to get your Below we explain how to create your Telegram Bot, and how to get your

View File

@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.binance import Binance
from freqtrade.exchange.interface import Exchange from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -28,6 +29,7 @@ class Exchanges(enum.Enum):
Maps supported exchange names to correspondent classes. Maps supported exchange names to correspondent classes.
""" """
BITTREX = Bittrex BITTREX = Bittrex
BINANCE = Binance
def init(config: dict) -> None: def init(config: dict) -> None:
@ -54,6 +56,7 @@ def init(config: dict) -> None:
except KeyError: except KeyError:
raise OperationalException('Exchange {} is not supported'.format(name)) raise OperationalException('Exchange {} is not supported'.format(name))
exchange_config['stake_currency'] = config['stake_currency']
_API = exchange_class(exchange_config) _API = exchange_class(exchange_config)
# Check if all pairs are available # Check if all pairs are available
@ -143,14 +146,14 @@ def get_ticker_history(pair: str, tick_interval) -> List[Dict]:
return _API.get_ticker_history(pair, tick_interval) return _API.get_ticker_history(pair, tick_interval)
def cancel_order(order_id: str) -> None: def cancel_order(order_id: str, pair: str) -> None:
if _CONF['dry_run']: if _CONF['dry_run']:
return return
return _API.cancel_order(order_id) return _API.cancel_order(order_id, pair)
def get_order(order_id: str) -> Dict: def get_order(order_id: str, pair: str) -> Dict:
if _CONF['dry_run']: if _CONF['dry_run']:
order = _DRY_RUN_OPEN_ORDERS[order_id] order = _DRY_RUN_OPEN_ORDERS[order_id]
order.update({ order.update({
@ -158,7 +161,7 @@ def get_order(order_id: str) -> Dict:
}) })
return order return order
return _API.get_order(order_id) return _API.get_order(order_id, pair)
def get_pair_detail_url(pair: str) -> str: def get_pair_detail_url(pair: str) -> str:
@ -183,3 +186,7 @@ def get_fee() -> float:
def get_wallet_health() -> List[Dict]: def get_wallet_health() -> List[Dict]:
return _API.get_wallet_health() return _API.get_wallet_health()
def get_trade_qty(pair: str) -> tuple:
return _API.get_trade_qty(pair)

View File

@ -0,0 +1,549 @@
import logging
import datetime
import json
import http
from time import sleep
from typing import List, Dict, Optional
from binance.client import Client as _Binance
from binance.exceptions import BinanceAPIException
from binance.enums import (KLINE_INTERVAL_1MINUTE, KLINE_INTERVAL_5MINUTE,
KLINE_INTERVAL_30MINUTE, KLINE_INTERVAL_1HOUR,
KLINE_INTERVAL_1DAY)
from decimal import Decimal
from freqtrade import OperationalException
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Binance = None
_EXCHANGE_CONF: dict = {}
_CONF: dict = {}
class Binance(Exchange):
"""
Binance API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.binance.com'
def __init__(self, config: dict) -> None:
global _API, _EXCHANGE_CONF, _CONF
_EXCHANGE_CONF.update(config)
_API = _Binance(_EXCHANGE_CONF['key'], _EXCHANGE_CONF['secret'])
def _pair_to_symbol(self, pair, seperator='') -> str:
"""
Turns freqtrade pair into Binance symbol
- Freqtrade pair = <stake_currency>_<currency>
i.e.: BTC_XALT
- Binance symbol = <currency><stake_currency>
i.e.: XALTBTC
"""
pair_currencies = pair.split('_')
return '{0}{1}{2}'.format(pair_currencies[1], seperator, pair_currencies[0])
def _symbol_to_pair(self, symbol) -> str:
"""
Turns Binance symbol into freqtrade pair
- Freqtrade pair = <stake_currency>_<currency>
i.e.: BTC_XALT
- Binance symbol = <currency><stake_currency>
i.e.: XALTBTC
"""
stake = _EXCHANGE_CONF['stake_currency']
symbol_stake_currency = symbol[-len(stake):]
symbol_currency = symbol[:-len(stake)]
return '{0}_{1}'.format(symbol_stake_currency, symbol_currency)
@staticmethod
def _handle_exception(excepter) -> Dict:
"""
Validates the given Binance response/exception
and raises a ContentDecodingError if a non-fatal issue happened.
"""
# Python exceptions:
# http://python-binance.readthedocs.io/en/latest/binance.html#module-binance.exceptions
handle = {}
if type(excepter) == http.client.RemoteDisconnected:
logger.info(
'Retrying: got disconnected from Binance: %s' % excepter
)
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
if type(excepter) == json.decoder.JSONDecodeError:
logger.info(
'Retrying: got JSON error from Binance: %s' % excepter
)
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
# API errors:
# https://github.com/binance-exchange/binance-official-api-docs/blob/master/errors.md
if type(excepter) == BinanceAPIException:
if excepter.code == -1000:
logger.info(
'Retrying: General unknown API error from Binance: %s' % excepter
)
handle['retry'] = True
handle['retry_max'] = 3
handle['fatal'] = False
return handle
if excepter.code == -1003:
logger.error(
'Binance API Rate limiter hit: %s' % excepter
)
# Panic: this is bad: we don't want to get banned
# TODO: automatic request throttling respecting API rate limits?
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -1021:
logger.error(
"Binance reports invalid timestamp, " +
"check your machine (NTP) time synchronisation: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -1015:
logger.error(
'Binance says we have too many orders: %s' % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2014:
logger.error(
"Binance reports bad api key format, " +
"you're probably trying to use the API with an empty key/secret: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2015:
logger.error(
"Binance reports invalid api key, source IP or permission, " +
"check your API key settings in config.json and on binance.com: {}".format(
excepter)
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
if excepter.code == -2011:
logger.error(
"Binance rejected order cancellation: %s" % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
# All other exceptions we don't know about
logger.info(
'Got error: %s' % excepter
)
handle['retry'] = False
handle['retry_max'] = None
handle['fatal'] = True
return handle
raise type(excepter)(excepter.args)
@property
def fee(self) -> float:
# 0.1 %: See https://support.binance.com/hc/en-us
# /articles/115000429332-Fee-Structure-on-Binance
return 0.001
def buy(self, pair: str, rate: float, amount: float) -> str:
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.order_limit_buy(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal'] or tries == max_tries:
raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=Decimal(rate),
amount=Decimal(amount)))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data['orderId']
def sell(self, pair: str, rate: float, amount: float) -> str:
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.order_limit_sell(
symbol=symbol,
quantity="{0:.8f}".format(amount),
price="{0:.8f}".format(rate))
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException(
'{message} params=({pair}, {rate}, {amount})'.format(
message=str(e),
pair=pair,
rate=rate,
amount=amount))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data['orderId']
def get_balance(self, currency: str) -> float:
try:
data = _API.get_asset_balance(asset=currency)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({currency})'.format(
message=str(e),
currency=currency))
return float(data['free'] or 0.0)
def get_balances(self) -> List[Dict]:
try:
data = _API.get_account()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
balances = data['balances']
currency_balances = []
for currency in balances:
balance = {}
if float(currency['free']) == 0 and float(currency['locked']) == 0:
continue
balance['Currency'] = currency.pop('asset')
balance['Available'] = currency.pop('free')
balance['Pending'] = currency.pop('locked')
balance['Balance'] = float(balance['Available']) + float(balance['Pending'])
currency_balances.append(balance)
return currency_balances
def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict:
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_ticker(symbol=symbol)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({pair} {refresh})'.format(
message=str(e),
pair=pair,
refresh=refresh))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return {
'bid': float(data['bidPrice']),
'ask': float(data['askPrice']),
'last': float(data['lastPrice']),
}
def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]:
if tick_interval == 1:
INTERVAL_ENUM = KLINE_INTERVAL_1MINUTE
elif tick_interval == 5:
INTERVAL_ENUM = KLINE_INTERVAL_5MINUTE
elif tick_interval == 30:
INTERVAL_ENUM = KLINE_INTERVAL_30MINUTE
elif tick_interval == 60:
INTERVAL_ENUM = KLINE_INTERVAL_1HOUR
elif tick_interval == 1440:
INTERVAL_ENUM = KLINE_INTERVAL_1DAY
else:
raise ValueError('Cannot parse tick_interval: {}'.format(tick_interval))
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_klines(symbol=symbol, interval=INTERVAL_ENUM)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({pair} {tick_interval})'.format(
message=str(e),
pair=pair,
tick_interval=tick_interval))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
tick_data = []
for tick in data:
t = {}
t['O'] = float(tick[1])
t['H'] = float(tick[2])
t['L'] = float(tick[3])
t['C'] = float(tick[4])
t['V'] = float(tick[5])
t['T'] = datetime.datetime.fromtimestamp(int(tick[6])/1000).isoformat()
t['BV'] = float(tick[7])
tick_data.append(t)
return tick_data
def get_order(self, order_id: str, pair: str) -> Dict:
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_all_orders(symbol=symbol, orderId=order_id)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException(
'{message} params=({symbol},{order_id})'.format(
message=str(e),
symbol=symbol,
order_id=order_id))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
order = {}
for o in data:
if o['orderId'] == int(order_id):
order['id'] = o['orderId']
order['type'] = "{}_{}".format(o['type'], o['side'])
order['pair'] = self._symbol_to_pair(o['symbol'])
order['opened'] = datetime.datetime.fromtimestamp(
int(o['time'])/1000).isoformat()
order['closed'] = datetime.datetime.fromtimestamp(
int(o['time'])/1000).isoformat()\
if o['status'] == 'FILLED' else None
order['rate'] = float(o['price'])
order['amount'] = float(o['origQty'])
order['remaining'] = int(float(o['origQty']) - float(o['executedQty']))
return order
def cancel_order(self, order_id: str, pair: str) -> None:
symbol = self._pair_to_symbol(pair)
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message} params=({order_id}, {pair})'.format(
message=str(e),
order_id=order_id),
pair=pair)
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
return data
def get_pair_detail_url(self, pair: str) -> str:
symbol = self._pair_to_symbol(pair, '_')
return 'https://www.binance.com/indexSpa.html#/trade/index?symbol={}'.format(symbol)
def get_markets(self) -> List[str]:
try:
data = _API.get_all_tickers()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
markets = []
stake = _EXCHANGE_CONF['stake_currency']
for t in data:
symbol = t['symbol']
symbol_stake_currency = symbol[-len(stake):]
if symbol_stake_currency == stake:
pair = self._symbol_to_pair(symbol)
markets.append(pair)
return markets
def get_market_summaries(self) -> List[Dict]:
try:
data = _API.get_ticker()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
market_summaries = []
for t in data:
market = {}
# Looks like this one is only one actually used
market['MarketName'] = self._symbol_to_pair(t['symbol'])
market['High'] = t['highPrice']
market['Low'] = t['lowPrice']
market['Volume'] = t['volume']
market['Last'] = t['lastPrice']
market['TimeStamp'] = t['closeTime']
market['BaseVolume'] = t['volume']
market['Bid'] = t['bidPrice']
market['Ask'] = t['askPrice']
market['OpenBuyOrders'] = None # TODO: Implement me (or dont care)
market['OpenSellOrders'] = None # TODO: Implement me (or dont care)
market['PrevDay'] = t['prevClosePrice']
market['Created'] = None # TODO: Implement me (or don't care)
market_summaries.append(market)
return market_summaries
def get_trade_qty(self, pair: str) -> tuple:
api_try = True
tries = 0
max_tries = 1
while api_try and tries < max_tries:
try:
tries = tries + 1
data = _API.get_exchange_info()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
api_try = h['retry']
max_tries = h['retry_max']
sleep(0.1)
symbol = self._pair_to_symbol(pair)
for s in data['symbols']:
if symbol == s['symbol']:
for f in s['filters']:
if f['filterType'] == 'LOT_SIZE':
return (float(f['minQty']), float(f['maxQty']), float(f['stepSize']))
return (None, None, None)
def get_wallet_health(self) -> List[Dict]:
try:
data = _API.get_exchange_info()
except Exception as e:
h = Binance._handle_exception(e)
if h['fatal']:
raise OperationalException('{message}'.format(message=str(e)))
wallet_health = []
for s in data['symbols']:
wallet = {}
wallet['Currency'] = s['baseAsset']
wallet['IsActive'] = True if s['status'] == 'TRADING' else False
wallet['LastChecked'] = None # TODO
wallet['Notice'] = s['status'] if s['status'] != 'TRADING' else ''
wallet_health.append(wallet)
return wallet_health

View File

@ -157,7 +157,8 @@ class Bittrex(Exchange):
return data['result'] return data['result']
def get_order(self, order_id: str) -> Dict: def get_order(self, order_id: str, pair: str) -> Dict:
data = _API.get_order(order_id) data = _API.get_order(order_id)
if not data['success']: if not data['success']:
Bittrex._validate_response(data) Bittrex._validate_response(data)
@ -176,7 +177,8 @@ class Bittrex(Exchange):
'closed': data['Closed'], 'closed': data['Closed'],
} }
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
data = _API.cancel(order_id) data = _API.cancel(order_id)
if not data['success']: if not data['success']:
Bittrex._validate_response(data) Bittrex._validate_response(data)
@ -212,3 +214,6 @@ class Bittrex(Exchange):
'LastChecked': entry['Health']['LastChecked'], 'LastChecked': entry['Health']['LastChecked'],
'Notice': entry['Currency'].get('Notice'), 'Notice': entry['Currency'].get('Notice'),
} for entry in data['result']] } for entry in data['result']]
def get_trade_qty(self, pair: str) -> tuple:
return (None, None, None)

View File

@ -94,7 +94,7 @@ class Exchange(ABC):
] ]
""" """
def get_order(self, order_id: str) -> Dict: def get_order(self, order_id: str, pair: str) -> Dict:
""" """
Get order details for the given order_id. Get order details for the given order_id.
:param order_id: ID as str :param order_id: ID as str
@ -111,7 +111,7 @@ class Exchange(ABC):
""" """
@abstractmethod @abstractmethod
def cancel_order(self, order_id: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
""" """
Cancels order for given order_id. Cancels order for given order_id.
:param order_id: ID as str :param order_id: ID as str
@ -170,3 +170,11 @@ class Exchange(ABC):
}, },
... ...
""" """
@abstractmethod
def get_trade_qty(self, pair: str) -> tuple:
"""
Returns a tuple of trade quantity limits
:return: tuple, format: ( min_qty: str, max_qty: str, step_qty: str )
...
"""

View File

@ -84,7 +84,7 @@ def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
if trade.open_order_id: if trade.open_order_id:
# Update trade with order values # Update trade with order values
logger.info('Got open order for %s', trade) logger.info('Got open order for %s', trade)
trade.update(exchange.get_order(trade.open_order_id)) trade.update(exchange.get_order(trade.open_order_id, trade.pair))
if trade.is_open and trade.open_order_id is None: if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair # Check if we can sell our current pair
@ -151,7 +151,7 @@ def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
"""Buy timeout - cancel order """Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
exchange.cancel_order(trade.open_order_id) exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
Trade.session.delete(trade) Trade.session.delete(trade)
@ -165,7 +165,20 @@ def handle_timedout_limit_buy(trade: Trade, order: Dict) -> bool:
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
# and close the order # and close the order
trade.amount = order['amount'] - order['remaining']
new_trade_amount = order['amount'] - order['remaining']
(min_qty, max_qty, step_qty) = exchange.get_trade_qty(trade.pair)
if min_qty:
# Remaining amount must be exchange minimum order quantity to be able to sell it
if new_trade_amount < min_qty:
logger.info('Wont cancel partial filled buy order that timed out for {}:'.format(
trade) +
'remaining amount {} too low for new order '.format(new_trade_amount) +
'(minimum order quantity: {})'.format(new_trade_amount, min_qty))
return False
trade.amount = new_trade_amount
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
@ -180,21 +193,66 @@ def handle_timedout_limit_sell(trade: Trade, order: Dict) -> bool:
Sell timeout - cancel order and update trade Sell timeout - cancel order and update trade
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
if order['remaining'] == order['amount']: logger.info('Sell order timeout for %s.', trade)
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
logger.info('Sell order timeout for %s.', trade)
return True
# TODO: figure out how to handle partially complete sell orders # Partial filled sell order timed out
return False if order['remaining'] < order['amount']:
(min_qty, max_qty, step_qty) = exchange.get_trade_qty(trade.pair)
# Create new trade for partial filled amount and close that new trade
new_trade_amount = order['amount'] - order['remaining']
if min_qty:
# Remaining amount must be exchange minimum order quantity to be able to sell it
if new_trade_amount < min_qty:
logger.info('Wont cancel partial filled sell order that timed out for {}:'.format(
trade) +
'remaining amount {} too low for new order '.format(new_trade_amount) +
'(minimum order quantity: {})'.format(new_trade_amount, min_qty))
return False
exchange.cancel_order(trade.open_order_id, trade.pair)
new_trade_stake_amount = new_trade_amount * trade.open_rate
# but give it half fee: because we share buy order with current trade
# this trade only costs sell fee
new_trade = Trade(
pair=trade.pair,
stake_amount=new_trade_stake_amount,
amount=new_trade_amount,
fee=(trade.fee/2),
open_rate=trade.open_rate,
open_date=trade.open_date,
exchange=trade.exchange,
open_order_id=None
)
new_trade.close(order['rate'])
# Update stake and amount leftover of current trade to still be handled
trade.amount = order['remaining']
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
rpc.send_msg('*Timeout:* Partially filled sell order for {} cancelled: '.format(
trade.pair.replace('_', '/')) +
'{} amount remains'.format(trade.amount))
return False
# Order is not partially filled: full amount remains
# Just remove the order and the trade remains to be handled
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close_rate = None
trade.close_profit = None
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
return True
def check_handle_timedout(timeoutvalue: int) -> None: def check_handle_timedout(timeoutvalue: int) -> None:
@ -207,7 +265,7 @@ def check_handle_timedout(timeoutvalue: int) -> None:
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try: try:
order = exchange.get_order(trade.open_order_id) order = exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException: except requests.exceptions.RequestException:
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
@ -378,10 +436,16 @@ def create_trade(stake_amount: float, interval: int) -> bool:
stake_amount stake_amount
) )
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist']) whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount: # We need minimum funds of: stake amount + 2x transaction (buy+sell) fee to create a trade
min_required_funds = stake_amount + (stake_amount * (exchange.get_fee() * 2))
fund_balance = exchange.get_balance(_CONF['stake_currency'])
# Check if we have enough funds to be able to trade
if fund_balance < min_required_funds:
raise DependencyException( raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency']) 'not enough funds to create trade (balance={}, required={})'.format(
fund_balance, min_required_funds)
) )
# Remove currently opened and latest pairs from whitelist # Remove currently opened and latest pairs from whitelist
@ -401,15 +465,42 @@ def create_trade(stake_amount: float, interval: int) -> bool:
else: else:
return False return False
# Calculate amount min_qty = None
max_qty = None
step_qty = None
(min_qty, max_qty, step_qty) = exchange.get_trade_qty(pair)
# Calculate bid price
buy_limit = get_target_bid(exchange.get_ticker(pair)) buy_limit = get_target_bid(exchange.get_ticker(pair))
# Calculate base amount
amount = stake_amount / buy_limit amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount) # if amount above max qty: just buy max qty
if max_qty:
if amount > max_qty:
amount = max_qty
if min_qty:
if amount < min_qty:
raise DependencyException(
'stake amount is too low (min_qty={})'.format(min_qty)
)
# make trade exact amount of step qty
if step_qty:
real_amount = (amount // step_qty) * step_qty
else:
real_amount = amount
order_id = exchange.buy(pair, buy_limit, real_amount)
real_stake_amount = buy_limit * real_amount
fiat_converter = CryptoToFiatConverter() fiat_converter = CryptoToFiatConverter()
stake_amount_fiat = fiat_converter.convert_amount( stake_amount_fiat = fiat_converter.convert_amount(
stake_amount, real_stake_amount,
_CONF['stake_currency'], _CONF['stake_currency'],
_CONF['fiat_display_currency'] _CONF['fiat_display_currency']
) )
@ -419,7 +510,7 @@ def create_trade(stake_amount: float, interval: int) -> bool:
exchange.get_name().upper(), exchange.get_name().upper(),
pair.replace('_', '/'), pair.replace('_', '/'),
exchange.get_pair_detail_url(pair), exchange.get_pair_detail_url(pair),
buy_limit, stake_amount, _CONF['stake_currency'], buy_limit, real_stake_amount, _CONF['stake_currency'],
stake_amount_fiat, _CONF['fiat_display_currency'] stake_amount_fiat, _CONF['fiat_display_currency']
)) ))
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL

View File

@ -5,6 +5,7 @@ import logging
import time import time
import os import os
import re import re
import sys
from datetime import datetime from datetime import datetime
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict, List
@ -91,8 +92,14 @@ def load_config(path: str) -> Dict:
:param path: path as str :param path: path as str
:return: configuration as dictionary :return: configuration as dictionary
""" """
with open(path) as file: try:
conf = json.load(file) with open(path) as file:
conf = json.load(file)
except json.decoder.JSONDecodeError as e:
logger.fatal('Syntax configuration error: invalid JSON format in {path}: {error}'.format(
path=path, error=e))
sys.exit(1)
if 'internals' not in conf: if 'internals' not in conf:
conf['internals'] = {} conf['internals'] = {}
logger.info('Validating configuration ...') logger.info('Validating configuration ...')

View File

@ -6,12 +6,12 @@ from typing import Dict, Tuple
import arrow import arrow
from pandas import DataFrame, Series from pandas import DataFrame, Series
from tabulate import tabulate from tabulate import tabulate
from freqtrade import OperationalException
import freqtrade.misc as misc import freqtrade.misc as misc
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import exchange from freqtrade import exchange
from freqtrade.analyze import populate_buy_trend, populate_sell_trend from freqtrade.analyze import populate_buy_trend, populate_sell_trend
from freqtrade.exchange import Bittrex
from freqtrade.main import should_sell from freqtrade.main import should_sell
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy import Strategy from freqtrade.strategy.strategy import Strategy
@ -105,16 +105,21 @@ def backtest(args) -> DataFrame:
sell_profit_only: sell if profit only sell_profit_only: sell if profit only
use_sell_signal: act on sell-signal use_sell_signal: act on sell-signal
stoploss: use stoploss stoploss: use stoploss
exchange_name: which exchange to use
:return: DataFrame :return: DataFrame
""" """
processed = args['processed'] processed = args['processed']
max_open_trades = args.get('max_open_trades', 0) max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', True) realistic = args.get('realistic', True)
record = args.get('record', None) record = args.get('record', None)
exchange_name = args.get('exchange_name', None)
records = [] records = []
trades = [] trades = []
trade_count_lock: dict = {} trade_count_lock: dict = {}
exchange._API = Bittrex({'key': '', 'secret': ''})
exchange_class = exchange.Exchanges[exchange_name.upper()].value
exchange._API = exchange_class({'key': '', 'secret': ''})
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 pair_data['buy'], pair_data['sell'] = 0, 0
ticker = populate_sell_trend(populate_buy_trend(pair_data)) ticker = populate_sell_trend(populate_buy_trend(pair_data))
@ -167,8 +172,6 @@ def start(args):
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
) )
exchange._API = Bittrex({'key': '', 'secret': ''})
logger.info('Using config: %s ...', args.config) logger.info('Using config: %s ...', args.config)
config = misc.load_config(args.config) config = misc.load_config(args.config)
@ -184,6 +187,15 @@ def start(args):
logger.info('Using ticker_interval: %d ...', strategy.ticker_interval) logger.info('Using ticker_interval: %d ...', strategy.ticker_interval)
exchange_name = config['exchange']['name']
try:
exchange_class = exchange.Exchanges[exchange_name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(
exchange_name))
exchange._API = exchange_class({'key': '', 'secret': ''})
data = {} data = {}
pairs = config['exchange']['pair_whitelist'] pairs = config['exchange']['pair_whitelist']
if args.live: if args.live:
@ -227,7 +239,8 @@ def start(args):
'sell_profit_only': sell_profit_only, 'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal, 'use_sell_signal': use_sell_signal,
'stoploss': strategy.stoploss, 'stoploss': strategy.stoploss,
'record': args.export 'record': args.export,
'exchange_name': exchange_name
}) })
logger.info( logger.info(
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa '\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa

View File

@ -19,10 +19,10 @@ from hyperopt.mongoexp import MongoTrials
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade import OperationalException
# Monkey patch config # Monkey patch config
from freqtrade import main # noqa; noqa from freqtrade import main # noqa; noqa
from freqtrade import exchange, misc, optimize from freqtrade import exchange, misc, optimize
from freqtrade.exchange import Bittrex
from freqtrade.misc import load_config from freqtrade.misc import load_config
from freqtrade.optimize import backtesting from freqtrade.optimize import backtesting
from freqtrade.optimize.backtesting import backtest from freqtrade.optimize.backtesting import backtest
@ -401,7 +401,8 @@ def optimizer(params):
results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'], results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'],
'processed': PROCESSED, 'processed': PROCESSED,
'stoploss': params['stoploss']}) 'stoploss': params['stoploss'],
'exchange_name': _CONFIG['exchange']['name']})
result_explanation = format_results(results) result_explanation = format_results(results)
total_profit = results.profit_percent.sum() total_profit = results.profit_percent.sum()
@ -445,12 +446,10 @@ def format_results(results: DataFrame):
def start(args): def start(args):
global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES, _CONFIG
TOTAL_TRIES = args.epochs TOTAL_TRIES = args.epochs
exchange._API = Bittrex({'key': '', 'secret': ''})
# Initialize logger # Initialize logger
logging.basicConfig( logging.basicConfig(
level=args.loglevel, level=args.loglevel,
@ -458,18 +457,27 @@ def start(args):
) )
logger.info('Using config: %s ...', args.config) logger.info('Using config: %s ...', args.config)
config = load_config(args.config) _CONFIG = load_config(args.config)
pairs = config['exchange']['pair_whitelist'] pairs = _CONFIG['exchange']['pair_whitelist']
exchange_name = _CONFIG['exchange']['name']
try:
exchange_class = exchange.Exchanges[exchange_name.upper()].value
except KeyError:
raise OperationalException('Exchange {} is not supported'.format(
exchange_name))
exchange._API = exchange_class({'key': '', 'secret': ''})
# If -i/--ticker-interval is use we override the configuration parameter # If -i/--ticker-interval is use we override the configuration parameter
# (that will override the strategy configuration) # (that will override the strategy configuration)
if args.ticker_interval: if args.ticker_interval:
config.update({'ticker_interval': args.ticker_interval}) _CONFIG.update({'ticker_interval': args.ticker_interval})
# init the strategy to use # init the strategy to use
config.update({'strategy': args.strategy}) _CONFIG.update({'strategy': args.strategy})
strategy = Strategy() strategy = Strategy()
strategy.init(config) strategy.init(_CONFIG)
timerange = misc.parse_timerange(args.timerange) timerange = misc.parse_timerange(args.timerange)
data = optimize.load_data(args.datadir, pairs=pairs, data = optimize.load_data(args.datadir, pairs=pairs,

View File

@ -133,7 +133,7 @@ class Trade(_DECL_BASE):
self.is_open = False self.is_open = False
self.open_order_id = None self.open_order_id = None
logger.info( logger.info(
'Marking %s as closed as the trade is fulfilled and found no open orders for it.', 'Marking %s as closed since found no open orders for it.',
self self
) )

View File

@ -84,7 +84,7 @@ def rpc_trade_status():
for trade in trades: for trade in trades:
order = None order = None
if trade.open_order_id: if trade.open_order_id:
order = exchange.get_order(trade.open_order_id) order = exchange.get_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user # calculate profit and send message to user
current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_rate = exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
@ -340,11 +340,11 @@ def rpc_forcesell(trade_id) -> None:
def _exec_forcesell(trade: Trade) -> str: def _exec_forcesell(trade: Trade) -> str:
# Check if there is there is an open order # Check if there is there is an open order
if trade.open_order_id: if trade.open_order_id:
order = exchange.get_order(trade.open_order_id) order = exchange.get_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade # Cancel open LIMIT_BUY orders and close trade
if order and not order['closed'] and order['type'] == 'LIMIT_BUY': if order and not order['closed'] and order['type'] == 'LIMIT_BUY':
exchange.cancel_order(trade.open_order_id) exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('rate') or trade.open_rate) trade.close(order.get('rate') or trade.open_rate)
# TODO: sell amount which has been bought already # TODO: sell amount which has been bought already
return return

View File

@ -184,6 +184,19 @@ def limit_buy_order_old_partial():
} }
@pytest.fixture
def limit_sell_order_old_partial():
return {
'id': 'mocked_limit_sell_old_partial',
'type': 'LIMIT_SELL',
'pair': 'BTC_ETH',
'opened': str(arrow.utcnow().shift(minutes=-601).datetime),
'rate': 0.00001099,
'amount': 90.99181073,
'remaining': 67.99181073,
}
@pytest.fixture @pytest.fixture
def limit_sell_order(): def limit_sell_order():
return { return {

View File

@ -227,7 +227,7 @@ def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
assert cancel_order(order_id='123') is None assert cancel_order(order_id='123', pair='ABC_XYZ') is None
# Ensure that if not dry_run, we should call API # Ensure that if not dry_run, we should call API
@ -237,7 +237,7 @@ def test_cancel_order(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.cancel_order = MagicMock(return_value=123) api_mock.cancel_order = MagicMock(return_value=123)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
assert cancel_order(order_id='_') == 123 assert cancel_order(order_id='_', pair='ABC_XYZ') == 123
def test_get_order(default_conf, mocker): def test_get_order(default_conf, mocker):
@ -245,16 +245,18 @@ def test_get_order(default_conf, mocker):
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
order.pair = 'ABC_XYZ'
exchange._DRY_RUN_OPEN_ORDERS['X'] = order exchange._DRY_RUN_OPEN_ORDERS['X'] = order
print(exchange.get_order('X')) print(exchange.get_order('X', 'ABC_XYZ'))
assert exchange.get_order('X').myid == 123 assert exchange.get_order('X', 'ABC_XYZ').myid == 123
assert exchange.get_order('X', 'ABC_XYZ').pair == 'ABC_XYZ'
default_conf['dry_run'] = False default_conf['dry_run'] = False
mocker.patch.dict('freqtrade.exchange._CONF', default_conf) mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
api_mock = MagicMock() api_mock = MagicMock()
api_mock.get_order = MagicMock(return_value=456) api_mock.get_order = MagicMock(return_value=456)
mocker.patch('freqtrade.exchange._API', api_mock) mocker.patch('freqtrade.exchange._API', api_mock)
assert exchange.get_order('X') == 456 assert exchange.get_order('X', 'ABC_XYZ') == 456
def test_get_name(default_conf, mocker): def test_get_name(default_conf, mocker):

View File

@ -0,0 +1,353 @@
# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument
from unittest.mock import MagicMock
import pytest
import datetime
import dateutil
from freqtrade.exchange.binance import Binance
import freqtrade.exchange.binance as bin
# Eat this flake8
# +------------------+
# | binance.Binance |
# +------------------+
# |
# (mock Fake_binance)
# |
# +-----------------------------+
# | freqtrade.exchange.Binance |
# +-----------------------------+
# Call into Binance will flow up to the
# external package binance.Binance.
# By inserting a mock, we redirect those
# calls.
# The faked binance API is called just 'fb'
# The freqtrade.exchange.Binance is a
# wrapper, and is called 'wb'
def _stub_config():
return {'key': '',
'secret': ''}
class FakeBinance():
def __init__(self, success=True):
self.success = True # Believe in yourself
self.result = None
self.get_ticker_call_count = 0
# This is really ugly, doing side-effect during instance creation
# But we're allowed to in testing-code
bin._API = MagicMock()
bin._API.order_limit_buy = self.fake_order_limit_buy
bin._API.order_limit_sell = self.fake_order_limit_sell
bin._API.get_asset_balance = self.fake_get_asset_balance
bin._API.get_account = self.fake_get_account
bin._API.get_ticker = self.fake_get_ticker
bin._API.get_klines = self.fake_get_klines
bin._API.get_all_orders = self.fake_get_all_orders
bin._API.cancel_order = self.fake_cancel_order
bin._API.get_all_tickers = self.fake_get_all_tickers
bin._API.get_exchange_info = self.fake_get_exchange_info
bin._EXCHANGE_CONF = {'stake_currency': 'BTC'}
def fake_order_limit_buy(self, symbol, quantity, price):
return {"symbol": "BTCETH",
"orderId": 42,
"clientOrderId": "6gCrw2kRUAF9CvJDGP16IP",
"transactTime": 1507725176595,
"price": "0.00000000",
"origQty": "10.00000000",
"executedQty": "10.00000000",
"status": "FILLED",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY"}
def fake_order_limit_sell(self, symbol, quantity, price):
return {"symbol": "BTCETH",
"orderId": 42,
"clientOrderId": "6gCrw2kRUAF9CvJDGP16IP",
"transactTime": 1507725176595,
"price": "0.00000000",
"origQty": "10.00000000",
"executedQty": "10.00000000",
"status": "FILLED",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "SELL"}
def fake_get_asset_balance(self, asset):
return {
"asset": "BTC",
"free": "4723846.89208129",
"locked": "0.00000000"
}
def fake_get_account(self):
return {
"makerCommission": 15,
"takerCommission": 15,
"buyerCommission": 0,
"sellerCommission": 0,
"canTrade": True,
"canWithdraw": True,
"canDeposit": True,
"balances": [
{
"asset": "BTC",
"free": "4723846.89208129",
"locked": "0.00000000"
},
{
"asset": "LTC",
"free": "4763368.68006011",
"locked": "0.00000000"
}
]
}
def fake_get_ticker(self, symbol=None):
self.get_ticker_call_count += 1
t = {"symbol": "ETHBTC",
"priceChange": "-94.99999800",
"priceChangePercent": "-95.960",
"weightedAvgPrice": "0.29628482",
"prevClosePrice": "0.10002000",
"lastPrice": "4.00000200",
"bidPrice": "4.00000000",
"askPrice": "4.00000200",
"openPrice": "99.00000000",
"highPrice": "100.00000000",
"lowPrice": "0.10000000",
"volume": "8913.30000000",
"openTime": 1499783499040,
"closeTime": 1499869899040,
"fristId": 28385,
"lastId": 28460,
"count": 76}
return t if symbol else [t]
def fake_get_klines(self, symbol, interval):
return [[0,
"0",
"0",
"0",
"0",
"0",
0,
"0",
0,
"0",
"0",
"0"]]
def fake_get_all_orders(self, symbol, orderId):
return [{"symbol": "LTCBTC",
"orderId": 42,
"clientOrderId": "myOrder1",
"price": "0.1",
"origQty": "1.0",
"executedQty": "0.0",
"status": "NEW",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"stopPrice": "0.0",
"icebergQty": "0.0",
"status_code": "200",
"time": 1499827319559}]
def fake_cancel_order(self, symbol, orderId):
return {"symbol": "LTCBTC",
"origClientOrderId": "myOrder1",
"orderId": 42,
"clientOrderId": "cancelMyOrder1"}
def fake_get_all_tickers(self):
return [{"symbol": "LTCBTC",
"price": "4.00000200"},
{"symbol": "ETHBTC",
"price": "0.07946600"}]
def fake_get_exchange_info(self):
return {
"timezone": "UTC",
"serverTime": 1508631584636,
"rateLimits": [
{
"rateLimitType": "REQUESTS",
"interval": "MINUTE",
"limit": 1200
},
{
"rateLimitType": "ORDERS",
"interval": "SECOND",
"limit": 10
},
{
"rateLimitType": "ORDERS",
"interval": "DAY",
"limit": 100000
}
],
"exchangeFilters": [],
"symbols": [
{
"symbol": "ETHBTC",
"status": "TRADING",
"baseAsset": "ETH",
"baseAssetPrecision": 8,
"quoteAsset": "BTC",
"quotePrecision": 8,
"orderTypes": ["LIMIT", "MARKET"],
"icebergAllowed": False,
"filters": [
{
"filterType": "PRICE_FILTER",
"minPrice": "0.00000100",
"maxPrice": "100000.00000000",
"tickSize": "0.00000100"
}, {
"filterType": "LOT_SIZE",
"minQty": "0.00100000",
"maxQty": "100000.00000000",
"stepSize": "0.00100000"
}, {
"filterType": "MIN_NOTIONAL",
"minNotional": "0.00100000"
}
]
}
]
}
# The freqtrade.exchange.binance is called wrap_binance
# to not confuse naming with binance.binance
def make_wrap_binance():
conf = _stub_config()
wb = bin.Binance(conf)
return wb
def test_exchange_binance_class():
conf = _stub_config()
b = Binance(conf)
assert isinstance(b, Binance)
slots = dir(b)
for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances',
'get_ticker', 'get_ticker_history', 'get_order',
'cancel_order', 'get_pair_detail_url', 'get_markets',
'get_market_summaries', 'get_wallet_health']:
assert name in slots
# FIX: ensure that the slot is also a method in the class
# getattr(b, name) => bound method Binance.buy
# type(getattr(b, name)) => class 'method'
def test_exchange_binance_fee():
fee = Binance.fee.__get__(Binance)
assert fee >= 0 and fee < 0.1 # Fee is 0-10 %
def test_exchange_binance_buy_good():
wb = make_wrap_binance()
fb = FakeBinance()
uuid = wb.buy('BTC_ETH', 1, 1)
assert uuid == fb.fake_order_limit_buy(1, 2, 3)['orderId']
with pytest.raises(IndexError, match=r'.*'):
wb.buy('BAD', 1, 1)
def test_exchange_binance_sell_good():
wb = make_wrap_binance()
fb = FakeBinance()
uuid = wb.sell('BTC_ETH', 1, 1)
assert uuid == fb.fake_order_limit_sell(1, 2, 3)['orderId']
with pytest.raises(IndexError, match=r'.*'):
uuid = wb.sell('BAD', 1, 1)
def test_exchange_binance_get_balance():
wb = make_wrap_binance()
fb = FakeBinance()
bal = wb.get_balance('BTC')
assert str(bal) == fb.fake_get_asset_balance(1)['free']
def test_exchange_binance_get_balances():
wb = make_wrap_binance()
fb = FakeBinance()
bals = wb.get_balances()
assert len(bals) <= len(fb.fake_get_account()['balances'])
def test_exchange_binance_get_ticker():
wb = make_wrap_binance()
FakeBinance()
# Poll ticker, which updates the cache
tick = wb.get_ticker('BTC_ETH')
for x in ['bid', 'ask', 'last']:
assert x in tick
def test_exchange_binance_get_ticker_history_intervals():
wb = make_wrap_binance()
FakeBinance()
for tick_interval in [1, 5]:
h = wb.get_ticker_history('BTC_ETH', tick_interval)
assert type(dateutil.parser.parse(h[0]['T'])) is datetime.datetime
del h[0]['T']
assert [{'O': 0.0, 'H': 0.0,
'L': 0.0, 'C': 0.0,
'V': 0.0, 'BV': 0.0}] == h
def test_exchange_binance_get_ticker_history():
wb = make_wrap_binance()
FakeBinance()
assert wb.get_ticker_history('BTC_ETH', 5)
def test_exchange_binance_get_order():
wb = make_wrap_binance()
FakeBinance()
order = wb.get_order('42', 'BTC_LTC')
assert order['id'] == 42
def test_exchange_binance_cancel_order():
wb = make_wrap_binance()
FakeBinance()
assert wb.cancel_order('42', 'BTC_LTC')['orderId'] == 42
def test_exchange_get_pair_detail_url():
wb = make_wrap_binance()
FakeBinance()
assert wb.get_pair_detail_url('BTC_ETH')
def test_exchange_get_markets():
wb = make_wrap_binance()
FakeBinance()
x = wb.get_markets()
assert len(x) > 0
def test_exchange_get_market_summaries():
wb = make_wrap_binance()
FakeBinance()
assert wb.get_market_summaries()
def test_exchange_get_wallet_health():
wb = make_wrap_binance()
FakeBinance()
x = wb.get_wallet_health()
assert x[0]['Currency'] == 'ETH'

View File

@ -264,27 +264,27 @@ def test_exchange_bittrex_get_ticker_history():
def test_exchange_bittrex_get_order(): def test_exchange_bittrex_get_order():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
fb = FakeBittrex() fb = FakeBittrex()
order = wb.get_order('someUUID') order = wb.get_order('someUUID', 'somePAIR')
assert order['id'] == 'ABC123' assert order['id'] == 'ABC123'
fb.success = False fb.success = False
with pytest.raises(btx.OperationalException, match=r'lost'): with pytest.raises(btx.OperationalException, match=r'lost'):
wb.get_order('someUUID') wb.get_order('someUUID', 'somePAIR')
def test_exchange_bittrex_cancel_order(): def test_exchange_bittrex_cancel_order():
wb = make_wrap_bittrex() wb = make_wrap_bittrex()
fb = FakeBittrex() fb = FakeBittrex()
wb.cancel_order('someUUID') wb.cancel_order('someUUID', 'somePAIR')
with pytest.raises(btx.OperationalException, match=r'no such order'): with pytest.raises(btx.OperationalException, match=r'no such order'):
fb.success = False fb.success = False
wb.cancel_order('someUUID') wb.cancel_order('someUUID', 'somePAIR')
# Note: this can be a bug in exchange.bittrex._validate_response # Note: this can be a bug in exchange.bittrex._validate_response
with pytest.raises(KeyError): with pytest.raises(KeyError):
fb.result = {'success': False} # message is missing! fb.result = {'success': False} # message is missing!
wb.cancel_order('someUUID') wb.cancel_order('someUUID', 'somePAIR')
with pytest.raises(btx.OperationalException, match=r'foo'): with pytest.raises(btx.OperationalException, match=r'foo'):
fb.result = {'success': False, 'message': 'foo'} fb.result = {'success': False, 'message': 'foo'}
wb.cancel_order('someUUID') wb.cancel_order('someUUID', 'somePAIR')
def test_exchange_get_pair_detail_url(): def test_exchange_get_pair_detail_url():

View File

@ -5,7 +5,6 @@ import math
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd import pandas as pd
from freqtrade import exchange, optimize from freqtrade import exchange, optimize
from freqtrade.exchange import Bittrex
from freqtrade.optimize import preprocess from freqtrade.optimize import preprocess
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
import freqtrade.optimize.backtesting as backtesting import freqtrade.optimize.backtesting as backtesting
@ -47,29 +46,31 @@ def test_get_timeframe(default_strategy):
def test_backtest(default_strategy, default_conf, mocker): def test_backtest(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'], for exch in exchange.Exchanges.__members__.keys():
'processed': optimize.preprocess(data), results = backtest({'stake_amount': default_conf['stake_amount'],
'max_open_trades': 10, 'processed': optimize.preprocess(data),
'realistic': True}) 'max_open_trades': 10,
assert not results.empty 'realistic': True,
'exchange_name': exch.lower()})
assert not results.empty
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker): def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
exchange._API = Bittrex({'key': '', 'secret': ''})
# Run a backtesting for an exiting 5min ticker_interval # Run a backtesting for an exiting 5min ticker_interval
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST']) data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'], for exch in exchange.Exchanges.__members__.keys():
'processed': optimize.preprocess(data), results = backtest({'stake_amount': default_conf['stake_amount'],
'max_open_trades': 1, 'processed': optimize.preprocess(data),
'realistic': True}) 'max_open_trades': 1,
assert not results.empty 'realistic': True,
'exchange_name': exch.lower()})
assert not results.empty
def load_data_test(what): def load_data_test(what):
@ -122,7 +123,8 @@ def simple_backtest(config, contour, num_results):
results = backtest({'stake_amount': config['stake_amount'], results = backtest({'stake_amount': config['stake_amount'],
'processed': processed, 'processed': processed,
'max_open_trades': 1, 'max_open_trades': 1,
'realistic': True}) 'realistic': True,
'exchange_name': config['exchange']['name']})
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results assert len(results) == num_results
@ -135,11 +137,13 @@ def test_backtest2(default_conf, mocker, default_strategy):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH']) data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
data = trim_dictlist(data, -200) data = trim_dictlist(data, -200)
results = backtest({'stake_amount': default_conf['stake_amount'], for exch in exchange.Exchanges.__members__.keys():
'processed': optimize.preprocess(data), results = backtest({'stake_amount': default_conf['stake_amount'],
'max_open_trades': 10, 'processed': optimize.preprocess(data),
'realistic': True}) 'max_open_trades': 10,
assert not results.empty 'realistic': True,
'exchange_name': exch.lower()})
assert not results.empty
def test_processed(default_conf, mocker, default_strategy): def test_processed(default_conf, mocker, default_strategy):

View File

@ -220,7 +220,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
get_ticker=ticker, get_ticker=ticker,
buy=MagicMock(return_value='mocked_limit_buy'), buy=MagicMock(return_value='mocked_limit_buy'),
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)) get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5))
with pytest.raises(DependencyException, match=r'.*stake amount.*'): with pytest.raises(DependencyException, match=r'.*not enough funds.*'):
create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval'])) create_trade(default_conf['stake_amount'], int(default_conf['ticker_interval']))
@ -535,14 +535,50 @@ def test_handle_timedout_limit_sell(mocker):
'amount': 1} 'amount': 1}
assert main.handle_timedout_limit_sell(trade, order) assert main.handle_timedout_limit_sell(trade, order)
assert cancel_order.call_count == 1 assert cancel_order.call_count == 1
order['amount'] = 2
assert not main.handle_timedout_limit_sell(trade, order)
# Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order.call_count == 1
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, def test_check_handle_timedout_partial_sell(default_conf, ticker, limit_sell_order_old_partial,
mocker): mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock()
get_trade_qty_mock = MagicMock(return_value=(None, None, None))
mocker.patch('freqtrade.rpc.init', MagicMock())
rpc_mock = mocker.patch('freqtrade.main.rpc.send_msg', MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old_partial),
cancel_order=cancel_order_mock,
get_trade_qty=get_trade_qty_mock)
init(default_conf, create_engine('sqlite://'))
trade_sell = Trade(
pair='BTC_ETH',
open_rate=0.00001099,
exchange='BITTREX',
open_order_id='123456789',
amount=90.99181073,
fee=0.0,
stake_amount=1,
open_date=arrow.utcnow().shift(minutes=-601).datetime,
is_open=True
)
Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit
# note this is for a partially-complete sell order
check_handle_timedout(600)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_sell.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 67.99181073
assert trades[0].stake_amount == trade_sell.open_rate * trades[0].amount
def test_check_handle_timedout_partial_buy(default_conf, ticker, limit_buy_order_old_partial,
mocker):
mocker.patch.dict('freqtrade.main._CONF', default_conf) mocker.patch.dict('freqtrade.main._CONF', default_conf)
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch('freqtrade.rpc.init', MagicMock()) mocker.patch('freqtrade.rpc.init', MagicMock())

View File

@ -1,4 +1,5 @@
python-bittrex==0.3.0 python-bittrex==0.3.0
python-binance==0.6.1
SQLAlchemy==1.2.2 SQLAlchemy==1.2.2
python-telegram-bot==9.0.0 python-telegram-bot==9.0.0
arrow==0.12.1 arrow==0.12.1

View File

@ -22,6 +22,7 @@ setup(name='freqtrade',
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[ install_requires=[
'python-bittrex', 'python-bittrex',
'python-binance',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot', 'python-telegram-bot',
'arrow', 'arrow',

View File

@ -98,7 +98,7 @@ function config_generator () {
read -p "Fiat currency: (Default: USD) " fiat_currency read -p "Fiat currency: (Default: USD) " fiat_currency
echo "------------------------" echo "------------------------"
echo "Bittrex config generator" echo "Exchange config generator"
echo "------------------------" echo "------------------------"
echo echo
read -p "Exchange API key: " api_key read -p "Exchange API key: " api_key