Merge 56e6c563aa
into 53b1f7ac4d
This commit is contained in:
commit
3e1bddbdd1
@ -71,7 +71,7 @@ your current trades.
|
|||||||
|
|
||||||
### Exchange supported
|
### Exchange supported
|
||||||
- [x] Bittrex
|
- [x] Bittrex
|
||||||
- [ ] Binance
|
- [x] Binance
|
||||||
- [ ] Others
|
- [ ] Others
|
||||||
|
|
||||||
## Quick start
|
## 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.
|
| `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).
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
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']
|
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)
|
||||||
|
@ -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 )
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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 ...')
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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):
|
||||||
|
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():
|
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():
|
||||||
|
@ -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):
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
|
1
setup.py
1
setup.py
@ -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',
|
||||||
|
4
setup.sh
4
setup.sh
@ -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
|
||||||
@ -205,4 +205,4 @@ plot
|
|||||||
help
|
help
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
exit 0
|
exit 0
|
||||||
|
Loading…
Reference in New Issue
Block a user