Merge 56e6c563aa
into 53b1f7ac4d
This commit is contained in:
commit
3e1bddbdd1
@ -71,7 +71,7 @@ your current trades.
|
||||
|
||||
### Exchange supported
|
||||
- [x] Bittrex
|
||||
- [ ] Binance
|
||||
- [x] Binance
|
||||
- [ ] Others
|
||||
|
||||
## Quick start
|
||||
|
@ -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.
|
||||
| `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.
|
||||
| `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.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.
|
||||
@ -94,7 +94,7 @@ creating trades.
|
||||
"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
|
||||
"exchange": {
|
||||
"name": "bittrex",
|
||||
@ -120,7 +120,7 @@ you run it in production mode.
|
||||
"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
|
||||
"exchange": {
|
||||
"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).
|
||||
|
||||
|
||||
|
@ -1,15 +1,23 @@
|
||||
# Pre-requisite
|
||||
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).
|
||||
|
||||
## 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)
|
||||
|
||||
## Setup your Bittrex account
|
||||
## Setup your exchange account
|
||||
|
||||
### Bittrex
|
||||
*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
|
||||
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
|
||||
|
@ -11,6 +11,7 @@ from cachetools import cached, TTLCache
|
||||
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade.exchange.bittrex import Bittrex
|
||||
from freqtrade.exchange.binance import Binance
|
||||
from freqtrade.exchange.interface import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -28,6 +29,7 @@ class Exchanges(enum.Enum):
|
||||
Maps supported exchange names to correspondent classes.
|
||||
"""
|
||||
BITTREX = Bittrex
|
||||
BINANCE = Binance
|
||||
|
||||
|
||||
def init(config: dict) -> None:
|
||||
@ -54,6 +56,7 @@ def init(config: dict) -> None:
|
||||
except KeyError:
|
||||
raise OperationalException('Exchange {} is not supported'.format(name))
|
||||
|
||||
exchange_config['stake_currency'] = config['stake_currency']
|
||||
_API = exchange_class(exchange_config)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
def cancel_order(order_id: str) -> None:
|
||||
def cancel_order(order_id: str, pair: str) -> None:
|
||||
if _CONF['dry_run']:
|
||||
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']:
|
||||
order = _DRY_RUN_OPEN_ORDERS[order_id]
|
||||
order.update({
|
||||
@ -158,7 +161,7 @@ def get_order(order_id: str) -> Dict:
|
||||
})
|
||||
return order
|
||||
|
||||
return _API.get_order(order_id)
|
||||
return _API.get_order(order_id, pair)
|
||||
|
||||
|
||||
def get_pair_detail_url(pair: str) -> str:
|
||||
@ -183,3 +186,7 @@ def get_fee() -> float:
|
||||
|
||||
def get_wallet_health() -> List[Dict]:
|
||||
return _API.get_wallet_health()
|
||||
|
||||
|
||||
def get_trade_qty(pair: str) -> tuple:
|
||||
return _API.get_trade_qty(pair)
|
||||
|
549
freqtrade/exchange/binance.py
Normal file
549
freqtrade/exchange/binance.py
Normal 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
|
@ -157,7 +157,8 @@ class Bittrex(Exchange):
|
||||
|
||||
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)
|
||||
if not data['success']:
|
||||
Bittrex._validate_response(data)
|
||||
@ -176,7 +177,8 @@ class Bittrex(Exchange):
|
||||
'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)
|
||||
if not data['success']:
|
||||
Bittrex._validate_response(data)
|
||||
@ -212,3 +214,6 @@ class Bittrex(Exchange):
|
||||
'LastChecked': entry['Health']['LastChecked'],
|
||||
'Notice': entry['Currency'].get('Notice'),
|
||||
} for entry in data['result']]
|
||||
|
||||
def get_trade_qty(self, pair: str) -> tuple:
|
||||
return (None, None, None)
|
||||
|
@ -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.
|
||||
:param order_id: ID as str
|
||||
@ -111,7 +111,7 @@ class Exchange(ABC):
|
||||
"""
|
||||
|
||||
@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.
|
||||
: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 )
|
||||
...
|
||||
"""
|
||||
|
@ -84,7 +84,7 @@ def process_maybe_execute_sell(trade: Trade, interval: int) -> bool:
|
||||
if trade.open_order_id:
|
||||
# Update trade with order values
|
||||
logger.info('Got open order for %s', trade)
|
||||
trade.update(exchange.get_order(trade.open_order_id))
|
||||
trade.update(exchange.get_order(trade.open_order_id, trade.pair))
|
||||
|
||||
if trade.is_open and trade.open_order_id is None:
|
||||
# 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
|
||||
: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 trade is not partially completed, just delete the 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
|
||||
# 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.open_order_id = None
|
||||
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
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
if order['remaining'] == order['amount']:
|
||||
# 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
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
return False
|
||||
# Partial filled sell order timed out
|
||||
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:
|
||||
@ -207,7 +265,7 @@ def check_handle_timedout(timeoutvalue: int) -> None:
|
||||
|
||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
||||
try:
|
||||
order = exchange.get_order(trade.open_order_id)
|
||||
order = exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except requests.exceptions.RequestException:
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
@ -378,10 +436,16 @@ def create_trade(stake_amount: float, interval: int) -> bool:
|
||||
stake_amount
|
||||
)
|
||||
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(
|
||||
'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
|
||||
@ -401,15 +465,42 @@ def create_trade(stake_amount: float, interval: int) -> bool:
|
||||
else:
|
||||
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))
|
||||
|
||||
# Calculate base amount
|
||||
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()
|
||||
stake_amount_fiat = fiat_converter.convert_amount(
|
||||
stake_amount,
|
||||
real_stake_amount,
|
||||
_CONF['stake_currency'],
|
||||
_CONF['fiat_display_currency']
|
||||
)
|
||||
@ -419,7 +510,7 @@ def create_trade(stake_amount: float, interval: int) -> bool:
|
||||
exchange.get_name().upper(),
|
||||
pair.replace('_', '/'),
|
||||
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']
|
||||
))
|
||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||
|
@ -5,6 +5,7 @@ import logging
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
@ -91,8 +92,14 @@ def load_config(path: str) -> Dict:
|
||||
:param path: path as str
|
||||
:return: configuration as dictionary
|
||||
"""
|
||||
with open(path) as file:
|
||||
conf = json.load(file)
|
||||
try:
|
||||
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:
|
||||
conf['internals'] = {}
|
||||
logger.info('Validating configuration ...')
|
||||
|
@ -6,12 +6,12 @@ from typing import Dict, Tuple
|
||||
import arrow
|
||||
from pandas import DataFrame, Series
|
||||
from tabulate import tabulate
|
||||
from freqtrade import OperationalException
|
||||
|
||||
import freqtrade.misc as misc
|
||||
import freqtrade.optimize as optimize
|
||||
from freqtrade import exchange
|
||||
from freqtrade.analyze import populate_buy_trend, populate_sell_trend
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.main import should_sell
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.strategy.strategy import Strategy
|
||||
@ -105,16 +105,21 @@ def backtest(args) -> DataFrame:
|
||||
sell_profit_only: sell if profit only
|
||||
use_sell_signal: act on sell-signal
|
||||
stoploss: use stoploss
|
||||
exchange_name: which exchange to use
|
||||
:return: DataFrame
|
||||
"""
|
||||
processed = args['processed']
|
||||
max_open_trades = args.get('max_open_trades', 0)
|
||||
realistic = args.get('realistic', True)
|
||||
record = args.get('record', None)
|
||||
exchange_name = args.get('exchange_name', None)
|
||||
records = []
|
||||
trades = []
|
||||
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():
|
||||
pair_data['buy'], pair_data['sell'] = 0, 0
|
||||
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',
|
||||
)
|
||||
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
logger.info('Using config: %s ...', args.config)
|
||||
config = misc.load_config(args.config)
|
||||
|
||||
@ -184,6 +187,15 @@ def start(args):
|
||||
|
||||
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 = {}
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
if args.live:
|
||||
@ -227,7 +239,8 @@ def start(args):
|
||||
'sell_profit_only': sell_profit_only,
|
||||
'use_sell_signal': use_sell_signal,
|
||||
'stoploss': strategy.stoploss,
|
||||
'record': args.export
|
||||
'record': args.export,
|
||||
'exchange_name': exchange_name
|
||||
})
|
||||
logger.info(
|
||||
'\n==================================== BACKTESTING REPORT ====================================\n%s', # noqa
|
||||
|
@ -19,10 +19,10 @@ from hyperopt.mongoexp import MongoTrials
|
||||
from pandas import DataFrame
|
||||
|
||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||
from freqtrade import OperationalException
|
||||
# Monkey patch config
|
||||
from freqtrade import main # noqa; noqa
|
||||
from freqtrade import exchange, misc, optimize
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.misc import load_config
|
||||
from freqtrade.optimize import backtesting
|
||||
from freqtrade.optimize.backtesting import backtest
|
||||
@ -401,7 +401,8 @@ def optimizer(params):
|
||||
|
||||
results = backtest({'stake_amount': OPTIMIZE_CONFIG['stake_amount'],
|
||||
'processed': PROCESSED,
|
||||
'stoploss': params['stoploss']})
|
||||
'stoploss': params['stoploss'],
|
||||
'exchange_name': _CONFIG['exchange']['name']})
|
||||
result_explanation = format_results(results)
|
||||
|
||||
total_profit = results.profit_percent.sum()
|
||||
@ -445,12 +446,10 @@ def format_results(results: DataFrame):
|
||||
|
||||
|
||||
def start(args):
|
||||
global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES
|
||||
global TOTAL_TRIES, PROCESSED, TRIALS, _CURRENT_TRIES, _CONFIG
|
||||
|
||||
TOTAL_TRIES = args.epochs
|
||||
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
# Initialize logger
|
||||
logging.basicConfig(
|
||||
level=args.loglevel,
|
||||
@ -458,18 +457,27 @@ def start(args):
|
||||
)
|
||||
|
||||
logger.info('Using config: %s ...', args.config)
|
||||
config = load_config(args.config)
|
||||
pairs = config['exchange']['pair_whitelist']
|
||||
_CONFIG = load_config(args.config)
|
||||
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
|
||||
# (that will override the strategy configuration)
|
||||
if args.ticker_interval:
|
||||
config.update({'ticker_interval': args.ticker_interval})
|
||||
_CONFIG.update({'ticker_interval': args.ticker_interval})
|
||||
|
||||
# init the strategy to use
|
||||
config.update({'strategy': args.strategy})
|
||||
_CONFIG.update({'strategy': args.strategy})
|
||||
strategy = Strategy()
|
||||
strategy.init(config)
|
||||
strategy.init(_CONFIG)
|
||||
|
||||
timerange = misc.parse_timerange(args.timerange)
|
||||
data = optimize.load_data(args.datadir, pairs=pairs,
|
||||
|
@ -133,7 +133,7 @@ class Trade(_DECL_BASE):
|
||||
self.is_open = False
|
||||
self.open_order_id = None
|
||||
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
|
||||
)
|
||||
|
||||
|
@ -84,7 +84,7 @@ def rpc_trade_status():
|
||||
for trade in trades:
|
||||
order = None
|
||||
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
|
||||
current_rate = exchange.get_ticker(trade.pair, False)['bid']
|
||||
current_profit = trade.calc_profit_percent(current_rate)
|
||||
@ -340,11 +340,11 @@ def rpc_forcesell(trade_id) -> None:
|
||||
def _exec_forcesell(trade: Trade) -> str:
|
||||
# Check if there is there is an open order
|
||||
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
|
||||
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)
|
||||
# TODO: sell amount which has been bought already
|
||||
return
|
||||
|
@ -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
|
||||
def limit_sell_order():
|
||||
return {
|
||||
|
@ -227,7 +227,7 @@ def test_cancel_order_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
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
|
||||
@ -237,7 +237,7 @@ def test_cancel_order(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(return_value=123)
|
||||
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):
|
||||
@ -245,16 +245,18 @@ def test_get_order(default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
order.pair = 'ABC_XYZ'
|
||||
exchange._DRY_RUN_OPEN_ORDERS['X'] = order
|
||||
print(exchange.get_order('X'))
|
||||
assert exchange.get_order('X').myid == 123
|
||||
print(exchange.get_order('X', 'ABC_XYZ'))
|
||||
assert exchange.get_order('X', 'ABC_XYZ').myid == 123
|
||||
assert exchange.get_order('X', 'ABC_XYZ').pair == 'ABC_XYZ'
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)
|
||||
api_mock = MagicMock()
|
||||
api_mock.get_order = MagicMock(return_value=456)
|
||||
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):
|
||||
|
353
freqtrade/tests/exchange/test_exchange_binance.py
Normal file
353
freqtrade/tests/exchange/test_exchange_binance.py
Normal 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'
|
@ -264,27 +264,27 @@ def test_exchange_bittrex_get_ticker_history():
|
||||
def test_exchange_bittrex_get_order():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
order = wb.get_order('someUUID')
|
||||
order = wb.get_order('someUUID', 'somePAIR')
|
||||
assert order['id'] == 'ABC123'
|
||||
fb.success = False
|
||||
with pytest.raises(btx.OperationalException, match=r'lost'):
|
||||
wb.get_order('someUUID')
|
||||
wb.get_order('someUUID', 'somePAIR')
|
||||
|
||||
|
||||
def test_exchange_bittrex_cancel_order():
|
||||
wb = make_wrap_bittrex()
|
||||
fb = FakeBittrex()
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID', 'somePAIR')
|
||||
with pytest.raises(btx.OperationalException, match=r'no such order'):
|
||||
fb.success = False
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID', 'somePAIR')
|
||||
# Note: this can be a bug in exchange.bittrex._validate_response
|
||||
with pytest.raises(KeyError):
|
||||
fb.result = {'success': False} # message is missing!
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID', 'somePAIR')
|
||||
with pytest.raises(btx.OperationalException, match=r'foo'):
|
||||
fb.result = {'success': False, 'message': 'foo'}
|
||||
wb.cancel_order('someUUID')
|
||||
wb.cancel_order('someUUID', 'somePAIR')
|
||||
|
||||
|
||||
def test_exchange_get_pair_detail_url():
|
||||
|
@ -5,7 +5,6 @@ import math
|
||||
from unittest.mock import MagicMock
|
||||
import pandas as pd
|
||||
from freqtrade import exchange, optimize
|
||||
from freqtrade.exchange import Bittrex
|
||||
from freqtrade.optimize import preprocess
|
||||
from freqtrade.optimize.backtesting import backtest, generate_text_table, get_timeframe
|
||||
import freqtrade.optimize.backtesting as backtesting
|
||||
@ -47,29 +46,31 @@ def test_get_timeframe(default_strategy):
|
||||
|
||||
def test_backtest(default_strategy, default_conf, mocker):
|
||||
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 = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
for exch in exchange.Exchanges.__members__.keys():
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True,
|
||||
'exchange_name': exch.lower()})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_backtest_1min_ticker_interval(default_strategy, default_conf, mocker):
|
||||
mocker.patch.dict('freqtrade.main._CONF', default_conf)
|
||||
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||
|
||||
# Run a backtesting for an exiting 5min ticker_interval
|
||||
data = optimize.load_data(None, ticker_interval=1, pairs=['BTC_UNITEST'])
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
for exch in exchange.Exchanges.__members__.keys():
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 1,
|
||||
'realistic': True,
|
||||
'exchange_name': exch.lower()})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def load_data_test(what):
|
||||
@ -122,7 +123,8 @@ def simple_backtest(config, contour, num_results):
|
||||
results = backtest({'stake_amount': config['stake_amount'],
|
||||
'processed': processed,
|
||||
'max_open_trades': 1,
|
||||
'realistic': True})
|
||||
'realistic': True,
|
||||
'exchange_name': config['exchange']['name']})
|
||||
# results :: <class 'pandas.core.frame.DataFrame'>
|
||||
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)
|
||||
data = optimize.load_data(None, ticker_interval=5, pairs=['BTC_ETH'])
|
||||
data = trim_dictlist(data, -200)
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True})
|
||||
assert not results.empty
|
||||
for exch in exchange.Exchanges.__members__.keys():
|
||||
results = backtest({'stake_amount': default_conf['stake_amount'],
|
||||
'processed': optimize.preprocess(data),
|
||||
'max_open_trades': 10,
|
||||
'realistic': True,
|
||||
'exchange_name': exch.lower()})
|
||||
assert not results.empty
|
||||
|
||||
|
||||
def test_processed(default_conf, mocker, default_strategy):
|
||||
|
@ -220,7 +220,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, mocker):
|
||||
get_ticker=ticker,
|
||||
buy=MagicMock(return_value='mocked_limit_buy'),
|
||||
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']))
|
||||
|
||||
|
||||
@ -535,14 +535,50 @@ def test_handle_timedout_limit_sell(mocker):
|
||||
'amount': 1}
|
||||
assert main.handle_timedout_limit_sell(trade, order)
|
||||
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,
|
||||
mocker):
|
||||
def test_check_handle_timedout_partial_sell(default_conf, ticker, limit_sell_order_old_partial,
|
||||
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)
|
||||
cancel_order_mock = MagicMock()
|
||||
mocker.patch('freqtrade.rpc.init', MagicMock())
|
||||
|
@ -1,4 +1,5 @@
|
||||
python-bittrex==0.3.0
|
||||
python-binance==0.6.1
|
||||
SQLAlchemy==1.2.2
|
||||
python-telegram-bot==9.0.0
|
||||
arrow==0.12.1
|
||||
|
1
setup.py
1
setup.py
@ -22,6 +22,7 @@ setup(name='freqtrade',
|
||||
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
|
||||
install_requires=[
|
||||
'python-bittrex',
|
||||
'python-binance',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot',
|
||||
'arrow',
|
||||
|
4
setup.sh
4
setup.sh
@ -98,7 +98,7 @@ function config_generator () {
|
||||
read -p "Fiat currency: (Default: USD) " fiat_currency
|
||||
|
||||
echo "------------------------"
|
||||
echo "Bittrex config generator"
|
||||
echo "Exchange config generator"
|
||||
echo "------------------------"
|
||||
echo
|
||||
read -p "Exchange API key: " api_key
|
||||
@ -205,4 +205,4 @@ plot
|
||||
help
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
exit 0
|
||||
|
Loading…
Reference in New Issue
Block a user